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
@@ -8,7 +8,7 @@ import {
8
8
  CallToolRequestSchema,
9
9
  ListToolsRequestSchema,
10
10
  } from "@modelcontextprotocol/sdk/types.js";
11
- import { getConfig } from "../config.js";
11
+ import { getAllConfiguredTestCommands, getAllConfiguredDirectCommands, getConfig } from "../config.js";
12
12
  import { savePlan } from "../tools/savePlan.js";
13
13
  import { getPlan } from "../tools/getPlan.js";
14
14
  import { createTask } from "../tools/createTask.js";
@@ -28,7 +28,16 @@ import { getTaskSummary } from "../tools/getTaskSummary.js";
28
28
  import { waitForTask } from "../tools/waitForTask.js";
29
29
  import { errorPayload, PatchWardenError } from "../errors.js";
30
30
  import { auditTask } from "../tools/auditTask.js";
31
+ import { safeStatus } from "../tools/safeStatus.js";
32
+ import { logger } from "../logging.js";
31
33
  import { runTask } from "../runner/runTask.js";
34
+ import { createDirectSession } from "../tools/createDirectSession.js";
35
+ import { searchWorkspace } from "../tools/searchWorkspace.js";
36
+ import { applyPatch } from "../tools/applyPatch.js";
37
+ import { runVerification } from "../tools/runVerification.js";
38
+ import { finalizeDirectSession } from "../tools/finalizeDirectSession.js";
39
+ import { auditSession } from "../tools/auditSession.js";
40
+ import { syncFile } from "../tools/syncFile.js";
32
41
  import { TASK_TEMPLATE_NAMES } from "./taskTemplates.js";
33
42
  import {
34
43
  buildToolCatalogSnapshot,
@@ -56,19 +65,19 @@ export function getToolDefs(): ToolDef[] {
56
65
  const agentDescription = agentNames.length > 0
57
66
  ? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}`
58
67
  : "Configured local agent name. No agents are currently configured.";
59
- const testCommands = [...config.allowedTestCommands].sort();
68
+ const testCommands = getAllConfiguredTestCommands(config).sort();
60
69
  const tools: ToolDef[] = [
61
70
  {
62
71
  name: "save_plan",
63
72
  description:
64
- "Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution.",
73
+ "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.",
65
74
  inputSchema: {
66
75
  type: "object",
67
76
  properties: {
68
- title: { type: "string", description: "Plan title" },
69
- content: { type: "string", description: "Plan content in Markdown" },
77
+ title: { type: "string", description: "Plan title. Defaults to 'Inline plan' or 'Plan from file' when omitted." },
78
+ content: { type: "string", description: "Plan content in Markdown. Required unless plan_ref is provided." },
79
+ 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." },
70
80
  },
71
- required: ["title", "content"],
72
81
  },
73
82
  },
74
83
  {
@@ -110,7 +119,7 @@ export function getToolDefs(): ToolDef[] {
110
119
  {
111
120
  name: "create_task",
112
121
  description:
113
- "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.",
122
+ "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.",
114
123
  inputSchema: {
115
124
  type: "object",
116
125
  properties: {
@@ -120,7 +129,7 @@ export function getToolDefs(): ToolDef[] {
120
129
  template: {
121
130
  type: "string",
122
131
  enum: [...TASK_TEMPLATE_NAMES],
123
- description: "Built-in guarded task template. Use with goal; rollback_scope_violation also requires source_task_id.",
132
+ 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.",
124
133
  },
125
134
  goal: { type: "string", description: "Required task goal when template is supplied." },
126
135
  source_task_id: { type: "string", description: "Required source task for rollback_scope_violation review." },
@@ -147,7 +156,7 @@ export function getToolDefs(): ToolDef[] {
147
156
  type: "string",
148
157
  ...(testCommands.length > 0 ? { enum: testCommands } : {}),
149
158
  },
150
- description: "Recommended allow-listed commands PatchWarden runs independently after the agent exits.",
159
+ description: "Recommended exact-match commands PatchWarden runs independently after the agent exits. Repository-scoped commands are re-authorized after repo_path is resolved.",
151
160
  },
152
161
  timeout_seconds: {
153
162
  type: "integer",
@@ -156,8 +165,18 @@ export function getToolDefs(): ToolDef[] {
156
165
  default: config.defaultTaskTimeoutSeconds,
157
166
  description: `Total task timeout in seconds (default ${config.defaultTaskTimeoutSeconds}, max ${config.maxTaskTimeoutSeconds})`,
158
167
  },
168
+ execution_mode: {
169
+ type: "string",
170
+ enum: ["assess_only", "execute"],
171
+ default: "execute",
172
+ 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.",
173
+ },
174
+ assessment_id: {
175
+ type: "string",
176
+ 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.",
177
+ },
159
178
  },
160
- required: ["agent", "repo_path"],
179
+ required: [],
161
180
  },
162
181
  },
163
182
  {
@@ -232,13 +251,17 @@ export function getToolDefs(): ToolDef[] {
232
251
  {
233
252
  name: "read_workspace_file",
234
253
  description:
235
- "Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked.",
254
+ "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.",
236
255
  inputSchema: {
237
256
  type: "object",
238
257
  properties: {
239
258
  path: {
240
259
  type: "string",
241
- description: "Relative path to a file inside the workspace",
260
+ description: "Relative path to a file inside the workspace or session repo",
261
+ },
262
+ session_id: {
263
+ type: "string",
264
+ description: "Optional Direct session ID. When provided, read scope is limited to the session's repo_path and sha256 is returned.",
242
265
  },
243
266
  },
244
267
  required: ["path"],
@@ -372,11 +395,13 @@ export function getToolDefs(): ToolDef[] {
372
395
  {
373
396
  name: "get_task_summary",
374
397
  description:
375
- "Return one structured acceptance summary: terminal status, scope violations, verification evidence, changed files, artifact availability, warnings, and errors.",
398
+ "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.",
376
399
  inputSchema: {
377
400
  type: "object",
378
401
  properties: {
379
402
  task_id: { type: "string", description: "Task ID" },
403
+ view: { type: "string", enum: ["compact", "standard"], default: "standard", description: "Compact returns bounded acceptance evidence; standard preserves the full legacy summary." },
404
+ max_items: { type: "integer", minimum: 1, maximum: 50, default: 8, description: "Maximum entries per compact evidence group." },
380
405
  },
381
406
  required: ["task_id"],
382
407
  },
@@ -384,7 +409,7 @@ export function getToolDefs(): ToolDef[] {
384
409
  {
385
410
  name: "audit_task",
386
411
  description:
387
- "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.",
412
+ "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.",
388
413
  inputSchema: {
389
414
  type: "object",
390
415
  properties: {
@@ -393,8 +418,154 @@ export function getToolDefs(): ToolDef[] {
393
418
  required: ["task_id"],
394
419
  },
395
420
  },
421
+ {
422
+ name: "safe_status",
423
+ description:
424
+ "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.",
425
+ inputSchema: {
426
+ type: "object",
427
+ properties: {
428
+ task_id: { type: "string", description: "Task ID to check" },
429
+ },
430
+ required: ["task_id"],
431
+ },
432
+ },
396
433
  ];
397
434
 
435
+ // Direct session tools
436
+ const directCommands = getAllConfiguredDirectCommands(config);
437
+ tools.push({
438
+ name: "create_direct_session",
439
+ description:
440
+ "Create a Direct editing session for ChatGPT to apply patches directly. Requires enableDirectProfile: true in config.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ repo_path: {
445
+ type: "string",
446
+ description: "Repository path inside workspaceRoot (e.g., 'my-project')",
447
+ },
448
+ title: {
449
+ type: "string",
450
+ description: "Optional title describing the session's purpose",
451
+ },
452
+ },
453
+ required: ["repo_path"],
454
+ },
455
+ });
456
+
457
+ tools.push({
458
+ name: "search_workspace",
459
+ description:
460
+ "Search file contents (grep-like) within a Direct session's repo_path. Skips .git, node_modules, dist, release, and sensitive files.",
461
+ inputSchema: {
462
+ type: "object",
463
+ properties: {
464
+ session_id: { type: "string", description: "Session ID from create_direct_session" },
465
+ query: { type: "string", description: "Search query string" },
466
+ max_results: { type: "number", description: "Max results (default 20)" },
467
+ case_sensitive: { type: "boolean", description: "Case sensitive search (default false)" },
468
+ max_preview_chars: { type: "number", description: "Max preview chars per match (default 200)" },
469
+ include_globs: {
470
+ type: "array",
471
+ items: { type: "string" },
472
+ description: "Optional file name glob patterns to include (e.g., ['*.ts', '*.js'])",
473
+ },
474
+ },
475
+ required: ["session_id", "query"],
476
+ },
477
+ });
478
+
479
+ tools.push({
480
+ name: "apply_patch",
481
+ description:
482
+ "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.",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ session_id: { type: "string", description: "Session ID" },
487
+ path: { type: "string", description: "Relative file path within the session repo" },
488
+ expected_sha256: { type: "string", description: "Expected SHA-256 hash of the current file content" },
489
+ operations: {
490
+ type: "array",
491
+ items: {
492
+ type: "object",
493
+ properties: {
494
+ type: { type: "string", enum: ["replace_exact", "insert_before", "insert_after", "replace_whole_file"] },
495
+ old_text: { type: "string", description: "Text to find (required for replace_exact, insert_before, insert_after)" },
496
+ new_text: { type: "string", description: "Replacement or insertion text" },
497
+ occurrence: { type: "string", enum: ["first", "all", "exactly_once"], description: "Match mode for replace_exact (default first)" },
498
+ },
499
+ required: ["type", "new_text"],
500
+ },
501
+ },
502
+ },
503
+ required: ["session_id", "path", "expected_sha256", "operations"],
504
+ },
505
+ });
506
+
507
+ tools.push({
508
+ name: "run_verification",
509
+ description:
510
+ "Run a whitelisted verification command within a Direct session. Command must be in the Direct allowlist.",
511
+ inputSchema: {
512
+ type: "object",
513
+ properties: {
514
+ session_id: { type: "string", description: "Session ID" },
515
+ command: {
516
+ type: "string",
517
+ description: "Verification command to run",
518
+ ...(directCommands.length > 0 ? { enum: directCommands } : {}),
519
+ },
520
+ timeout_seconds: { type: "number", description: "Timeout in seconds (default 120)" },
521
+ },
522
+ required: ["session_id", "command"],
523
+ },
524
+ });
525
+
526
+ tools.push({
527
+ name: "finalize_direct_session",
528
+ description:
529
+ "Finalize a Direct session: capture after snapshot, generate diff/summary/change artifacts, mark session as finalized. Must be called before audit_session.",
530
+ inputSchema: {
531
+ type: "object",
532
+ properties: {
533
+ session_id: { type: "string", description: "Session ID to finalize" },
534
+ },
535
+ required: ["session_id"],
536
+ },
537
+ });
538
+
539
+ tools.push({
540
+ name: "audit_session",
541
+ description:
542
+ "Independently audit a Direct session's changes. Performs 16 deterministic checks and returns pass/warn/fail decision. Requires session to be finalized first.",
543
+ inputSchema: {
544
+ type: "object",
545
+ properties: {
546
+ session_id: { type: "string", description: "Session ID to audit" },
547
+ },
548
+ required: ["session_id"],
549
+ },
550
+ });
551
+
552
+ tools.push({
553
+ name: "sync_file",
554
+ description:
555
+ "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.",
556
+ inputSchema: {
557
+ type: "object",
558
+ properties: {
559
+ session_id: { type: "string", description: "Direct session ID" },
560
+ source_path: { type: "string", description: "Relative path to source file within repo" },
561
+ target_path: { type: "string", description: "Relative path to target file within repo" },
562
+ expected_source_sha256: { type: "string", description: "Optional: expected sha256 of source file" },
563
+ expected_target_sha256: { type: "string", description: "Optional: expected sha256 of target file before copy" },
564
+ },
565
+ required: ["session_id", "source_path", "target_path"],
566
+ },
567
+ });
568
+
398
569
  // run_task: only available when explicitly enabled
399
570
  if ((config as any).enableRunTaskTool === true) {
400
571
  tools.push({
@@ -412,7 +583,7 @@ export function getToolDefs(): ToolDef[] {
412
583
  }
413
584
 
414
585
  const profile = resolveToolProfile(config.toolProfile);
415
- const selected = selectToolsForProfile(tools, profile);
586
+ const selected = selectToolsForProfile(tools, profile, config.enableDirectProfile);
416
587
  buildToolCatalogSnapshot(selected, profile);
417
588
  return selected;
418
589
  }
@@ -425,13 +596,41 @@ export function getToolCatalogSnapshot(): ToolCatalogSnapshot {
425
596
 
426
597
  // ── Request handler ───────────────────────────────────────────────
427
598
 
599
+ function guardDirectProfileEnabled(): void {
600
+ const config = getConfig();
601
+ if (!config.enableDirectProfile) {
602
+ throw new PatchWardenError(
603
+ "direct_profile_disabled",
604
+ "Direct profile is disabled by local config.",
605
+ "Set enableDirectProfile: true in patchwarden.config.json to use Direct session tools.",
606
+ true,
607
+ { operation: "direct_tool_call" }
608
+ );
609
+ }
610
+ }
611
+
428
612
  export async function handleToolCall(name: string, args: Record<string, unknown> | undefined) {
613
+ const startTime = Date.now();
614
+ const taskId = args?.task_id ? String(args.task_id) : args?.session_id ? String(args.session_id) : undefined;
615
+ try {
616
+ const result = await handleToolCallInternal(name, args);
617
+ logger.audit(name, true, Date.now() - startTime, undefined, taskId);
618
+ return result;
619
+ } catch (err) {
620
+ const errorReason = err instanceof Error ? err.message : String(err);
621
+ logger.audit(name, false, Date.now() - startTime, errorReason, taskId);
622
+ throw err;
623
+ }
624
+ }
625
+
626
+ async function handleToolCallInternal(name: string, args: Record<string, unknown> | undefined) {
429
627
  switch (name) {
430
628
  case "save_plan": {
431
629
  return toResult(
432
630
  savePlan({
433
631
  title: String(args?.title ?? ""),
434
- content: String(args?.content ?? ""),
632
+ content: args?.content !== undefined ? String(args.content) : "",
633
+ plan_ref: args?.plan_ref ? String(args.plan_ref) : undefined,
435
634
  })
436
635
  );
437
636
  }
@@ -460,6 +659,8 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
460
659
  timeout_seconds: args?.timeout_seconds !== undefined
461
660
  ? Number(args.timeout_seconds)
462
661
  : undefined,
662
+ execution_mode: args?.execution_mode === "assess_only" ? "assess_only" : "execute",
663
+ assessment_id: args?.assessment_id ? String(args.assessment_id) : undefined,
463
664
  })
464
665
  );
465
666
  }
@@ -491,7 +692,11 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
491
692
  }
492
693
 
493
694
  case "read_workspace_file": {
494
- return toResult(readWorkspaceFile(String(args?.path ?? "")));
695
+ const sessionId = args?.session_id ? String(args.session_id) : undefined;
696
+ return toResult(readWorkspaceFile({
697
+ path: String(args?.path ?? ""),
698
+ session_id: sessionId,
699
+ }));
495
700
  }
496
701
 
497
702
  case "list_tasks": {
@@ -556,13 +761,20 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
556
761
  }
557
762
 
558
763
  case "get_task_summary": {
559
- return toResult(getTaskSummary(String(args?.task_id ?? "")));
764
+ return toResult(getTaskSummary(String(args?.task_id ?? ""), {
765
+ view: normalizeSummaryView(args?.view),
766
+ max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
767
+ }));
560
768
  }
561
769
 
562
770
  case "audit_task": {
563
771
  return toResult(auditTask(String(args?.task_id ?? "")));
564
772
  }
565
773
 
774
+ case "safe_status": {
775
+ return toResult(safeStatus(String(args?.task_id ?? "")));
776
+ }
777
+
566
778
  case "run_task": {
567
779
  const config = getConfig();
568
780
  if ((config as any).enableRunTaskTool !== true) {
@@ -575,6 +787,72 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
575
787
  return toResult(result);
576
788
  }
577
789
 
790
+ case "create_direct_session": {
791
+ guardDirectProfileEnabled();
792
+ return toResult(createDirectSession({
793
+ repo_path: String(args?.repo_path ?? ""),
794
+ title: args?.title ? String(args.title) : undefined,
795
+ }));
796
+ }
797
+
798
+ case "search_workspace": {
799
+ guardDirectProfileEnabled();
800
+ return toResult(searchWorkspace({
801
+ session_id: String(args?.session_id ?? ""),
802
+ query: String(args?.query ?? ""),
803
+ max_results: args?.max_results ? Number(args.max_results) : undefined,
804
+ case_sensitive: args?.case_sensitive !== undefined ? Boolean(args.case_sensitive) : undefined,
805
+ max_preview_chars: args?.max_preview_chars ? Number(args.max_preview_chars) : undefined,
806
+ include_globs: Array.isArray(args?.include_globs) ? args.include_globs.map(String) : undefined,
807
+ }));
808
+ }
809
+
810
+ case "apply_patch": {
811
+ guardDirectProfileEnabled();
812
+ return toResult(applyPatch({
813
+ session_id: String(args?.session_id ?? ""),
814
+ path: String(args?.path ?? ""),
815
+ expected_sha256: String(args?.expected_sha256 ?? ""),
816
+ operations: Array.isArray(args?.operations) ? args.operations as any : [],
817
+ }));
818
+ }
819
+
820
+ case "run_verification": {
821
+ guardDirectProfileEnabled();
822
+ return toResult(await runVerification({
823
+ session_id: String(args?.session_id ?? ""),
824
+ command: String(args?.command ?? ""),
825
+ timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
826
+ }));
827
+ }
828
+
829
+ case "finalize_direct_session": {
830
+ guardDirectProfileEnabled();
831
+ return toResult(finalizeDirectSession({
832
+ session_id: String(args?.session_id ?? ""),
833
+ }));
834
+ }
835
+
836
+ case "audit_session": {
837
+ guardDirectProfileEnabled();
838
+ return toResult(auditSession({
839
+ session_id: String(args?.session_id ?? ""),
840
+ }));
841
+ }
842
+
843
+ case "sync_file": {
844
+ guardDirectProfileEnabled();
845
+ return toResult(syncFile(
846
+ String(args?.session_id ?? ""),
847
+ String(args?.source_path ?? ""),
848
+ String(args?.target_path ?? ""),
849
+ {
850
+ expected_source_sha256: args?.expected_source_sha256 ? String(args.expected_source_sha256) : undefined,
851
+ expected_target_sha256: args?.expected_target_sha256 ? String(args.expected_target_sha256) : undefined,
852
+ }
853
+ ));
854
+ }
855
+
578
856
  default:
579
857
  throw new Error(`Unknown tool: ${name}`);
580
858
  }
@@ -590,6 +868,14 @@ function normalizeWaitSeconds(args: Record<string, unknown> | undefined): number
590
868
  return value === undefined ? undefined : Number(value);
591
869
  }
592
870
 
871
+ function normalizeSummaryView(value: unknown): "compact" | "standard" {
872
+ if (value === undefined) return "standard";
873
+ if (value !== "compact" && value !== "standard") {
874
+ throw new Error('view must be "compact" or "standard".');
875
+ }
876
+ return value;
877
+ }
878
+
593
879
  function toResult(data: unknown) {
594
880
  return {
595
881
  content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
@@ -647,7 +933,7 @@ export function registerTools(server: Server) {
647
933
  arguments: { detail: "self_diagnostic" },
648
934
  },
649
935
  connector_refresh_steps: [
650
- "1. Run Check-PatchWarden-Health.cmd locally to confirm the active profile and manifest hash.",
936
+ "1. Run PatchWarden.cmd health locally to confirm the active profile and manifest hash.",
651
937
  "2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
652
938
  "3. Open a NEW ChatGPT conversation; old conversations retain their cached tool catalog.",
653
939
  "4. Call health_check in the new conversation and verify tool_manifest_sha256 matches the local report.",
@@ -0,0 +1,58 @@
1
+ import {
2
+ readDirectSession,
3
+ appendDirectSessionVerificationRun,
4
+ } from "../direct/directSessionStore.js";
5
+ import { guardDirectSessionActive } from "../direct/directGuards.js";
6
+ import { runDirectVerification } from "../direct/directVerification.js";
7
+
8
+ export interface RunVerificationInput {
9
+ session_id: string;
10
+ command: string;
11
+ timeout_seconds?: number;
12
+ }
13
+
14
+ export interface RunVerificationOutput {
15
+ command: string;
16
+ exit_code: number | null;
17
+ passed: boolean;
18
+ timed_out: boolean;
19
+ stdout_tail: string;
20
+ stderr_tail: string;
21
+ log_path: string;
22
+ next_action: string;
23
+ }
24
+
25
+ export async function runVerification(
26
+ input: RunVerificationInput
27
+ ): Promise<RunVerificationOutput> {
28
+ // 1. Read session and guard active
29
+ const session = readDirectSession(input.session_id);
30
+ guardDirectSessionActive(session);
31
+
32
+ // 2. Call runDirectVerification with command, resolvedRepoPath,
33
+ // sessionId, and timeoutSeconds
34
+ const timeoutSeconds = input.timeout_seconds ?? 120;
35
+ const result = await runDirectVerification({
36
+ command: input.command,
37
+ resolvedRepoPath: session.resolved_repo_path,
38
+ sessionId: input.session_id,
39
+ timeoutSeconds,
40
+ });
41
+
42
+ // 3. Append verification run to session
43
+ appendDirectSessionVerificationRun(input.session_id, result.run);
44
+
45
+ // 4. Return result
46
+ return {
47
+ command: result.run.command,
48
+ exit_code: result.run.exit_code,
49
+ passed: result.run.passed,
50
+ timed_out: result.run.timed_out,
51
+ stdout_tail: result.run.stdout_tail,
52
+ stderr_tail: result.run.stderr_tail,
53
+ log_path: result.run.log_path,
54
+ next_action: result.run.passed
55
+ ? "Call finalize_direct_session to complete the session."
56
+ : "Review the verification output and apply_patch to fix issues.",
57
+ };
58
+ }
@@ -0,0 +1,96 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { getTasksDir, getConfig, type PatchWardenConfig } from "../config.js";
4
+ import { guardReadPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+ import type { TaskStatus, TaskPhase } from "./createTask.js";
7
+ import { readTaskRuntime } from "../taskRuntime.js";
8
+ import { readWatcherStatus } from "../watcherStatus.js";
9
+
10
+ export interface SafeStatusOutput {
11
+ task_id: string;
12
+ status: TaskStatus | "not_found";
13
+ phase: TaskPhase | null;
14
+ created_at: string | null;
15
+ started_at: string | null;
16
+ updated_at: string | null;
17
+ finished_at: string | null;
18
+ last_heartbeat_at: string | null;
19
+ current_command: string | null;
20
+ verify_status: "passed" | "failed" | "skipped" | null;
21
+ artifact_status: string | null;
22
+ watcher_state: string | null;
23
+ error_code: string | null;
24
+ error_summary: string | null;
25
+ }
26
+
27
+ export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStatusOutput {
28
+ const cfg = config || getConfig();
29
+ const tasksDir = getTasksDir(cfg);
30
+
31
+ const taskDir = resolve(tasksDir, taskId);
32
+ const statusFile = join(taskDir, "status.json");
33
+
34
+ // Check existence BEFORE guardReadPath so that a non-existent task
35
+ // returns a structured not_found response instead of throwing.
36
+ if (!existsSync(statusFile)) {
37
+ return {
38
+ task_id: taskId,
39
+ status: "not_found",
40
+ phase: null,
41
+ created_at: null,
42
+ started_at: null,
43
+ updated_at: null,
44
+ finished_at: null,
45
+ last_heartbeat_at: null,
46
+ current_command: null,
47
+ verify_status: null,
48
+ artifact_status: null,
49
+ watcher_state: null,
50
+ error_code: null,
51
+ error_summary: null,
52
+ };
53
+ }
54
+
55
+ guardReadPath(statusFile, cfg.workspaceRoot, cfg.tasksDir);
56
+ guardSensitivePath(statusFile);
57
+
58
+ const raw = readFileSync(statusFile, "utf-8");
59
+ const status = JSON.parse(raw) as Record<string, unknown>;
60
+ const runtime = readTaskRuntime(taskDir);
61
+ const phase = (runtime.phase || status.phase || "queued") as TaskPhase;
62
+ const watcher = readWatcherStatus(cfg);
63
+
64
+ // Extract short error summary — never the full error text
65
+ let errorCode: string | null = null;
66
+ let errorSummary: string | null = null;
67
+ if (status.error && typeof status.error === "string") {
68
+ const errorStr = status.error as string;
69
+ // Keep only the first 200 chars as summary
70
+ errorSummary = errorStr.length > 200 ? errorStr.slice(0, 200) + "..." : errorStr;
71
+ // Try to extract an error code from the status
72
+ if (typeof status.status === "string" && (status.status as string).startsWith("failed")) {
73
+ errorCode = status.status as string;
74
+ }
75
+ } else if (typeof status.status === "string" && (status.status as string).startsWith("failed")) {
76
+ errorCode = status.status as string;
77
+ errorSummary = `Task ended with status: ${status.status}`;
78
+ }
79
+
80
+ return {
81
+ task_id: taskId,
82
+ status: (status.status as TaskStatus) || "not_found",
83
+ phase,
84
+ created_at: typeof status.created_at === "string" ? status.created_at : null,
85
+ started_at: typeof status.started_at === "string" ? status.started_at : null,
86
+ updated_at: typeof status.updated_at === "string" ? status.updated_at : null,
87
+ finished_at: typeof status.finished_at === "string" ? status.finished_at : null,
88
+ last_heartbeat_at: runtime.last_heartbeat_at || (typeof status.last_heartbeat_at === "string" ? status.last_heartbeat_at : null),
89
+ current_command: runtime.current_command ?? (typeof status.current_command === "string" ? status.current_command : null) ?? null,
90
+ verify_status: typeof status.verify_status === "string" ? (status.verify_status as "passed" | "failed" | "skipped") : null,
91
+ artifact_status: typeof status.artifact_status === "string" ? status.artifact_status : null,
92
+ watcher_state: watcher.status,
93
+ error_code: errorCode,
94
+ error_summary: errorSummary,
95
+ };
96
+ }