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
@@ -12,7 +12,7 @@ const TERMINAL_STATUSES = new Set([
12
12
  "failed_policy_violation",
13
13
  "canceled",
14
14
  ]);
15
- export function getTaskSummary(taskId) {
15
+ export function getTaskSummary(taskId, options = {}) {
16
16
  const config = getConfig();
17
17
  const taskDir = resolve(getTasksDir(config), taskId);
18
18
  const statusFile = join(taskDir, "status.json");
@@ -23,7 +23,12 @@ export function getTaskSummary(taskId) {
23
23
  const result = resultRead.data;
24
24
  const verify = verifyRead.data;
25
25
  const terminal = TERMINAL_STATUSES.has(String(status.status));
26
- const outOfScope = asArray(result.out_of_scope_changes ?? status.out_of_scope_changes);
26
+ // Phase 4: Use new_out_of_scope_changes (task-caused) for acceptance status.
27
+ // Pre-existing external dirty files that didn't change should NOT fail acceptance.
28
+ const outOfScope = asArray(result.new_out_of_scope_changes
29
+ ?? status.new_out_of_scope_changes
30
+ ?? result.out_of_scope_changes
31
+ ?? status.out_of_scope_changes);
27
32
  const verifyStatus = String(verify.status ?? result.verify_status ?? result.verify?.status ?? status.verify_status ?? "not_available");
28
33
  const errors = [status.error, ...asArray(result.errors), ...asArray(result.known_issues)]
29
34
  .filter((value) => typeof value === "string" && value.trim() !== "");
@@ -92,6 +97,10 @@ export function getTaskSummary(taskId) {
92
97
  ? Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : Date.now()) - startedAt)
93
98
  : 0;
94
99
  const changedFiles = asArray(result.changed_files ?? status.changed_files);
100
+ const changedFilesRead = tryReadJson(join(taskDir, "changed-files.json"));
101
+ const artifactHygiene = asRecord(result.artifact_hygiene ?? changedFilesRead.data.artifact_hygiene ?? {
102
+ counts: status.artifact_hygiene_counts || {},
103
+ });
95
104
  const verifyCommands = asArray(verify.commands ?? result.verify_commands ?? result.verify?.commands);
96
105
  const testLogSummary = summarizeTestLog(join(taskDir, "test.log"));
97
106
  const verificationSummary = buildVerificationSummary(verifyStatus, verifyCommands, testLogSummary);
@@ -156,7 +165,11 @@ export function getTaskSummary(taskId) {
156
165
  watcher: status.watcher,
157
166
  pending_reason: status.pending_reason || null,
158
167
  execution_blocked: Boolean(status.execution_blocked),
168
+ artifact_hygiene: artifactHygiene,
159
169
  };
170
+ if ((options.view || "standard") === "compact") {
171
+ return buildCompactSummary(output, normalizeMaxItems(options.max_items));
172
+ }
160
173
  const safe = redactSensitiveValue(output);
161
174
  return {
162
175
  ...safe.value,
@@ -164,6 +177,67 @@ export function getTaskSummary(taskId) {
164
177
  redaction_categories: safe.redaction_categories,
165
178
  };
166
179
  }
180
+ function buildCompactSummary(output, maxItems) {
181
+ const hygiene = asRecord(output.artifact_hygiene);
182
+ const groupNames = [
183
+ "source_changes",
184
+ "tracked_build_artifacts",
185
+ "ignored_untracked_artifacts",
186
+ "runtime_generated_files",
187
+ "suspicious_changes",
188
+ ];
189
+ const groups = Object.fromEntries(groupNames.map((name) => [name, asArray(hygiene[name]).slice(0, maxItems)]));
190
+ const truncated = groupNames.some((name) => asArray(hygiene[name]).length > maxItems);
191
+ // Phase 6: Read artifact_manifest.json for release artifact info
192
+ const taskDir = resolve(getTasksDir(getConfig()), String(output.task_id));
193
+ const manifestRead = tryReadJson(join(taskDir, "artifact_manifest.json"));
194
+ const manifest = asRecord(manifestRead.data);
195
+ const releaseArtifacts = asArray(manifest.artifacts);
196
+ const releaseArtifactPaths = releaseArtifacts.map((a) => String(a.path || "")).slice(0, maxItems);
197
+ const compact = {
198
+ view: "compact",
199
+ task_id: String(output.task_id),
200
+ status: String(output.status),
201
+ terminal: Boolean(output.terminal),
202
+ acceptance_status: output.acceptance_status,
203
+ phase: String(output.phase),
204
+ repo_path: String(output.repo_path),
205
+ changed_files_total: asArray(output.changed_files).length,
206
+ out_of_scope_changes_total: asArray(output.out_of_scope_changes).length,
207
+ artifact_hygiene: {
208
+ counts: asRecord(hygiene.counts),
209
+ ...groups,
210
+ max_items: maxItems,
211
+ truncated,
212
+ },
213
+ release_artifacts_count: releaseArtifacts.length,
214
+ release_artifact_paths: releaseArtifactPaths,
215
+ artifact_status: String(output.artifact_status || manifest.status || "collected"),
216
+ verification_summary: output.verification_summary,
217
+ summary: String(output.summary).slice(0, 1000),
218
+ warnings: asArray(output.warnings).slice(0, maxItems),
219
+ errors: asArray(output.errors).slice(0, maxItems),
220
+ failure_reason: output.failure_reason || null,
221
+ failed_command: output.failed_command || null,
222
+ suggested_next_action: String(output.suggested_next_action),
223
+ execution_blocked: Boolean(output.execution_blocked),
224
+ pending_reason: output.pending_reason || null,
225
+ };
226
+ const safe = redactSensitiveValue(compact);
227
+ return {
228
+ ...safe.value,
229
+ redacted: safe.redacted,
230
+ redaction_categories: safe.redaction_categories,
231
+ };
232
+ }
233
+ function normalizeMaxItems(value) {
234
+ if (value === undefined)
235
+ return 8;
236
+ if (!Number.isInteger(value) || value < 1 || value > 50) {
237
+ throw new Error("max_items must be an integer from 1 to 50.");
238
+ }
239
+ return value;
240
+ }
167
241
  function tryReadJson(path) {
168
242
  if (!existsSync(path))
169
243
  return { data: {} };
@@ -177,6 +251,9 @@ function tryReadJson(path) {
177
251
  function asArray(value) {
178
252
  return Array.isArray(value) ? value : [];
179
253
  }
254
+ function asRecord(value) {
255
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
256
+ }
180
257
  function summarizeTestLog(path) {
181
258
  if (!existsSync(path))
182
259
  return "test.log missing";
@@ -20,18 +20,6 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
20
20
  error: string | null;
21
21
  }[];
22
22
  } | undefined;
23
- status: string;
24
- server_version: string;
25
- schema_epoch: string;
26
- tool_profile: string;
27
- tool_count: number | null;
28
- tool_names: string[];
29
- tool_manifest_sha256: string | null;
30
- profile_consistent: boolean;
31
- profile_errors: string[];
32
- catalog_consistent: boolean;
33
- mismatch_report: string[];
34
- tunnel_catalog_comparison: string;
35
23
  connector_visibility: {
36
24
  status: string;
37
25
  verification: string;
@@ -43,6 +31,10 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
43
31
  uptime_seconds: number;
44
32
  checked_at: string;
45
33
  };
34
+ path_encoding: {
35
+ encoding: string;
36
+ warnings: string[];
37
+ };
46
38
  watcher: {
47
39
  supervisor: Record<string, unknown>;
48
40
  status: import("../watcherStatus.js").WatcherState;
@@ -54,6 +46,7 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
54
46
  instance_id: string | null;
55
47
  launcher_pid: number | null;
56
48
  reason: string | null;
49
+ activity: string | null;
57
50
  };
58
51
  workspace_root: {
59
52
  available: boolean;
@@ -76,4 +69,26 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
76
69
  agent_status: {
77
70
  [k: string]: string;
78
71
  };
72
+ direct_tool_count?: number | undefined;
73
+ status: string;
74
+ server_version: string;
75
+ schema_epoch: string;
76
+ tool_profile: string;
77
+ tool_count: number | null;
78
+ tool_names: string[];
79
+ tool_manifest_sha256: string | null;
80
+ profile_consistent: boolean;
81
+ profile_errors: string[];
82
+ catalog_consistent: boolean;
83
+ mismatch_report: string[];
84
+ tunnel_catalog_comparison: string;
85
+ direct_profile_enabled: boolean;
86
+ direct_sessions_dir: {
87
+ available: boolean;
88
+ path: string;
89
+ readable: boolean;
90
+ writable: boolean;
91
+ reason: string | null;
92
+ };
93
+ direct_session_ttl_seconds: number;
79
94
  };
@@ -1,11 +1,11 @@
1
1
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
4
+ import { getConfig, getTasksDir, getDirectSessionsDir, resolveWorkspaceRoot } from "../config.js";
5
5
  import { listAgents } from "./listAgents.js";
6
6
  import { redactSensitiveContent, redactSensitiveValue } from "../security/contentRedaction.js";
7
7
  import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
8
- import { CHATGPT_CORE_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
8
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
9
9
  import { readWatcherStatus } from "../watcherStatus.js";
10
10
  import { listTasks } from "./listTasks.js";
11
11
  const SERVER_STARTED_AT = Date.now();
@@ -26,9 +26,19 @@ export function healthCheck(catalog, input = {}) {
26
26
  let profileConsistent = true;
27
27
  if (catalog) {
28
28
  const activeProfile = resolveToolProfile(config.toolProfile);
29
- const expectedNames = activeProfile === "chatgpt_core"
30
- ? [...CHATGPT_CORE_TOOL_NAMES]
31
- : null; // full profile: all tools are accepted
29
+ let expectedNames = null;
30
+ if (activeProfile === "chatgpt_core") {
31
+ expectedNames = [...CHATGPT_CORE_TOOL_NAMES];
32
+ }
33
+ else if (activeProfile === "chatgpt_direct" && config.enableDirectProfile) {
34
+ expectedNames = [...CHATGPT_DIRECT_TOOL_NAMES];
35
+ }
36
+ else if (activeProfile === "chatgpt_direct" && !config.enableDirectProfile) {
37
+ // Degraded mode: only health_check should be exposed
38
+ expectedNames = ["health_check"];
39
+ profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
40
+ profileConsistent = false;
41
+ }
32
42
  if (expectedNames) {
33
43
  const catalogNames = new Set(catalog.tool_names);
34
44
  for (const name of expectedNames) {
@@ -65,6 +75,9 @@ export function healthCheck(catalog, input = {}) {
65
75
  const status = watcher.available && workspace.available && tasks.available && agentReady && catalogConsistent
66
76
  ? "healthy"
67
77
  : "degraded";
78
+ const directSessionsDir = getDirectSessionsDir(config);
79
+ const directSessions = directoryStatus(directSessionsDir, true, workspaceRoot);
80
+ const activeProfile = resolveToolProfile(config.toolProfile);
68
81
  return {
69
82
  status,
70
83
  server_version: catalog?.server_version || PATCHWARDEN_VERSION,
@@ -78,11 +91,15 @@ export function healthCheck(catalog, input = {}) {
78
91
  catalog_consistent: catalogConsistent,
79
92
  mismatch_report: mismatchReport,
80
93
  tunnel_catalog_comparison: tunnelCatalogComparison,
94
+ direct_profile_enabled: config.enableDirectProfile ?? false,
95
+ direct_sessions_dir: directSessions,
96
+ direct_session_ttl_seconds: config.directSessionTtlSeconds,
97
+ ...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
81
98
  connector_visibility: {
82
99
  status: "not_observable_server_side",
83
100
  verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
84
101
  refresh_steps: [
85
- "1. Run Check-PatchWarden-Health.cmd to confirm tool_profile=chatgpt_core, tool_count=16, and catalog_consistent=true.",
102
+ "1. Run PatchWarden.cmd health to confirm the active profile, tool count, and catalog consistency.",
86
103
  "2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
87
104
  "3. Open a NEW ChatGPT conversation (old conversations retain their cached tool catalog).",
88
105
  "4. Call health_check in the new conversation; verify tool_manifest_sha256 matches the local report.",
@@ -94,6 +111,7 @@ export function healthCheck(catalog, input = {}) {
94
111
  uptime_seconds: Math.round((Date.now() - SERVER_STARTED_AT) / 1000),
95
112
  checked_at: new Date().toISOString(),
96
113
  },
114
+ path_encoding: checkPathEncoding(),
97
115
  watcher,
98
116
  workspace_root: workspace,
99
117
  tasks_dir: tasks,
@@ -228,3 +246,23 @@ function readTunnelStatus() {
228
246
  function safeText(value, maxLength) {
229
247
  return typeof value === "string" ? value.replace(/[\r\n]+/g, " ").slice(0, maxLength) : "";
230
248
  }
249
+ function checkPathEncoding() {
250
+ const warnings = [];
251
+ // Node.js always uses UTF-8 for fs operations, but on Windows the console
252
+ // codepage can cause display issues. We check if the environment is likely
253
+ // to cause problems with non-ASCII paths.
254
+ if (process.platform === "win32") {
255
+ // Check if the system locale uses a codepage that might not support UTF-8
256
+ const lang = process.env.LANG || process.env.LC_ALL || "";
257
+ const chcp = process.env.PATCHWARDEN_CHCP || "";
258
+ if (chcp && !chcp.includes("65001") && !chcp.includes("utf")) {
259
+ warnings.push(`Windows codepage "${chcp}" may cause display issues with non-ASCII paths. Consider setting codepage to 65001 (UTF-8).`);
260
+ }
261
+ }
262
+ // Verify that Node.js fs operations use UTF-8 by default
263
+ // This is always true in Node.js >= 18
264
+ return {
265
+ encoding: "utf-8",
266
+ warnings,
267
+ };
268
+ }
@@ -4,5 +4,11 @@ export interface ReadWorkspaceFileOutput {
4
4
  size: number;
5
5
  redacted?: boolean;
6
6
  redaction_categories?: string[];
7
+ relative_path?: string;
8
+ sha256?: string;
7
9
  }
8
- export declare function readWorkspaceFile(relativePath: string): ReadWorkspaceFileOutput;
10
+ export interface ReadWorkspaceFileInput {
11
+ path: string;
12
+ session_id?: string;
13
+ }
14
+ export declare function readWorkspaceFile(input: string | ReadWorkspaceFileInput): ReadWorkspaceFileOutput;
@@ -1,10 +1,28 @@
1
- import { readFileSync, statSync } from "node:fs";
1
+ import { readFileSync, statSync, existsSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
2
3
  import { getConfig } from "../config.js";
3
4
  import { guardReadPath } from "../security/pathGuard.js";
4
5
  import { guardSensitivePath } from "../security/sensitiveGuard.js";
5
6
  import { redactSensitiveContent } from "../security/contentRedaction.js";
6
- export function readWorkspaceFile(relativePath) {
7
+ import { PatchWardenError } from "../errors.js";
8
+ import { readDirectSession } from "../direct/directSessionStore.js";
9
+ import { guardDirectSessionActive, guardDirectReadPath, isBinaryFile, guardDirectFileSize } from "../direct/directGuards.js";
10
+ import { resolveToolProfile } from "./toolCatalog.js";
11
+ export function readWorkspaceFile(input) {
7
12
  const config = getConfig();
13
+ // Support both old string API and new object API
14
+ const relativePath = typeof input === "string" ? input : input.path;
15
+ const sessionId = typeof input === "string" ? undefined : input.session_id;
16
+ // Direct mode: session_id provided
17
+ if (sessionId) {
18
+ return readWorkspaceFileDirect(relativePath, sessionId);
19
+ }
20
+ // In chatgpt_direct profile, session_id is required
21
+ const profile = resolveToolProfile(config.toolProfile);
22
+ if (profile === "chatgpt_direct") {
23
+ throw new PatchWardenError("session_id_required", "session_id is required when using the chatgpt_direct profile.", "Provide a session_id from create_direct_session.", true, { operation: "read_workspace_file" });
24
+ }
25
+ // Compatibility mode: no session_id, original behavior
8
26
  const safePath = guardReadPath(relativePath, config.workspaceRoot);
9
27
  guardSensitivePath(safePath);
10
28
  const stat = statSync(safePath);
@@ -24,3 +42,31 @@ export function readWorkspaceFile(relativePath) {
24
42
  redaction_categories: redaction.redaction_categories,
25
43
  };
26
44
  }
45
+ function readWorkspaceFileDirect(relativePath, sessionId) {
46
+ const config = getConfig();
47
+ const session = readDirectSession(sessionId);
48
+ guardDirectSessionActive(session);
49
+ // Resolve path within session's repo
50
+ const safePath = guardDirectReadPath(relativePath, session.resolved_repo_path, config.workspaceRoot);
51
+ if (!existsSync(safePath)) {
52
+ throw new PatchWardenError("file_not_found", `File "${relativePath}" not found in session repo.`, "Use search_workspace to find available files.", true, { path: relativePath, session_id: sessionId });
53
+ }
54
+ const stat = statSync(safePath);
55
+ guardDirectFileSize(stat.size);
56
+ // Block binary files
57
+ if (isBinaryFile(safePath)) {
58
+ throw new PatchWardenError("binary_file_blocked", `File "${relativePath}" appears to be a binary file.`, "Binary files cannot be read in Direct mode.", true, { path: relativePath });
59
+ }
60
+ const content = readFileSync(safePath, "utf-8");
61
+ const redaction = redactSensitiveContent(content);
62
+ const sha256 = createHash("sha256").update(content, "utf-8").digest("hex");
63
+ return {
64
+ path: safePath,
65
+ relative_path: relativePath,
66
+ content: redaction.content,
67
+ size: stat.size,
68
+ sha256,
69
+ redacted: redaction.redacted,
70
+ redaction_categories: redaction.redaction_categories,
71
+ };
72
+ }