patchwarden 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) 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/runner/agentInvocation.d.ts +23 -0
  24. package/dist/runner/agentInvocation.js +106 -0
  25. package/dist/runner/changeCapture.d.ts +29 -0
  26. package/dist/runner/changeCapture.js +107 -5
  27. package/dist/runner/runTask.js +70 -44
  28. package/dist/runner/simpleProcess.d.ts +21 -0
  29. package/dist/runner/simpleProcess.js +206 -0
  30. package/dist/runner/watch.js +17 -2
  31. package/dist/security/commandGuard.d.ts +2 -1
  32. package/dist/security/commandGuard.js +25 -3
  33. package/dist/security/riskEngine.d.ts +27 -0
  34. package/dist/security/riskEngine.js +118 -0
  35. package/dist/security/runtimeGuard.d.ts +6 -0
  36. package/dist/security/runtimeGuard.js +28 -0
  37. package/dist/smoke-test.js +1353 -7
  38. package/dist/tools/applyPatch.d.ts +16 -0
  39. package/dist/tools/applyPatch.js +41 -0
  40. package/dist/tools/auditSession.d.ts +5 -0
  41. package/dist/tools/auditSession.js +12 -0
  42. package/dist/tools/auditTask.d.ts +7 -0
  43. package/dist/tools/auditTask.js +94 -2
  44. package/dist/tools/createDirectSession.d.ts +14 -0
  45. package/dist/tools/createDirectSession.js +54 -0
  46. package/dist/tools/createTask.d.ts +48 -1
  47. package/dist/tools/createTask.js +298 -47
  48. package/dist/tools/finalizeDirectSession.d.ts +19 -0
  49. package/dist/tools/finalizeDirectSession.js +84 -0
  50. package/dist/tools/getTaskSummary.d.ts +47 -0
  51. package/dist/tools/getTaskSummary.js +64 -1
  52. package/dist/tools/healthCheck.d.ts +22 -12
  53. package/dist/tools/healthCheck.js +23 -6
  54. package/dist/tools/readWorkspaceFile.d.ts +7 -1
  55. package/dist/tools/readWorkspaceFile.js +48 -2
  56. package/dist/tools/registry.js +217 -19
  57. package/dist/tools/runVerification.d.ts +16 -0
  58. package/dist/tools/runVerification.js +32 -0
  59. package/dist/tools/savePlan.d.ts +1 -0
  60. package/dist/tools/savePlan.js +38 -7
  61. package/dist/tools/searchWorkspace.d.ts +19 -0
  62. package/dist/tools/searchWorkspace.js +205 -0
  63. package/dist/tools/taskTemplates.js +2 -1
  64. package/dist/tools/toolCatalog.d.ts +3 -2
  65. package/dist/tools/toolCatalog.js +32 -5
  66. package/dist/tools/waitForTask.d.ts +2 -2
  67. package/dist/tools/waitForTask.js +1 -1
  68. package/dist/version.d.ts +2 -2
  69. package/dist/version.js +2 -2
  70. package/docs/chatgpt-usage.md +101 -0
  71. package/docs/release-checklist.md +14 -0
  72. package/docs/release-v0.6.0.md +71 -0
  73. package/examples/config.example.json +5 -0
  74. package/examples/openai-tunnel/README.md +11 -4
  75. package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
  76. package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
  77. package/package.json +12 -8
  78. package/scripts/brand-check.js +58 -12
  79. package/scripts/control-smoke.js +206 -0
  80. package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
  81. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  82. package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
  83. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
  84. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
  85. package/scripts/lifecycle-smoke.js +66 -6
  86. package/scripts/manage-patchwarden.ps1 +639 -0
  87. package/scripts/mcp-manifest-check.js +100 -58
  88. package/scripts/mcp-smoke.js +157 -2
  89. package/scripts/pack-clean.js +3 -4
  90. package/scripts/package-manifest-check.js +78 -0
  91. package/scripts/patchwarden-mcp-direct.cmd +7 -0
  92. package/scripts/patchwarden-mcp-stdio.cmd +1 -1
  93. package/scripts/restart-patchwarden.ps1 +5 -4
  94. package/scripts/start-patchwarden-tunnel.ps1 +261 -30
  95. package/scripts/tunnel-supervisor-smoke.js +36 -4
  96. package/scripts/watcher-supervisor-smoke.js +10 -6
  97. package/src/assessments/agentAssessor.ts +324 -0
  98. package/src/assessments/assessmentStore.ts +426 -0
  99. package/src/assessments/confirmCli.ts +29 -0
  100. package/src/config.ts +178 -4
  101. package/src/direct/directAudit.ts +400 -0
  102. package/src/direct/directGuards.ts +279 -0
  103. package/src/direct/directPatch.ts +258 -0
  104. package/src/direct/directSessionStore.ts +345 -0
  105. package/src/direct/directVerification.ts +138 -0
  106. package/src/doctor.ts +103 -7
  107. package/src/runner/agentInvocation.ts +125 -0
  108. package/src/runner/changeCapture.ts +140 -5
  109. package/src/runner/runTask.ts +61 -43
  110. package/src/runner/simpleProcess.ts +223 -0
  111. package/src/runner/watch.ts +18 -2
  112. package/src/security/commandGuard.ts +46 -4
  113. package/src/security/riskEngine.ts +160 -0
  114. package/src/security/runtimeGuard.ts +41 -0
  115. package/src/smoke-test.ts +1291 -4
  116. package/src/tools/applyPatch.ts +86 -0
  117. package/src/tools/auditSession.ts +28 -0
  118. package/src/tools/auditTask.ts +100 -2
  119. package/src/tools/createDirectSession.ts +113 -0
  120. package/src/tools/createTask.ts +405 -55
  121. package/src/tools/finalizeDirectSession.ts +144 -0
  122. package/src/tools/getTaskSummary.ts +111 -1
  123. package/src/tools/healthCheck.ts +23 -6
  124. package/src/tools/readWorkspaceFile.ts +85 -2
  125. package/src/tools/registry.ts +242 -19
  126. package/src/tools/runVerification.ts +58 -0
  127. package/src/tools/savePlan.ts +57 -7
  128. package/src/tools/searchWorkspace.ts +275 -0
  129. package/src/tools/taskTemplates.ts +2 -1
  130. package/src/tools/toolCatalog.ts +35 -6
  131. package/src/tools/waitForTask.ts +3 -3
  132. package/src/version.ts +2 -2
  133. package/tsconfig.json +18 -17
  134. package/Check-PatchWarden-Health.cmd +0 -6
  135. package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
  136. package/Restart-PatchWarden.cmd +0 -19
  137. package/Start-PatchWarden-Tunnel.cmd +0 -7
  138. package/docs/release-v0.3.0.md +0 -43
  139. 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";
@@ -29,6 +29,12 @@ import { waitForTask } from "../tools/waitForTask.js";
29
29
  import { errorPayload, PatchWardenError } from "../errors.js";
30
30
  import { auditTask } from "../tools/auditTask.js";
31
31
  import { runTask } from "../runner/runTask.js";
32
+ import { createDirectSession } from "../tools/createDirectSession.js";
33
+ import { searchWorkspace } from "../tools/searchWorkspace.js";
34
+ import { applyPatch } from "../tools/applyPatch.js";
35
+ import { runVerification } from "../tools/runVerification.js";
36
+ import { finalizeDirectSession } from "../tools/finalizeDirectSession.js";
37
+ import { auditSession } from "../tools/auditSession.js";
32
38
  import { TASK_TEMPLATE_NAMES } from "./taskTemplates.js";
33
39
  import {
34
40
  buildToolCatalogSnapshot,
@@ -56,19 +62,19 @@ export function getToolDefs(): ToolDef[] {
56
62
  const agentDescription = agentNames.length > 0
57
63
  ? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}`
58
64
  : "Configured local agent name. No agents are currently configured.";
59
- const testCommands = [...config.allowedTestCommands].sort();
65
+ const testCommands = getAllConfiguredTestCommands(config).sort();
60
66
  const tools: ToolDef[] = [
61
67
  {
62
68
  name: "save_plan",
63
69
  description:
64
- "Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution.",
70
+ "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
71
  inputSchema: {
66
72
  type: "object",
67
73
  properties: {
68
- title: { type: "string", description: "Plan title" },
69
- content: { type: "string", description: "Plan content in Markdown" },
74
+ title: { type: "string", description: "Plan title. Defaults to 'Inline plan' or 'Plan from file' when omitted." },
75
+ content: { type: "string", description: "Plan content in Markdown. Required unless plan_ref is provided." },
76
+ 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
77
  },
71
- required: ["title", "content"],
72
78
  },
73
79
  },
74
80
  {
@@ -110,7 +116,7 @@ export function getToolDefs(): ToolDef[] {
110
116
  {
111
117
  name: "create_task",
112
118
  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.",
119
+ "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
120
  inputSchema: {
115
121
  type: "object",
116
122
  properties: {
@@ -120,7 +126,7 @@ export function getToolDefs(): ToolDef[] {
120
126
  template: {
121
127
  type: "string",
122
128
  enum: [...TASK_TEMPLATE_NAMES],
123
- description: "Built-in guarded task template. Use with goal; rollback_scope_violation also requires source_task_id.",
129
+ 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
130
  },
125
131
  goal: { type: "string", description: "Required task goal when template is supplied." },
126
132
  source_task_id: { type: "string", description: "Required source task for rollback_scope_violation review." },
@@ -147,7 +153,7 @@ export function getToolDefs(): ToolDef[] {
147
153
  type: "string",
148
154
  ...(testCommands.length > 0 ? { enum: testCommands } : {}),
149
155
  },
150
- description: "Recommended allow-listed commands PatchWarden runs independently after the agent exits.",
156
+ description: "Recommended exact-match commands PatchWarden runs independently after the agent exits. Repository-scoped commands are re-authorized after repo_path is resolved.",
151
157
  },
152
158
  timeout_seconds: {
153
159
  type: "integer",
@@ -156,8 +162,18 @@ export function getToolDefs(): ToolDef[] {
156
162
  default: config.defaultTaskTimeoutSeconds,
157
163
  description: `Total task timeout in seconds (default ${config.defaultTaskTimeoutSeconds}, max ${config.maxTaskTimeoutSeconds})`,
158
164
  },
165
+ execution_mode: {
166
+ type: "string",
167
+ enum: ["assess_only", "execute"],
168
+ default: "execute",
169
+ 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.",
170
+ },
171
+ assessment_id: {
172
+ type: "string",
173
+ 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.",
174
+ },
159
175
  },
160
- required: ["agent", "repo_path"],
176
+ required: [],
161
177
  },
162
178
  },
163
179
  {
@@ -232,13 +248,17 @@ export function getToolDefs(): ToolDef[] {
232
248
  {
233
249
  name: "read_workspace_file",
234
250
  description:
235
- "Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked.",
251
+ "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
252
  inputSchema: {
237
253
  type: "object",
238
254
  properties: {
239
255
  path: {
240
256
  type: "string",
241
- description: "Relative path to a file inside the workspace",
257
+ description: "Relative path to a file inside the workspace or session repo",
258
+ },
259
+ session_id: {
260
+ type: "string",
261
+ description: "Optional Direct session ID. When provided, read scope is limited to the session's repo_path and sha256 is returned.",
242
262
  },
243
263
  },
244
264
  required: ["path"],
@@ -372,11 +392,13 @@ export function getToolDefs(): ToolDef[] {
372
392
  {
373
393
  name: "get_task_summary",
374
394
  description:
375
- "Return one structured acceptance summary: terminal status, scope violations, verification evidence, changed files, artifact availability, warnings, and errors.",
395
+ "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
396
  inputSchema: {
377
397
  type: "object",
378
398
  properties: {
379
399
  task_id: { type: "string", description: "Task ID" },
400
+ view: { type: "string", enum: ["compact", "standard"], default: "standard", description: "Compact returns bounded acceptance evidence; standard preserves the full legacy summary." },
401
+ max_items: { type: "integer", minimum: 1, maximum: 50, default: 8, description: "Maximum entries per compact evidence group." },
380
402
  },
381
403
  required: ["task_id"],
382
404
  },
@@ -384,7 +406,7 @@ export function getToolDefs(): ToolDef[] {
384
406
  {
385
407
  name: "audit_task",
386
408
  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.",
409
+ "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
410
  inputSchema: {
389
411
  type: "object",
390
412
  properties: {
@@ -395,6 +417,123 @@ export function getToolDefs(): ToolDef[] {
395
417
  },
396
418
  ];
397
419
 
420
+ // Direct session tools
421
+ const directCommands = getAllConfiguredDirectCommands(config);
422
+ tools.push({
423
+ name: "create_direct_session",
424
+ description:
425
+ "Create a Direct editing session for ChatGPT to apply patches directly. Requires enableDirectProfile: true in config.",
426
+ inputSchema: {
427
+ type: "object",
428
+ properties: {
429
+ repo_path: {
430
+ type: "string",
431
+ description: "Repository path inside workspaceRoot (e.g., 'my-project')",
432
+ },
433
+ title: {
434
+ type: "string",
435
+ description: "Optional title describing the session's purpose",
436
+ },
437
+ },
438
+ required: ["repo_path"],
439
+ },
440
+ });
441
+
442
+ tools.push({
443
+ name: "search_workspace",
444
+ description:
445
+ "Search file contents (grep-like) within a Direct session's repo_path. Skips .git, node_modules, dist, release, and sensitive files.",
446
+ inputSchema: {
447
+ type: "object",
448
+ properties: {
449
+ session_id: { type: "string", description: "Session ID from create_direct_session" },
450
+ query: { type: "string", description: "Search query string" },
451
+ max_results: { type: "number", description: "Max results (default 20)" },
452
+ case_sensitive: { type: "boolean", description: "Case sensitive search (default false)" },
453
+ max_preview_chars: { type: "number", description: "Max preview chars per match (default 200)" },
454
+ include_globs: {
455
+ type: "array",
456
+ items: { type: "string" },
457
+ description: "Optional file name glob patterns to include (e.g., ['*.ts', '*.js'])",
458
+ },
459
+ },
460
+ required: ["session_id", "query"],
461
+ },
462
+ });
463
+
464
+ tools.push({
465
+ name: "apply_patch",
466
+ description:
467
+ "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.",
468
+ inputSchema: {
469
+ type: "object",
470
+ properties: {
471
+ session_id: { type: "string", description: "Session ID" },
472
+ path: { type: "string", description: "Relative file path within the session repo" },
473
+ expected_sha256: { type: "string", description: "Expected SHA-256 hash of the current file content" },
474
+ operations: {
475
+ type: "array",
476
+ items: {
477
+ type: "object",
478
+ properties: {
479
+ type: { type: "string", enum: ["replace_exact", "insert_before", "insert_after", "replace_whole_file"] },
480
+ old_text: { type: "string", description: "Text to find (required for replace_exact, insert_before, insert_after)" },
481
+ new_text: { type: "string", description: "Replacement or insertion text" },
482
+ occurrence: { type: "string", enum: ["first", "all", "exactly_once"], description: "Match mode for replace_exact (default first)" },
483
+ },
484
+ required: ["type", "new_text"],
485
+ },
486
+ },
487
+ },
488
+ required: ["session_id", "path", "expected_sha256", "operations"],
489
+ },
490
+ });
491
+
492
+ tools.push({
493
+ name: "run_verification",
494
+ description:
495
+ "Run a whitelisted verification command within a Direct session. Command must be in the Direct allowlist.",
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: {
499
+ session_id: { type: "string", description: "Session ID" },
500
+ command: {
501
+ type: "string",
502
+ description: "Verification command to run",
503
+ ...(directCommands.length > 0 ? { enum: directCommands } : {}),
504
+ },
505
+ timeout_seconds: { type: "number", description: "Timeout in seconds (default 120)" },
506
+ },
507
+ required: ["session_id", "command"],
508
+ },
509
+ });
510
+
511
+ tools.push({
512
+ name: "finalize_direct_session",
513
+ description:
514
+ "Finalize a Direct session: capture after snapshot, generate diff/summary/change artifacts, mark session as finalized. Must be called before audit_session.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ session_id: { type: "string", description: "Session ID to finalize" },
519
+ },
520
+ required: ["session_id"],
521
+ },
522
+ });
523
+
524
+ tools.push({
525
+ name: "audit_session",
526
+ description:
527
+ "Independently audit a Direct session's changes. Performs 16 deterministic checks and returns pass/warn/fail decision. Requires session to be finalized first.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ session_id: { type: "string", description: "Session ID to audit" },
532
+ },
533
+ required: ["session_id"],
534
+ },
535
+ });
536
+
398
537
  // run_task: only available when explicitly enabled
399
538
  if ((config as any).enableRunTaskTool === true) {
400
539
  tools.push({
@@ -412,7 +551,7 @@ export function getToolDefs(): ToolDef[] {
412
551
  }
413
552
 
414
553
  const profile = resolveToolProfile(config.toolProfile);
415
- const selected = selectToolsForProfile(tools, profile);
554
+ const selected = selectToolsForProfile(tools, profile, config.enableDirectProfile);
416
555
  buildToolCatalogSnapshot(selected, profile);
417
556
  return selected;
418
557
  }
@@ -425,13 +564,27 @@ export function getToolCatalogSnapshot(): ToolCatalogSnapshot {
425
564
 
426
565
  // ── Request handler ───────────────────────────────────────────────
427
566
 
567
+ function guardDirectProfileEnabled(): void {
568
+ const config = getConfig();
569
+ if (!config.enableDirectProfile) {
570
+ throw new PatchWardenError(
571
+ "direct_profile_disabled",
572
+ "Direct profile is disabled by local config.",
573
+ "Set enableDirectProfile: true in patchwarden.config.json to use Direct session tools.",
574
+ true,
575
+ { operation: "direct_tool_call" }
576
+ );
577
+ }
578
+ }
579
+
428
580
  export async function handleToolCall(name: string, args: Record<string, unknown> | undefined) {
429
581
  switch (name) {
430
582
  case "save_plan": {
431
583
  return toResult(
432
584
  savePlan({
433
585
  title: String(args?.title ?? ""),
434
- content: String(args?.content ?? ""),
586
+ content: args?.content !== undefined ? String(args.content) : "",
587
+ plan_ref: args?.plan_ref ? String(args.plan_ref) : undefined,
435
588
  })
436
589
  );
437
590
  }
@@ -460,6 +613,8 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
460
613
  timeout_seconds: args?.timeout_seconds !== undefined
461
614
  ? Number(args.timeout_seconds)
462
615
  : undefined,
616
+ execution_mode: args?.execution_mode === "assess_only" ? "assess_only" : "execute",
617
+ assessment_id: args?.assessment_id ? String(args.assessment_id) : undefined,
463
618
  })
464
619
  );
465
620
  }
@@ -491,7 +646,11 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
491
646
  }
492
647
 
493
648
  case "read_workspace_file": {
494
- return toResult(readWorkspaceFile(String(args?.path ?? "")));
649
+ const sessionId = args?.session_id ? String(args.session_id) : undefined;
650
+ return toResult(readWorkspaceFile({
651
+ path: String(args?.path ?? ""),
652
+ session_id: sessionId,
653
+ }));
495
654
  }
496
655
 
497
656
  case "list_tasks": {
@@ -556,7 +715,10 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
556
715
  }
557
716
 
558
717
  case "get_task_summary": {
559
- return toResult(getTaskSummary(String(args?.task_id ?? "")));
718
+ return toResult(getTaskSummary(String(args?.task_id ?? ""), {
719
+ view: normalizeSummaryView(args?.view),
720
+ max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
721
+ }));
560
722
  }
561
723
 
562
724
  case "audit_task": {
@@ -575,6 +737,59 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
575
737
  return toResult(result);
576
738
  }
577
739
 
740
+ case "create_direct_session": {
741
+ guardDirectProfileEnabled();
742
+ return toResult(createDirectSession({
743
+ repo_path: String(args?.repo_path ?? ""),
744
+ title: args?.title ? String(args.title) : undefined,
745
+ }));
746
+ }
747
+
748
+ case "search_workspace": {
749
+ guardDirectProfileEnabled();
750
+ return toResult(searchWorkspace({
751
+ session_id: String(args?.session_id ?? ""),
752
+ query: String(args?.query ?? ""),
753
+ max_results: args?.max_results ? Number(args.max_results) : undefined,
754
+ case_sensitive: args?.case_sensitive !== undefined ? Boolean(args.case_sensitive) : undefined,
755
+ max_preview_chars: args?.max_preview_chars ? Number(args.max_preview_chars) : undefined,
756
+ include_globs: Array.isArray(args?.include_globs) ? args.include_globs.map(String) : undefined,
757
+ }));
758
+ }
759
+
760
+ case "apply_patch": {
761
+ guardDirectProfileEnabled();
762
+ return toResult(applyPatch({
763
+ session_id: String(args?.session_id ?? ""),
764
+ path: String(args?.path ?? ""),
765
+ expected_sha256: String(args?.expected_sha256 ?? ""),
766
+ operations: Array.isArray(args?.operations) ? args.operations as any : [],
767
+ }));
768
+ }
769
+
770
+ case "run_verification": {
771
+ guardDirectProfileEnabled();
772
+ return toResult(await runVerification({
773
+ session_id: String(args?.session_id ?? ""),
774
+ command: String(args?.command ?? ""),
775
+ timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
776
+ }));
777
+ }
778
+
779
+ case "finalize_direct_session": {
780
+ guardDirectProfileEnabled();
781
+ return toResult(finalizeDirectSession({
782
+ session_id: String(args?.session_id ?? ""),
783
+ }));
784
+ }
785
+
786
+ case "audit_session": {
787
+ guardDirectProfileEnabled();
788
+ return toResult(auditSession({
789
+ session_id: String(args?.session_id ?? ""),
790
+ }));
791
+ }
792
+
578
793
  default:
579
794
  throw new Error(`Unknown tool: ${name}`);
580
795
  }
@@ -590,6 +805,14 @@ function normalizeWaitSeconds(args: Record<string, unknown> | undefined): number
590
805
  return value === undefined ? undefined : Number(value);
591
806
  }
592
807
 
808
+ function normalizeSummaryView(value: unknown): "compact" | "standard" {
809
+ if (value === undefined) return "standard";
810
+ if (value !== "compact" && value !== "standard") {
811
+ throw new Error('view must be "compact" or "standard".');
812
+ }
813
+ return value;
814
+ }
815
+
593
816
  function toResult(data: unknown) {
594
817
  return {
595
818
  content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
@@ -647,7 +870,7 @@ export function registerTools(server: Server) {
647
870
  arguments: { detail: "self_diagnostic" },
648
871
  },
649
872
  connector_refresh_steps: [
650
- "1. Run Check-PatchWarden-Health.cmd locally to confirm the active profile and manifest hash.",
873
+ "1. Run PatchWarden.cmd health locally to confirm the active profile and manifest hash.",
651
874
  "2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
652
875
  "3. Open a NEW ChatGPT conversation; old conversations retain their cached tool catalog.",
653
876
  "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
+ }
@@ -1,12 +1,14 @@
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
 
7
8
  export interface SavePlanInput {
8
9
  title: string;
9
10
  content: string;
11
+ plan_ref?: string;
10
12
  }
11
13
 
12
14
  export interface SavePlanOutput {
@@ -19,9 +21,57 @@ export function savePlan(input: SavePlanInput): SavePlanOutput {
19
21
  const config = getConfig();
20
22
  const plansDir = getPlansDir(config);
21
23
 
22
- guardPlanContent(input.title, input.content);
24
+ let content = input.content || "";
25
+ let title = input.title || "";
23
26
 
24
- const planId = `plan_${Date.now()}_${sanitizeTitle(input.title)}`;
27
+ if (input.plan_ref) {
28
+ // 收缩 #3: plan_ref 只能读取 .patchwarden/plans 内文件,禁止任意路径
29
+ // plan_ref is relative to plansDir, not workspaceRoot
30
+ const targetPath = resolve(plansDir, input.plan_ref);
31
+ // Guard: must stay inside workspace and inside plansDir
32
+ guardPath(targetPath, config.workspaceRoot, config.plansDir);
33
+ const relativeToPlans = relative(plansDir, targetPath);
34
+ if (relativeToPlans.startsWith("..")) {
35
+ throw new PatchWardenError(
36
+ "plan_ref_outside_plans_dir",
37
+ `plan_ref must point to a file inside .patchwarden/plans.`,
38
+ "Use a path relative to .patchwarden/plans/.",
39
+ true,
40
+ { plan_ref: input.plan_ref, resolved: targetPath }
41
+ );
42
+ }
43
+ if (!existsSync(targetPath)) {
44
+ throw new PatchWardenError(
45
+ "plan_ref_not_found",
46
+ `plan_ref file not found: "${input.plan_ref}".`,
47
+ "Place the plan file under .patchwarden/plans/ first, then reference it.",
48
+ true,
49
+ { plan_ref: input.plan_ref }
50
+ );
51
+ }
52
+ content = readFileSync(targetPath, "utf-8");
53
+ if (!title || title.trim() === "") {
54
+ title = "Plan from file";
55
+ }
56
+ } else {
57
+ // Without plan_ref, content is required
58
+ if (!content || content.trim() === "") {
59
+ throw new PatchWardenError(
60
+ "plan_content_required",
61
+ "save_plan requires content or plan_ref.",
62
+ "Pass content with the plan text, or use plan_ref to load a file from .patchwarden/plans."
63
+ );
64
+ }
65
+ }
66
+
67
+ // Default title when empty
68
+ if (!title || title.trim() === "") {
69
+ title = "Inline plan";
70
+ }
71
+
72
+ guardPlanContent(title, content);
73
+
74
+ const planId = `plan_${Date.now()}_${sanitizeTitle(title)}`;
25
75
  const planDir = resolve(plansDir, planId);
26
76
 
27
77
  // Guards: plan dir & file must stay inside workspace
@@ -29,13 +79,13 @@ export function savePlan(input: SavePlanInput): SavePlanOutput {
29
79
  mkdirSync(planDir, { recursive: true });
30
80
 
31
81
  const planFile = join(planDir, "plan.md");
32
- const header = `# ${input.title}\n\n> Plan ID: ${planId}\n> Created: ${new Date().toISOString()}\n\n`;
33
- writeFileSync(planFile, header + input.content, "utf-8");
82
+ const header = `# ${title}\n\n> Plan ID: ${planId}\n> Created: ${new Date().toISOString()}\n\n`;
83
+ writeFileSync(planFile, header + content, "utf-8");
34
84
 
35
85
  return {
36
86
  plan_id: planId,
37
87
  path: planFile,
38
- title: input.title,
88
+ title: title,
39
89
  };
40
90
  }
41
91