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
@@ -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");
@@ -92,6 +92,10 @@ export function getTaskSummary(taskId) {
92
92
  ? Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : Date.now()) - startedAt)
93
93
  : 0;
94
94
  const changedFiles = asArray(result.changed_files ?? status.changed_files);
95
+ const changedFilesRead = tryReadJson(join(taskDir, "changed-files.json"));
96
+ const artifactHygiene = asRecord(result.artifact_hygiene ?? changedFilesRead.data.artifact_hygiene ?? {
97
+ counts: status.artifact_hygiene_counts || {},
98
+ });
95
99
  const verifyCommands = asArray(verify.commands ?? result.verify_commands ?? result.verify?.commands);
96
100
  const testLogSummary = summarizeTestLog(join(taskDir, "test.log"));
97
101
  const verificationSummary = buildVerificationSummary(verifyStatus, verifyCommands, testLogSummary);
@@ -156,7 +160,11 @@ export function getTaskSummary(taskId) {
156
160
  watcher: status.watcher,
157
161
  pending_reason: status.pending_reason || null,
158
162
  execution_blocked: Boolean(status.execution_blocked),
163
+ artifact_hygiene: artifactHygiene,
159
164
  };
165
+ if ((options.view || "standard") === "compact") {
166
+ return buildCompactSummary(output, normalizeMaxItems(options.max_items));
167
+ }
160
168
  const safe = redactSensitiveValue(output);
161
169
  return {
162
170
  ...safe.value,
@@ -164,6 +172,58 @@ export function getTaskSummary(taskId) {
164
172
  redaction_categories: safe.redaction_categories,
165
173
  };
166
174
  }
175
+ function buildCompactSummary(output, maxItems) {
176
+ const hygiene = asRecord(output.artifact_hygiene);
177
+ const groupNames = [
178
+ "source_changes",
179
+ "tracked_build_artifacts",
180
+ "ignored_untracked_artifacts",
181
+ "runtime_generated_files",
182
+ "suspicious_changes",
183
+ ];
184
+ const groups = Object.fromEntries(groupNames.map((name) => [name, asArray(hygiene[name]).slice(0, maxItems)]));
185
+ const truncated = groupNames.some((name) => asArray(hygiene[name]).length > maxItems);
186
+ const compact = {
187
+ view: "compact",
188
+ task_id: String(output.task_id),
189
+ status: String(output.status),
190
+ terminal: Boolean(output.terminal),
191
+ acceptance_status: output.acceptance_status,
192
+ phase: String(output.phase),
193
+ repo_path: String(output.repo_path),
194
+ changed_files_total: asArray(output.changed_files).length,
195
+ out_of_scope_changes_total: asArray(output.out_of_scope_changes).length,
196
+ artifact_hygiene: {
197
+ counts: asRecord(hygiene.counts),
198
+ ...groups,
199
+ max_items: maxItems,
200
+ truncated,
201
+ },
202
+ verification_summary: output.verification_summary,
203
+ summary: String(output.summary).slice(0, 1000),
204
+ warnings: asArray(output.warnings).slice(0, maxItems),
205
+ errors: asArray(output.errors).slice(0, maxItems),
206
+ failure_reason: output.failure_reason || null,
207
+ failed_command: output.failed_command || null,
208
+ suggested_next_action: String(output.suggested_next_action),
209
+ execution_blocked: Boolean(output.execution_blocked),
210
+ pending_reason: output.pending_reason || null,
211
+ };
212
+ const safe = redactSensitiveValue(compact);
213
+ return {
214
+ ...safe.value,
215
+ redacted: safe.redacted,
216
+ redaction_categories: safe.redaction_categories,
217
+ };
218
+ }
219
+ function normalizeMaxItems(value) {
220
+ if (value === undefined)
221
+ return 8;
222
+ if (!Number.isInteger(value) || value < 1 || value > 50) {
223
+ throw new Error("max_items must be an integer from 1 to 50.");
224
+ }
225
+ return value;
226
+ }
167
227
  function tryReadJson(path) {
168
228
  if (!existsSync(path))
169
229
  return { data: {} };
@@ -177,6 +237,9 @@ function tryReadJson(path) {
177
237
  function asArray(value) {
178
238
  return Array.isArray(value) ? value : [];
179
239
  }
240
+ function asRecord(value) {
241
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
242
+ }
180
243
  function summarizeTestLog(path) {
181
244
  if (!existsSync(path))
182
245
  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;
@@ -76,4 +64,26 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
76
64
  agent_status: {
77
65
  [k: string]: string;
78
66
  };
67
+ direct_tool_count?: number | undefined;
68
+ status: string;
69
+ server_version: string;
70
+ schema_epoch: string;
71
+ tool_profile: string;
72
+ tool_count: number | null;
73
+ tool_names: string[];
74
+ tool_manifest_sha256: string | null;
75
+ profile_consistent: boolean;
76
+ profile_errors: string[];
77
+ catalog_consistent: boolean;
78
+ mismatch_report: string[];
79
+ tunnel_catalog_comparison: string;
80
+ direct_profile_enabled: boolean;
81
+ direct_sessions_dir: {
82
+ available: boolean;
83
+ path: string;
84
+ readable: boolean;
85
+ writable: boolean;
86
+ reason: string | null;
87
+ };
88
+ direct_session_ttl_seconds: number;
79
89
  };
@@ -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.",
@@ -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
+ }