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
@@ -1,35 +1,64 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
6
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
- import { CHATGPT_CORE_TOOL_NAMES } from "../dist/tools/toolCatalog.js";
7
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "../dist/tools/toolCatalog.js";
7
8
 
8
9
  const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
9
10
  const jsonOnly = process.argv.includes("--json");
10
- const expectedTools = [...CHATGPT_CORE_TOOL_NAMES];
11
+ const profileIndex = process.argv.indexOf("--profile");
12
+ const profile = profileIndex >= 0 ? process.argv[profileIndex + 1] : "chatgpt_core";
13
+ if (profile !== "chatgpt_core" && profile !== "chatgpt_direct") {
14
+ throw new Error(`Unsupported manifest profile "${profile}".`);
15
+ }
16
+ const expectedTools = profile === "chatgpt_direct"
17
+ ? [...CHATGPT_DIRECT_TOOL_NAMES]
18
+ : [...CHATGPT_CORE_TOOL_NAMES];
19
+ const defaultConfigPath = resolve(root, "patchwarden.config.json");
20
+ const transportEnv = {
21
+ ...process.env,
22
+ PATCHWARDEN_TOOL_PROFILE: profile,
23
+ };
24
+
25
+ // When checking chatgpt_direct profile, ensure enableDirectProfile is true
26
+ // by creating a temporary config if needed.
27
+ let tempConfigPath = null;
28
+ if (profile === "chatgpt_direct") {
29
+ const baseConfig = existsSync(defaultConfigPath)
30
+ ? JSON.parse(readFileSync(defaultConfigPath, "utf-8"))
31
+ : {};
32
+ if (!baseConfig.enableDirectProfile) {
33
+ const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
34
+ const { tmpdir } = await import("node:os");
35
+ const { join } = await import("node:path");
36
+ tempConfigPath = join(mkdtempSync(join(tmpdir(), "pw-direct-")), "config.json");
37
+ writeFileSync(tempConfigPath, JSON.stringify({ ...baseConfig, enableDirectProfile: true }, null, 2), "utf-8");
38
+ transportEnv.PATCHWARDEN_CONFIG = tempConfigPath;
39
+ }
40
+ }
11
41
 
12
- const transport = process.platform === "win32"
13
- ? new StdioClientTransport({
14
- command: "cmd.exe",
15
- args: ["/d", "/c", resolve(root, "scripts", "patchwarden-mcp-stdio.cmd")],
16
- cwd: root,
17
- stderr: "pipe",
18
- })
19
- : new StdioClientTransport({
20
- command: process.execPath,
21
- args: [resolve(root, "dist", "index.js")],
22
- cwd: root,
23
- env: {
24
- ...process.env,
25
- PATCHWARDEN_CONFIG: resolve(root, "patchwarden.config.json"),
26
- PATCHWARDEN_TOOL_PROFILE: "chatgpt_core",
27
- },
28
- stderr: "pipe",
29
- });
42
+ if (!transportEnv.PATCHWARDEN_CONFIG) {
43
+ if (process.env.PATCHWARDEN_CONFIG) {
44
+ transportEnv.PATCHWARDEN_CONFIG = process.env.PATCHWARDEN_CONFIG;
45
+ } else if (existsSync(defaultConfigPath)) {
46
+ transportEnv.PATCHWARDEN_CONFIG = defaultConfigPath;
47
+ } else {
48
+ delete transportEnv.PATCHWARDEN_CONFIG;
49
+ }
50
+ }
51
+
52
+ const transport = new StdioClientTransport({
53
+ command: process.execPath,
54
+ args: [resolve(root, "dist", "index.js")],
55
+ cwd: root,
56
+ env: transportEnv,
57
+ stderr: "pipe",
58
+ });
30
59
 
31
60
  const client = new Client(
32
- { name: "patchwarden-manifest-check", version: "0.4.0" },
61
+ { name: "patchwarden-manifest-check", version: "0.6.0" },
33
62
  { capabilities: {} }
34
63
  );
35
64
 
@@ -38,10 +67,10 @@ try {
38
67
  const listed = await client.listTools();
39
68
  const names = listed.tools.map((tool) => tool.name);
40
69
  if (JSON.stringify(names) !== JSON.stringify(expectedTools)) {
41
- throw new Error(`chatgpt_core tool list mismatch. Expected ${expectedTools.join(", ")}; got ${names.join(", ")}`);
70
+ throw new Error(`${profile} tool list mismatch. Expected ${expectedTools.join(", ")}; got ${names.join(", ")}`);
42
71
  }
43
72
  // Layer 2: tools/list must carry _meta with manifest hash for catalog sync
44
- if (!listed._meta || listed._meta.tool_profile !== "chatgpt_core" || listed._meta.tool_count !== expectedTools.length) {
73
+ if (!listed._meta || listed._meta.tool_profile !== profile || listed._meta.tool_count !== expectedTools.length) {
45
74
  throw new Error(`tools/list _meta missing or wrong profile/count: ${JSON.stringify(listed._meta || null)}`);
46
75
  }
47
76
  if (typeof listed._meta.tool_manifest_sha256 !== "string" || listed._meta.tool_manifest_sha256.length !== 64) {
@@ -50,51 +79,77 @@ try {
50
79
  if (typeof listed._meta.schema_epoch !== "string" || typeof listed._meta.server_version !== "string") {
51
80
  throw new Error(`tools/list _meta missing schema_epoch/server_version: ${JSON.stringify(listed._meta)}`);
52
81
  }
53
- const createTask = listed.tools.find((tool) => tool.name === "create_task");
54
- const createProperties = Object.keys(createTask?.inputSchema?.properties || {});
55
- for (const requiredProperty of ["inline_plan", "verify_commands"]) {
56
- if (!createProperties.includes(requiredProperty)) {
57
- throw new Error(`create_task schema is missing ${requiredProperty}`);
82
+ if (profile === "chatgpt_core") {
83
+ const createTask = listed.tools.find((tool) => tool.name === "create_task");
84
+ const createProperties = Object.keys(createTask?.inputSchema?.properties || {});
85
+ for (const requiredProperty of ["inline_plan", "verify_commands"]) {
86
+ if (!createProperties.includes(requiredProperty)) {
87
+ throw new Error(`create_task schema is missing ${requiredProperty}`);
88
+ }
89
+ }
90
+ const waitTool = listed.tools.find((tool) => tool.name === "wait_for_task");
91
+ const waitProperties = Object.keys(waitTool?.inputSchema?.properties || {});
92
+ if (!waitProperties.includes("timeout_seconds") || !waitProperties.includes("wait_seconds")) {
93
+ throw new Error("wait_for_task schema must expose timeout_seconds and wait_seconds");
94
+ }
95
+ const healthProperties = Object.keys(listed.tools.find((tool) => tool.name === "health_check")?.inputSchema?.properties || {});
96
+ if (!healthProperties.includes("detail")) throw new Error("health_check schema must expose detail");
97
+ const listTaskProperties = Object.keys(listed.tools.find((tool) => tool.name === "list_tasks")?.inputSchema?.properties || {});
98
+ if (!listTaskProperties.includes("repo_path") || !listTaskProperties.includes("active_only")) {
99
+ throw new Error("list_tasks schema must expose repo_path and active_only");
100
+ }
101
+ const summaryProperties = Object.keys(listed.tools.find((tool) => tool.name === "get_task_summary")?.inputSchema?.properties || {});
102
+ if (!summaryProperties.includes("view") || !summaryProperties.includes("max_items")) {
103
+ throw new Error("get_task_summary schema must expose view and max_items");
104
+ }
105
+ } else {
106
+ const directRequirements = {
107
+ create_direct_session: ["repo_path"],
108
+ apply_patch: ["session_id", "path", "expected_sha256", "operations"],
109
+ run_verification: ["session_id", "command"],
110
+ finalize_direct_session: ["session_id"],
111
+ audit_session: ["session_id"],
112
+ };
113
+ for (const [toolName, requiredProperties] of Object.entries(directRequirements)) {
114
+ const tool = listed.tools.find((entry) => entry.name === toolName);
115
+ const properties = Object.keys(tool?.inputSchema?.properties || {});
116
+ for (const property of requiredProperties) {
117
+ if (!properties.includes(property)) {
118
+ throw new Error(`${toolName} schema is missing ${property}`);
119
+ }
120
+ }
58
121
  }
59
- }
60
- const waitTool = listed.tools.find((tool) => tool.name === "wait_for_task");
61
- const waitProperties = Object.keys(waitTool?.inputSchema?.properties || {});
62
- if (!waitProperties.includes("timeout_seconds") || !waitProperties.includes("wait_seconds")) {
63
- throw new Error("wait_for_task schema must expose timeout_seconds and wait_seconds");
64
- }
65
- const healthProperties = Object.keys(listed.tools.find((tool) => tool.name === "health_check")?.inputSchema?.properties || {});
66
- if (!healthProperties.includes("detail")) throw new Error("health_check schema must expose detail");
67
- const listTaskProperties = Object.keys(listed.tools.find((tool) => tool.name === "list_tasks")?.inputSchema?.properties || {});
68
- if (!listTaskProperties.includes("repo_path") || !listTaskProperties.includes("active_only")) {
69
- throw new Error("list_tasks schema must expose repo_path and active_only");
70
122
  }
71
123
  const healthResult = await client.callTool({ name: "health_check", arguments: {} });
72
124
  if (healthResult.isError) throw new Error(String(healthResult.content?.[0]?.text || "health_check failed"));
73
125
  const health = JSON.parse(String(healthResult.content?.[0]?.text || "{}"));
74
126
  if (
75
- health.tool_profile !== "chatgpt_core" ||
127
+ health.tool_profile !== profile ||
76
128
  health.tool_count !== expectedTools.length ||
77
- !health.tool_manifest_sha256
129
+ !health.tool_manifest_sha256 ||
130
+ (profile === "chatgpt_direct" && health.direct_profile_enabled !== true)
78
131
  ) {
79
132
  throw new Error(`health_check catalog mismatch: ${JSON.stringify(health)}`);
80
133
  }
81
- const hiddenResult = await client.callTool({ name: "get_plan", arguments: { plan_id: "stale-client-probe" } });
82
- const hiddenPayload = JSON.parse(String(hiddenResult.content?.[0]?.text || "{}"));
83
- if (
84
- !hiddenResult.isError ||
85
- hiddenPayload.reason !== "tool_catalog_mismatch" ||
86
- hiddenPayload.refresh_required !== true ||
87
- hiddenPayload.tool_manifest_sha256 !== health.tool_manifest_sha256
88
- ) {
89
- throw new Error(`hidden tool mismatch guidance is incomplete: ${JSON.stringify(hiddenPayload)}`);
90
- }
91
- // Layer 3: mismatch error must carry next_tool_call and connector_refresh_steps for self-healing
92
- if (hiddenPayload.next_tool_call?.name !== "health_check" || hiddenPayload.next_tool_call?.arguments?.detail !== "self_diagnostic") {
93
- throw new Error(`mismatch next_tool_call missing or wrong: ${JSON.stringify(hiddenPayload.next_tool_call || null)}`);
94
- }
95
- if (!Array.isArray(hiddenPayload.connector_refresh_steps) || hiddenPayload.connector_refresh_steps.length < 3) {
96
- throw new Error(`mismatch connector_refresh_steps missing or too short: ${JSON.stringify(hiddenPayload.connector_refresh_steps || null)}`);
134
+ if (profile === "chatgpt_core") {
135
+ const hiddenResult = await client.callTool({ name: "get_plan", arguments: { plan_id: "stale-client-probe" } });
136
+ const hiddenPayload = JSON.parse(String(hiddenResult.content?.[0]?.text || "{}"));
137
+ if (
138
+ !hiddenResult.isError ||
139
+ hiddenPayload.reason !== "tool_catalog_mismatch" ||
140
+ hiddenPayload.refresh_required !== true ||
141
+ hiddenPayload.tool_manifest_sha256 !== health.tool_manifest_sha256
142
+ ) {
143
+ throw new Error(`hidden tool mismatch guidance is incomplete: ${JSON.stringify(hiddenPayload)}`);
144
+ }
145
+ if (hiddenPayload.next_tool_call?.name !== "health_check" || hiddenPayload.next_tool_call?.arguments?.detail !== "self_diagnostic") {
146
+ throw new Error(`mismatch next_tool_call missing or wrong: ${JSON.stringify(hiddenPayload.next_tool_call || null)}`);
147
+ }
148
+ if (!Array.isArray(hiddenPayload.connector_refresh_steps) || hiddenPayload.connector_refresh_steps.length < 3) {
149
+ throw new Error(`mismatch connector_refresh_steps missing or too short: ${JSON.stringify(hiddenPayload.connector_refresh_steps || null)}`);
150
+ }
97
151
  }
152
+
98
153
  const output = {
99
154
  ok: true,
100
155
  server_version: health.server_version,
@@ -103,11 +158,18 @@ try {
103
158
  tool_count: health.tool_count,
104
159
  tool_names: health.tool_names,
105
160
  tool_manifest_sha256: health.tool_manifest_sha256,
106
- required_schema: {
161
+ required_schema: profile === "chatgpt_core" ? {
107
162
  create_task: ["inline_plan", "verify_commands"],
108
163
  wait_for_task: ["timeout_seconds", "wait_seconds"],
109
164
  health_check: ["detail"],
110
165
  list_tasks: ["repo_path", "active_only"],
166
+ get_task_summary: ["view", "max_items"],
167
+ } : {
168
+ create_direct_session: ["repo_path"],
169
+ apply_patch: ["session_id", "path", "expected_sha256", "operations"],
170
+ run_verification: ["session_id", "command"],
171
+ finalize_direct_session: ["session_id"],
172
+ audit_session: ["session_id"],
111
173
  },
112
174
  };
113
175
  console.log(jsonOnly ? JSON.stringify(output) : JSON.stringify(output, null, 2));
@@ -117,4 +179,7 @@ try {
117
179
  process.exitCode = 1;
118
180
  } finally {
119
181
  await client.close().catch(() => {});
182
+ if (tempConfigPath) {
183
+ try { rmSync(tempConfigPath, { force: true }); } catch {}
184
+ }
120
185
  }
@@ -101,9 +101,13 @@ try {
101
101
  const tools = await client.listTools();
102
102
  const names = tools.tools.map((tool) => tool.name).sort();
103
103
  const expected = [
104
+ "apply_patch",
105
+ "audit_session",
104
106
  "audit_task",
105
107
  "cancel_task",
108
+ "create_direct_session",
106
109
  "create_task",
110
+ "finalize_direct_session",
107
111
  "get_diff",
108
112
  "get_plan",
109
113
  "get_result",
@@ -121,7 +125,11 @@ try {
121
125
  "list_workspace",
122
126
  "read_workspace_file",
123
127
  "retry_task",
128
+ "run_verification",
129
+ "safe_status",
124
130
  "save_plan",
131
+ "search_workspace",
132
+ "sync_file",
125
133
  "wait_for_task",
126
134
  ];
127
135
  if (JSON.stringify(names) !== JSON.stringify(expected)) {
@@ -133,7 +141,7 @@ try {
133
141
  if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
134
142
  throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
135
143
  }
136
- if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 22) {
144
+ if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 30) {
137
145
  throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
138
146
  }
139
147
  if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
@@ -204,7 +212,7 @@ try {
204
212
  test_command: "npm test",
205
213
  });
206
214
  if (
207
- task.server_version !== "0.4.0" ||
215
+ task.server_version !== "0.6.1" ||
208
216
  !/^[a-f0-9]{64}$/.test(task.tool_manifest_sha256 || "") ||
209
217
  task.next_tool_call?.name !== "wait_for_task" ||
210
218
  task.next_tool_call?.arguments?.timeout_seconds !== 25
@@ -298,10 +306,17 @@ try {
298
306
  if (!summary.terminal || summary.acceptance_status !== "ready_for_review") {
299
307
  throw new Error(`unexpected terminal summary: ${JSON.stringify(summary)}`);
300
308
  }
309
+ const compactSummary = await parseToolJson("get_task_summary", { task_id: task.task_id, view: "compact", max_items: 2 });
310
+ if (compactSummary.view !== "compact" || "log_tails" in compactSummary || compactSummary.artifact_hygiene?.max_items !== 2) {
311
+ throw new Error(`unexpected compact summary: ${JSON.stringify(compactSummary)}`);
312
+ }
301
313
  const waited = await parseToolJson("wait_for_task", { task_id: task.task_id, timeout_seconds: 1 });
302
314
  if (!waited.terminal || waited.continuation_required || waited.next_tool_call?.name !== "audit_task") {
303
315
  throw new Error(`wait_for_task did not return terminal acceptance: ${JSON.stringify(waited)}`);
304
316
  }
317
+ if (waited.summary?.view !== "compact" || "log_tails" in (waited.summary || {})) {
318
+ throw new Error(`wait_for_task did not embed compact evidence: ${JSON.stringify(waited.summary)}`);
319
+ }
305
320
  const legacyWaited = await parseToolJson("wait_for_task", { task_id: task.task_id, wait_seconds: 1 });
306
321
  if (!legacyWaited.terminal) throw new Error("legacy wait_seconds alias stopped working");
307
322
  const conflictingWait = await client.callTool({
@@ -323,6 +338,149 @@ try {
323
338
  }
324
339
  await client.close();
325
340
  ok("runner executes a task and writes result files");
341
+
342
+ // ── Direct profile checks (lightweight) ──────────────────────────
343
+
344
+ // 1. chatgpt_direct disabled: only health_check exposed
345
+ const disabledConfigPath = join(tempRoot, "direct-disabled.json");
346
+ writeFileSync(
347
+ disabledConfigPath,
348
+ JSON.stringify({
349
+ workspaceRoot,
350
+ plansDir: ".patchwarden/plans",
351
+ tasksDir: ".patchwarden/tasks",
352
+ agents: { codex: { command: "node", args: ["-e", "console.log('agent')"] } },
353
+ allowedTestCommands: ["npm test"],
354
+ maxReadFileBytes: 200000,
355
+ enableDirectProfile: false,
356
+ }, null, 2),
357
+ "utf-8"
358
+ );
359
+
360
+ const disabledTransport = new StdioClientTransport({
361
+ command: "node",
362
+ args: ["dist/index.js"],
363
+ cwd: root,
364
+ env: {
365
+ PATCHWARDEN_CONFIG: disabledConfigPath,
366
+ PATCHWARDEN_TOOL_PROFILE: "chatgpt_direct",
367
+ },
368
+ stderr: "pipe",
369
+ });
370
+ const disabledClient = new Client(
371
+ { name: "patchwarden-direct-disabled", version: "0.1.0" },
372
+ { capabilities: {} }
373
+ );
374
+ await disabledClient.connect(disabledTransport);
375
+
376
+ const disabledTools = await disabledClient.listTools();
377
+ const disabledNames = disabledTools.tools.map((t) => t.name).sort();
378
+ if (disabledNames.length !== 1 || disabledNames[0] !== "health_check") {
379
+ throw new Error(`chatgpt_direct disabled should expose only health_check, got: ${disabledNames.join(", ")}`);
380
+ }
381
+ if (disabledTools._meta.tool_count !== 1) {
382
+ throw new Error(`chatgpt_direct disabled tool_count should be 1, got ${disabledTools._meta.tool_count}`);
383
+ }
384
+
385
+ const disabledHealth = JSON.parse(
386
+ (await disabledClient.callTool({ name: "health_check", arguments: {} })).content?.[0]?.text || "{}"
387
+ );
388
+ if (disabledHealth.direct_profile_enabled !== false) {
389
+ throw new Error(`direct_profile_enabled should be false, got ${disabledHealth.direct_profile_enabled}`);
390
+ }
391
+ await disabledClient.close();
392
+ ok("chatgpt_direct disabled exposes only health_check with diagnostic");
393
+
394
+ // 2. chatgpt_direct enabled: 10 tools + minimal create_direct_session
395
+ const enabledConfigPath = join(tempRoot, "direct-enabled.json");
396
+ const directRepo = join(workspaceRoot, "direct-fixture");
397
+ mkdirSync(join(directRepo, "src"), { recursive: true });
398
+ writeFileSync(join(directRepo, "src", "index.ts"), "export const x = 1;\n", "utf-8");
399
+ writeFileSync(join(directRepo, "package.json"), JSON.stringify({
400
+ name: "direct-fixture",
401
+ private: true,
402
+ scripts: { test: 'node -e "console.log(\'ok\')"' },
403
+ }, null, 2), "utf-8");
404
+
405
+ writeFileSync(
406
+ enabledConfigPath,
407
+ JSON.stringify({
408
+ workspaceRoot,
409
+ plansDir: ".patchwarden/plans",
410
+ tasksDir: ".patchwarden/tasks",
411
+ agents: { codex: { command: "node", args: ["-e", "console.log('agent')"] } },
412
+ allowedTestCommands: ["npm test"],
413
+ maxReadFileBytes: 200000,
414
+ enableDirectProfile: true,
415
+ directAllowedCommands: ["npm test", "npm run build", "npm run lint"],
416
+ directSessionsDir: ".patchwarden/direct-sessions",
417
+ directSessionTtlSeconds: 3600,
418
+ directMaxPatchBytes: 200000,
419
+ directMaxFileBytes: 500000,
420
+ }, null, 2),
421
+ "utf-8"
422
+ );
423
+
424
+ const enabledTransport = new StdioClientTransport({
425
+ command: "node",
426
+ args: ["dist/index.js"],
427
+ cwd: root,
428
+ env: {
429
+ PATCHWARDEN_CONFIG: enabledConfigPath,
430
+ PATCHWARDEN_TOOL_PROFILE: "chatgpt_direct",
431
+ },
432
+ stderr: "pipe",
433
+ });
434
+ const enabledClient = new Client(
435
+ { name: "patchwarden-direct-enabled", version: "0.1.0" },
436
+ { capabilities: {} }
437
+ );
438
+ await enabledClient.connect(enabledTransport);
439
+
440
+ const enabledTools = await enabledClient.listTools();
441
+ const enabledNames = enabledTools.tools.map((t) => t.name).sort();
442
+ const expectedDirect = [
443
+ "apply_patch",
444
+ "audit_session",
445
+ "create_direct_session",
446
+ "finalize_direct_session",
447
+ "health_check",
448
+ "list_workspace",
449
+ "read_workspace_file",
450
+ "run_verification",
451
+ "search_workspace",
452
+ "sync_file",
453
+ ];
454
+ if (JSON.stringify(enabledNames) !== JSON.stringify(expectedDirect)) {
455
+ throw new Error(`chatgpt_direct enabled tools mismatch: ${enabledNames.join(", ")}`);
456
+ }
457
+ if (enabledTools._meta.tool_count !== 10) {
458
+ throw new Error(`chatgpt_direct enabled tool_count should be 10, got ${enabledTools._meta.tool_count}`);
459
+ }
460
+
461
+ // Minimal create_direct_session
462
+ const sessionResult = JSON.parse(
463
+ (await enabledClient.callTool({
464
+ name: "create_direct_session",
465
+ arguments: { repo_path: "direct-fixture", title: "smoke test" },
466
+ })).content?.[0]?.text || "{}"
467
+ );
468
+ if (!sessionResult.session_id || !sessionResult.session_id.startsWith("direct_")) {
469
+ throw new Error(`create_direct_session failed: ${JSON.stringify(sessionResult)}`);
470
+ }
471
+
472
+ const enabledHealth = JSON.parse(
473
+ (await enabledClient.callTool({ name: "health_check", arguments: {} })).content?.[0]?.text || "{}"
474
+ );
475
+ if (enabledHealth.direct_profile_enabled !== true) {
476
+ throw new Error(`direct_profile_enabled should be true, got ${enabledHealth.direct_profile_enabled}`);
477
+ }
478
+ if (enabledHealth.direct_tool_count !== 10) {
479
+ throw new Error(`direct_tool_count should be 10, got ${enabledHealth.direct_tool_count}`);
480
+ }
481
+
482
+ await enabledClient.close();
483
+ ok("chatgpt_direct enabled exposes 10 tools and create_direct_session works");
326
484
  } catch (error) {
327
485
  fail("MCP smoke test", error);
328
486
  } finally {
@@ -3,11 +3,14 @@ import {
3
3
  cpSync,
4
4
  existsSync,
5
5
  mkdirSync,
6
+ readFileSync,
6
7
  readdirSync,
7
8
  rmSync,
8
9
  statSync,
10
+ writeFileSync,
9
11
  } from "node:fs";
10
12
  import { execFileSync } from "node:child_process";
13
+ import { deflateRawSync } from "node:zlib";
11
14
  import { join, relative, resolve } from "node:path";
12
15
  import { fileURLToPath } from "node:url";
13
16
 
@@ -16,16 +19,17 @@ const root = resolve(scriptDir, "..");
16
19
  const releaseDir = resolve(root, "release");
17
20
  const archivePath = resolve(root, "patchwarden-release.tar.gz");
18
21
 
22
+ // Read version from package.json for zip naming
23
+ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8"));
24
+ const zipArchivePath = resolve(root, `PatchWarden-v${pkg.version}.zip`);
25
+
19
26
  const include = [
20
27
  "dist",
21
28
  "docs",
22
29
  "examples",
23
30
  "scripts",
24
31
  "src",
25
- "Start-PatchWarden-Tunnel.cmd",
26
- "Restart-PatchWarden.cmd",
27
- "Check-PatchWarden-Health.cmd",
28
- "Reset-PatchWarden-Tunnel-Key.cmd",
32
+ "PatchWarden.cmd",
29
33
  ".gitignore",
30
34
  "LICENSE",
31
35
  "README.md",
@@ -44,8 +48,123 @@ const forbidden = [
44
48
  /\.dpapi$/i,
45
49
  /(^|[\\/])\.env$/,
46
50
  /\.log$/,
51
+ /^docs[\\/]optimization-proposal\.md$/i,
52
+ /(^|[\\/])kill-patchwarden\.(cmd|ps1)$/i,
47
53
  ];
48
54
 
55
+ // ── Minimal zip writer helpers (POSIX paths, DEFLATE via zlib) ────
56
+ // Defined before main execution so const CRC_TABLE is initialized.
57
+
58
+ const CRC_TABLE = new Uint32Array(256);
59
+ for (let i = 0; i < 256; i++) {
60
+ let c = i;
61
+ for (let j = 0; j < 8; j++) {
62
+ c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
63
+ }
64
+ CRC_TABLE[i] = c;
65
+ }
66
+
67
+ function crc32(buf) {
68
+ let c = 0xFFFFFFFF;
69
+ for (let i = 0; i < buf.length; i++) {
70
+ c = CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
71
+ }
72
+ return (c ^ 0xFFFFFFFF) >>> 0;
73
+ }
74
+
75
+ function dosDateTime(date) {
76
+ const dosTime = (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1);
77
+ const dosDate = (((date.getFullYear() - 1980) & 0x7F) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
78
+ return { dosTime, dosDate };
79
+ }
80
+
81
+ function createZip(files, outputPath) {
82
+ const localParts = [];
83
+ const centralParts = [];
84
+ let offset = 0;
85
+ const now = new Date();
86
+ const { dosTime, dosDate } = dosDateTime(now);
87
+
88
+ for (const file of files) {
89
+ const posixPath = file.path.replace(/\\/g, "/");
90
+ const nameBuf = Buffer.from(posixPath, "utf-8");
91
+ const content = file.content;
92
+ const compressed = deflateRawSync(content);
93
+ const useDeflate = compressed.length < content.length;
94
+ const method = useDeflate ? 8 : 0;
95
+ const data = useDeflate ? compressed : content;
96
+ const crc = crc32(content);
97
+
98
+ const lh = Buffer.alloc(30);
99
+ lh.writeUInt32LE(0x04034b50, 0);
100
+ lh.writeUInt16LE(20, 4);
101
+ lh.writeUInt16LE(0, 6);
102
+ lh.writeUInt16LE(method, 8);
103
+ lh.writeUInt16LE(dosTime, 10);
104
+ lh.writeUInt16LE(dosDate, 12);
105
+ lh.writeUInt32LE(crc, 14);
106
+ lh.writeUInt32LE(data.length, 18);
107
+ lh.writeUInt32LE(content.length, 22);
108
+ lh.writeUInt16LE(nameBuf.length, 26);
109
+ lh.writeUInt16LE(0, 28);
110
+
111
+ const localHeaderOffset = offset;
112
+ localParts.push(Buffer.concat([lh, nameBuf, data]));
113
+ offset += lh.length + nameBuf.length + data.length;
114
+
115
+ const cd = Buffer.alloc(46);
116
+ cd.writeUInt32LE(0x02014b50, 0);
117
+ cd.writeUInt16LE(20, 4);
118
+ cd.writeUInt16LE(20, 6);
119
+ cd.writeUInt16LE(0, 8);
120
+ cd.writeUInt16LE(method, 10);
121
+ cd.writeUInt16LE(dosTime, 12);
122
+ cd.writeUInt16LE(dosDate, 14);
123
+ cd.writeUInt32LE(crc, 16);
124
+ cd.writeUInt32LE(data.length, 20);
125
+ cd.writeUInt32LE(content.length, 24);
126
+ cd.writeUInt16LE(nameBuf.length, 28);
127
+ cd.writeUInt16LE(0, 30);
128
+ cd.writeUInt16LE(0, 32);
129
+ cd.writeUInt16LE(0, 34);
130
+ cd.writeUInt16LE(0, 36);
131
+ cd.writeUInt32LE(0, 38);
132
+ cd.writeUInt32LE(localHeaderOffset, 42);
133
+ centralParts.push(Buffer.concat([cd, nameBuf]));
134
+ }
135
+
136
+ const centralDir = Buffer.concat(centralParts);
137
+ const centralDirOffset = offset;
138
+
139
+ const eocd = Buffer.alloc(22);
140
+ eocd.writeUInt32LE(0x06054b50, 0);
141
+ eocd.writeUInt16LE(0, 4);
142
+ eocd.writeUInt16LE(0, 6);
143
+ eocd.writeUInt16LE(files.length, 8);
144
+ eocd.writeUInt16LE(files.length, 10);
145
+ eocd.writeUInt32LE(centralDir.length, 12);
146
+ eocd.writeUInt32LE(centralDirOffset, 16);
147
+ eocd.writeUInt16LE(0, 20);
148
+
149
+ writeFileSync(outputPath, Buffer.concat([...localParts, centralDir, eocd]));
150
+ }
151
+
152
+ function readZipEntryNames(zipPath) {
153
+ const buf = readFileSync(zipPath);
154
+ const names = [];
155
+ for (let i = 0; i < buf.length - 4; i++) {
156
+ if (buf.readUInt32LE(i) === 0x02014b50) {
157
+ const nameLen = buf.readUInt16LE(i + 28);
158
+ const extraLen = buf.readUInt16LE(i + 30);
159
+ const commentLen = buf.readUInt16LE(i + 32);
160
+ const name = buf.toString("utf-8", i + 46, i + 46 + nameLen);
161
+ names.push(name);
162
+ i += 45 + nameLen + extraLen + commentLen;
163
+ }
164
+ }
165
+ return names;
166
+ }
167
+
49
168
  console.log("[pack-clean] Preparing clean release directory...");
50
169
  rmSync(releaseDir, { recursive: true, force: true });
51
170
  rmSync(archivePath, { force: true });
@@ -116,7 +235,43 @@ if (badArchiveEntries.length > 0) {
116
235
 
117
236
  const sizeKb = (statSync(archivePath).size / 1024).toFixed(1);
118
237
  console.log(`[pack-clean] Release directory: ${releaseFiles.length} files`);
119
- console.log(`[pack-clean] Archive: ${archivePath} (${sizeKb} KB)`);
238
+ console.log(`[pack-clean] tar.gz Archive: ${archivePath} (${sizeKb} KB)`);
239
+
240
+ // ── Create zip archive with POSIX path separators ─────────────────
241
+ console.log("[pack-clean] Creating zip archive with POSIX paths...");
242
+ rmSync(zipArchivePath, { force: true });
243
+
244
+ const zipFiles = releaseFiles.map((file) => {
245
+ const rel = toPosix(relative(releaseDir, file));
246
+ return { path: rel, content: readFileSync(file) };
247
+ });
248
+
249
+ createZip(zipFiles, zipArchivePath);
250
+
251
+ // Verify zip entries use POSIX separators
252
+ const zipEntryNames = readZipEntryNames(zipArchivePath);
253
+ const badZipEntries = zipEntryNames.filter((name) => name.includes("\\"));
254
+ if (badZipEntries.length > 0) {
255
+ console.error("[pack-clean] Backslash path separators found in zip entries:");
256
+ for (const name of badZipEntries.slice(0, 20)) {
257
+ console.error(` ${name}`);
258
+ }
259
+ rmSync(zipArchivePath, { force: true });
260
+ process.exit(1);
261
+ }
262
+
263
+ const badZipForbidden = zipEntryNames.filter(isForbidden);
264
+ if (badZipForbidden.length > 0) {
265
+ console.error("[pack-clean] Forbidden files in zip archive:");
266
+ for (const entry of badZipForbidden) {
267
+ console.error(` ${entry}`);
268
+ }
269
+ rmSync(zipArchivePath, { force: true });
270
+ process.exit(1);
271
+ }
272
+
273
+ const zipSizeKb = (statSync(zipArchivePath).size / 1024).toFixed(1);
274
+ console.log(`[pack-clean] zip Archive: ${zipArchivePath} (${zipSizeKb} KB, ${zipEntryNames.length} entries)`);
120
275
  console.log("[pack-clean] OK");
121
276
 
122
277
  function listFiles(dir) {