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
@@ -0,0 +1,144 @@
1
+ import { join, resolve, relative, isAbsolute } from "node:path";
2
+ import {
3
+ readDirectSession,
4
+ updateDirectSession,
5
+ finalizeDirectSessionRecord,
6
+ getDirectSessionDir,
7
+ } from "../direct/directSessionStore.js";
8
+ import { guardDirectSessionActive } from "../direct/directGuards.js";
9
+ import {
10
+ captureRepoSnapshot,
11
+ buildChangeArtifacts,
12
+ type ChangeArtifacts,
13
+ type ClassifiedChange,
14
+ type ChangedFile,
15
+ } from "../runner/changeCapture.js";
16
+
17
+ // ── Types ──────────────────────────────────────────────────────────
18
+
19
+ export interface FinalizeDirectSessionInput {
20
+ session_id: string;
21
+ }
22
+
23
+ export interface FinalizeDirectSessionOutput {
24
+ session_id: string;
25
+ changed_files_total: number;
26
+ source_changes: ClassifiedChange[];
27
+ tracked_build_artifacts: ClassifiedChange[];
28
+ runtime_generated_files: ClassifiedChange[];
29
+ suspicious_changes: ClassifiedChange[];
30
+ out_of_scope_changes: ClassifiedChange[];
31
+ verification_summary: Record<string, "passed" | "failed">;
32
+ diff_path: string;
33
+ summary_path: string;
34
+ finalized: boolean;
35
+ next_action: string;
36
+ }
37
+
38
+ // ── Main function ──────────────────────────────────────────────────
39
+
40
+ export function finalizeDirectSession(
41
+ input: FinalizeDirectSessionInput
42
+ ): FinalizeDirectSessionOutput {
43
+ const { session_id } = input;
44
+
45
+ // 1. Read session and guard active (not expired, not finalized)
46
+ const session = readDirectSession(session_id);
47
+ guardDirectSessionActive(session);
48
+
49
+ // 2. Capture after snapshot
50
+ const afterSnapshot = captureRepoSnapshot(session.resolved_repo_path);
51
+
52
+ // 3. Build change artifacts from before/after snapshots
53
+ const changeArtifacts = buildChangeArtifacts(
54
+ session.resolved_repo_path,
55
+ session.workspace_snapshot_before,
56
+ afterSnapshot
57
+ );
58
+
59
+ // 4. Finalize session record — writes changed-files.json, diff.patch,
60
+ // summary.json, summary.md and marks the session as finalized.
61
+ finalizeDirectSessionRecord(session_id, afterSnapshot, changeArtifacts);
62
+
63
+ // 5. Build verification summary from session.verification_runs
64
+ const verification_summary: Record<string, "passed" | "failed"> = {};
65
+ for (const run of session.verification_runs) {
66
+ verification_summary[run.command] = run.passed ? "passed" : "failed";
67
+ }
68
+
69
+ // 6. Identify out-of-scope changes (paths outside resolved_repo_path)
70
+ const out_of_scope_changes = findOutOfScopeChanges(
71
+ changeArtifacts,
72
+ session.resolved_repo_path
73
+ );
74
+
75
+ // 7. Build compact summary with categorized changes
76
+ const sessionDir = getDirectSessionDir(session_id);
77
+
78
+ return {
79
+ session_id,
80
+ changed_files_total: changeArtifacts.changed_files.length,
81
+ source_changes: changeArtifacts.artifact_hygiene.source_changes,
82
+ tracked_build_artifacts:
83
+ changeArtifacts.artifact_hygiene.tracked_build_artifacts,
84
+ runtime_generated_files:
85
+ changeArtifacts.artifact_hygiene.runtime_generated_files,
86
+ suspicious_changes: changeArtifacts.artifact_hygiene.suspicious_changes,
87
+ out_of_scope_changes,
88
+ verification_summary,
89
+ diff_path: join(sessionDir, "diff.patch"),
90
+ summary_path: join(sessionDir, "summary.md"),
91
+ finalized: true,
92
+ next_action: "Call audit_session to independently review the changes.",
93
+ };
94
+ }
95
+
96
+ // ── Helpers ────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Filter changeArtifacts.changed_files for paths that resolve outside the
100
+ * session's resolved_repo_path and map them to ClassifiedChange format.
101
+ */
102
+ function findOutOfScopeChanges(
103
+ artifacts: ChangeArtifacts,
104
+ resolvedRepoPath: string
105
+ ): ClassifiedChange[] {
106
+ const normalizedRepo = resolve(resolvedRepoPath);
107
+ return artifacts.changed_files
108
+ .filter((file) => {
109
+ const fullPath = resolve(normalizedRepo, file.path);
110
+ const rel = relative(normalizedRepo, fullPath);
111
+ return isAbsolute(rel) || rel.startsWith("..");
112
+ })
113
+ .map((file) => toClassifiedChange(file));
114
+ }
115
+
116
+ /**
117
+ * Map a ChangedFile to the ClassifiedChange format by adding a reason string.
118
+ */
119
+ function toClassifiedChange(file: ChangedFile): ClassifiedChange {
120
+ return {
121
+ path: file.path,
122
+ change: file.change,
123
+ tracked: file.tracked,
124
+ ignored: file.ignored,
125
+ kind: file.kind,
126
+ reason: classificationReason(file),
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Produce a human-readable reason for a changed file classification.
132
+ * Mirrors the logic in changeCapture.ts classifyArtifactHygiene.
133
+ */
134
+ function classificationReason(change: ChangedFile): string {
135
+ if (change.ignored)
136
+ return "untracked path is ignored by repository Git rules";
137
+ if (change.kind === "build_artifact" && change.tracked)
138
+ return "artifact-like path is tracked by Git and requires review";
139
+ if (change.kind === "build_artifact")
140
+ return "artifact-like path is not ignored and requires review";
141
+ if (change.kind === "runtime_generated")
142
+ return "runtime-generated path is not ignored and requires review";
143
+ return change.tracked ? "tracked source change" : "untracked source change";
144
+ }
@@ -78,9 +78,54 @@ export interface TaskSummaryOutput {
78
78
  watcher: unknown;
79
79
  pending_reason: string | null;
80
80
  execution_blocked: boolean;
81
+ artifact_hygiene: Record<string, unknown>;
81
82
  }
82
83
 
83
- export function getTaskSummary(taskId: string): TaskSummaryOutput {
84
+ export interface CompactTaskSummaryOutput {
85
+ view: "compact";
86
+ task_id: string;
87
+ status: string;
88
+ terminal: boolean;
89
+ acceptance_status: TaskSummaryOutput["acceptance_status"];
90
+ phase: string;
91
+ repo_path: string;
92
+ changed_files_total: number;
93
+ out_of_scope_changes_total: number;
94
+ artifact_hygiene: {
95
+ counts: Record<string, number>;
96
+ source_changes: unknown[];
97
+ tracked_build_artifacts: unknown[];
98
+ ignored_untracked_artifacts: unknown[];
99
+ runtime_generated_files: unknown[];
100
+ suspicious_changes: unknown[];
101
+ max_items: number;
102
+ truncated: boolean;
103
+ };
104
+ verification_summary: TaskSummaryOutput["verification_summary"];
105
+ summary: string;
106
+ warnings: string[];
107
+ errors: string[];
108
+ failure_reason: string | null;
109
+ failed_command: string | null;
110
+ suggested_next_action: string;
111
+ execution_blocked: boolean;
112
+ pending_reason: string | null;
113
+ redacted: boolean;
114
+ redaction_categories: string[];
115
+ }
116
+
117
+ export interface GetTaskSummaryOptions {
118
+ view?: "compact" | "standard";
119
+ max_items?: number;
120
+ }
121
+
122
+ export type TaskSummaryResult = TaskSummaryOutput | CompactTaskSummaryOutput;
123
+
124
+ export function getTaskSummary(taskId: string): TaskSummaryOutput;
125
+ export function getTaskSummary(taskId: string, options: { view: "compact"; max_items?: number }): CompactTaskSummaryOutput;
126
+ export function getTaskSummary(taskId: string, options: { view?: "standard"; max_items?: number }): TaskSummaryOutput;
127
+ export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions): TaskSummaryResult;
128
+ export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions = {}): TaskSummaryResult {
84
129
  const config = getConfig();
85
130
  const taskDir = resolve(getTasksDir(config), taskId);
86
131
  const statusFile = join(taskDir, "status.json");
@@ -156,6 +201,10 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
156
201
  ? Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : Date.now()) - startedAt)
157
202
  : 0;
158
203
  const changedFiles = asArray(result.changed_files ?? status.changed_files);
204
+ const changedFilesRead = tryReadJson(join(taskDir, "changed-files.json"));
205
+ const artifactHygiene = asRecord(result.artifact_hygiene ?? changedFilesRead.data.artifact_hygiene ?? {
206
+ counts: status.artifact_hygiene_counts || {},
207
+ });
159
208
  const verifyCommands = asArray(verify.commands ?? result.verify_commands ?? result.verify?.commands);
160
209
  const testLogSummary = summarizeTestLog(join(taskDir, "test.log"));
161
210
  const verificationSummary = buildVerificationSummary(verifyStatus, verifyCommands, testLogSummary);
@@ -227,7 +276,11 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
227
276
  watcher: status.watcher,
228
277
  pending_reason: status.pending_reason || null,
229
278
  execution_blocked: Boolean(status.execution_blocked),
279
+ artifact_hygiene: artifactHygiene,
230
280
  };
281
+ if ((options.view || "standard") === "compact") {
282
+ return buildCompactSummary(output, normalizeMaxItems(options.max_items));
283
+ }
231
284
  const safe = redactSensitiveValue(output);
232
285
  return {
233
286
  ...safe.value,
@@ -236,6 +289,59 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
236
289
  } as TaskSummaryOutput;
237
290
  }
238
291
 
292
+ function buildCompactSummary(output: Record<string, any>, maxItems: number): CompactTaskSummaryOutput {
293
+ const hygiene = asRecord(output.artifact_hygiene);
294
+ const groupNames = [
295
+ "source_changes",
296
+ "tracked_build_artifacts",
297
+ "ignored_untracked_artifacts",
298
+ "runtime_generated_files",
299
+ "suspicious_changes",
300
+ ] as const;
301
+ const groups = Object.fromEntries(groupNames.map((name) => [name, asArray(hygiene[name]).slice(0, maxItems)]));
302
+ const truncated = groupNames.some((name) => asArray(hygiene[name]).length > maxItems);
303
+ const compact = {
304
+ view: "compact" as const,
305
+ task_id: String(output.task_id),
306
+ status: String(output.status),
307
+ terminal: Boolean(output.terminal),
308
+ acceptance_status: output.acceptance_status,
309
+ phase: String(output.phase),
310
+ repo_path: String(output.repo_path),
311
+ changed_files_total: asArray(output.changed_files).length,
312
+ out_of_scope_changes_total: asArray(output.out_of_scope_changes).length,
313
+ artifact_hygiene: {
314
+ counts: asRecord(hygiene.counts) as Record<string, number>,
315
+ ...groups,
316
+ max_items: maxItems,
317
+ truncated,
318
+ },
319
+ verification_summary: output.verification_summary,
320
+ summary: String(output.summary).slice(0, 1000),
321
+ warnings: asArray(output.warnings).slice(0, maxItems),
322
+ errors: asArray(output.errors).slice(0, maxItems),
323
+ failure_reason: output.failure_reason || null,
324
+ failed_command: output.failed_command || null,
325
+ suggested_next_action: String(output.suggested_next_action),
326
+ execution_blocked: Boolean(output.execution_blocked),
327
+ pending_reason: output.pending_reason || null,
328
+ };
329
+ const safe = redactSensitiveValue(compact);
330
+ return {
331
+ ...safe.value,
332
+ redacted: safe.redacted,
333
+ redaction_categories: safe.redaction_categories,
334
+ } as CompactTaskSummaryOutput;
335
+ }
336
+
337
+ function normalizeMaxItems(value: number | undefined): number {
338
+ if (value === undefined) return 8;
339
+ if (!Number.isInteger(value) || value < 1 || value > 50) {
340
+ throw new Error("max_items must be an integer from 1 to 50.");
341
+ }
342
+ return value;
343
+ }
344
+
239
345
  function tryReadJson(path: string): { data: Record<string, any>; error?: string } {
240
346
  if (!existsSync(path)) return { data: {} };
241
347
  try {
@@ -249,6 +355,10 @@ function asArray(value: unknown): any[] {
249
355
  return Array.isArray(value) ? value : [];
250
356
  }
251
357
 
358
+ function asRecord(value: unknown): Record<string, any> {
359
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
360
+ }
361
+
252
362
  function summarizeTestLog(path: string): string {
253
363
  if (!existsSync(path)) return "test.log missing";
254
364
  const text = readFileSync(path, "utf-8");
@@ -1,12 +1,12 @@
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
8
  import type { ToolCatalogSnapshot } from "./toolCatalog.js";
9
- import { CHATGPT_CORE_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
9
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
10
10
  import { readWatcherStatus } from "../watcherStatus.js";
11
11
  import { listTasks } from "./listTasks.js";
12
12
 
@@ -35,9 +35,17 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
35
35
  let profileConsistent = true;
36
36
  if (catalog) {
37
37
  const activeProfile = resolveToolProfile(config.toolProfile);
38
- const expectedNames = activeProfile === "chatgpt_core"
39
- ? [...CHATGPT_CORE_TOOL_NAMES]
40
- : null; // full profile: all tools are accepted
38
+ let expectedNames: string[] | null = null;
39
+ if (activeProfile === "chatgpt_core") {
40
+ expectedNames = [...CHATGPT_CORE_TOOL_NAMES];
41
+ } else if (activeProfile === "chatgpt_direct" && config.enableDirectProfile) {
42
+ expectedNames = [...CHATGPT_DIRECT_TOOL_NAMES];
43
+ } else if (activeProfile === "chatgpt_direct" && !config.enableDirectProfile) {
44
+ // Degraded mode: only health_check should be exposed
45
+ expectedNames = ["health_check"];
46
+ profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
47
+ profileConsistent = false;
48
+ }
41
49
  if (expectedNames) {
42
50
  const catalogNames = new Set(catalog.tool_names);
43
51
  for (const name of expectedNames) {
@@ -76,6 +84,11 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
76
84
  const status = watcher.available && workspace.available && tasks.available && agentReady && catalogConsistent
77
85
  ? "healthy"
78
86
  : "degraded";
87
+
88
+ const directSessionsDir = getDirectSessionsDir(config);
89
+ const directSessions = directoryStatus(directSessionsDir, true, workspaceRoot);
90
+ const activeProfile = resolveToolProfile(config.toolProfile);
91
+
79
92
  return {
80
93
  status,
81
94
  server_version: catalog?.server_version || PATCHWARDEN_VERSION,
@@ -89,11 +102,15 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
89
102
  catalog_consistent: catalogConsistent,
90
103
  mismatch_report: mismatchReport,
91
104
  tunnel_catalog_comparison: tunnelCatalogComparison,
105
+ direct_profile_enabled: config.enableDirectProfile ?? false,
106
+ direct_sessions_dir: directSessions,
107
+ direct_session_ttl_seconds: config.directSessionTtlSeconds,
108
+ ...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
92
109
  connector_visibility: {
93
110
  status: "not_observable_server_side",
94
111
  verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
95
112
  refresh_steps: [
96
- "1. Run Check-PatchWarden-Health.cmd to confirm tool_profile=chatgpt_core, tool_count=16, and catalog_consistent=true.",
113
+ "1. Run PatchWarden.cmd health to confirm the active profile, tool count, and catalog consistency.",
97
114
  "2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
98
115
  "3. Open a NEW ChatGPT conversation (old conversations retain their cached tool catalog).",
99
116
  "4. Call health_check in the new conversation; verify tool_manifest_sha256 matches the local report.",
@@ -1,8 +1,14 @@
1
- import { readFileSync, statSync } from "node:fs";
1
+ import { readFileSync, statSync, existsSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { resolve, relative, isAbsolute } from "node:path";
2
4
  import { getConfig } from "../config.js";
3
5
  import { guardReadPath } from "../security/pathGuard.js";
4
6
  import { guardSensitivePath } from "../security/sensitiveGuard.js";
5
7
  import { redactSensitiveContent } from "../security/contentRedaction.js";
8
+ import { PatchWardenError } from "../errors.js";
9
+ import { readDirectSession } from "../direct/directSessionStore.js";
10
+ import { guardDirectSessionActive, guardDirectReadPath, isBinaryFile, guardDirectFileSize } from "../direct/directGuards.js";
11
+ import { resolveToolProfile } from "./toolCatalog.js";
6
12
 
7
13
  export interface ReadWorkspaceFileOutput {
8
14
  path: string;
@@ -10,11 +16,41 @@ export interface ReadWorkspaceFileOutput {
10
16
  size: number;
11
17
  redacted?: boolean;
12
18
  redaction_categories?: string[];
19
+ // Direct mode extensions
20
+ relative_path?: string;
21
+ sha256?: string;
13
22
  }
14
23
 
15
- export function readWorkspaceFile(relativePath: string): ReadWorkspaceFileOutput {
24
+ export interface ReadWorkspaceFileInput {
25
+ path: string;
26
+ session_id?: string;
27
+ }
28
+
29
+ export function readWorkspaceFile(input: string | ReadWorkspaceFileInput): ReadWorkspaceFileOutput {
16
30
  const config = getConfig();
17
31
 
32
+ // Support both old string API and new object API
33
+ const relativePath = typeof input === "string" ? input : input.path;
34
+ const sessionId = typeof input === "string" ? undefined : input.session_id;
35
+
36
+ // Direct mode: session_id provided
37
+ if (sessionId) {
38
+ return readWorkspaceFileDirect(relativePath, sessionId);
39
+ }
40
+
41
+ // In chatgpt_direct profile, session_id is required
42
+ const profile = resolveToolProfile(config.toolProfile);
43
+ if (profile === "chatgpt_direct") {
44
+ throw new PatchWardenError(
45
+ "session_id_required",
46
+ "session_id is required when using the chatgpt_direct profile.",
47
+ "Provide a session_id from create_direct_session.",
48
+ true,
49
+ { operation: "read_workspace_file" }
50
+ );
51
+ }
52
+
53
+ // Compatibility mode: no session_id, original behavior
18
54
  const safePath = guardReadPath(relativePath, config.workspaceRoot);
19
55
  guardSensitivePath(safePath);
20
56
 
@@ -39,3 +75,50 @@ export function readWorkspaceFile(relativePath: string): ReadWorkspaceFileOutput
39
75
  redaction_categories: redaction.redaction_categories,
40
76
  };
41
77
  }
78
+
79
+ function readWorkspaceFileDirect(relativePath: string, sessionId: string): ReadWorkspaceFileOutput {
80
+ const config = getConfig();
81
+ const session = readDirectSession(sessionId);
82
+ guardDirectSessionActive(session);
83
+
84
+ // Resolve path within session's repo
85
+ const safePath = guardDirectReadPath(relativePath, session.resolved_repo_path, config.workspaceRoot);
86
+
87
+ if (!existsSync(safePath)) {
88
+ throw new PatchWardenError(
89
+ "file_not_found",
90
+ `File "${relativePath}" not found in session repo.`,
91
+ "Use search_workspace to find available files.",
92
+ true,
93
+ { path: relativePath, session_id: sessionId }
94
+ );
95
+ }
96
+
97
+ const stat = statSync(safePath);
98
+ guardDirectFileSize(stat.size);
99
+
100
+ // Block binary files
101
+ if (isBinaryFile(safePath)) {
102
+ throw new PatchWardenError(
103
+ "binary_file_blocked",
104
+ `File "${relativePath}" appears to be a binary file.`,
105
+ "Binary files cannot be read in Direct mode.",
106
+ true,
107
+ { path: relativePath }
108
+ );
109
+ }
110
+
111
+ const content = readFileSync(safePath, "utf-8");
112
+ const redaction = redactSensitiveContent(content);
113
+ const sha256 = createHash("sha256").update(content, "utf-8").digest("hex");
114
+
115
+ return {
116
+ path: safePath,
117
+ relative_path: relativePath,
118
+ content: redaction.content,
119
+ size: stat.size,
120
+ sha256,
121
+ redacted: redaction.redacted,
122
+ redaction_categories: redaction.redaction_categories,
123
+ };
124
+ }