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
package/dist/doctor.js CHANGED
@@ -17,7 +17,7 @@ import { isSensitivePath } from "./security/sensitiveGuard.js";
17
17
  import { guardPlanContent } from "./security/planGuard.js";
18
18
  import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
19
19
  import { getToolDefs } from "./tools/registry.js";
20
- import { CHATGPT_CORE_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
20
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
21
21
  import { PATCHWARDEN_VERSION } from "./version.js";
22
22
  // ── State ──────────────────────────────────────────────────────────
23
23
  let ok = 0;
@@ -59,6 +59,7 @@ function cmd(cmdStr) {
59
59
  // ══════════════════════════════════════════════════════════════════
60
60
  async function main() {
61
61
  console.log("PatchWarden Doctor\n");
62
+ const allowDefaultConfig = process.argv.includes("--allow-default-config");
62
63
  // 1. Node version
63
64
  const nodeVer = process.version;
64
65
  const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
@@ -66,6 +67,16 @@ async function main() {
66
67
  // 2. npm
67
68
  const npmVer = cmd("npm --version");
68
69
  check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
70
+ // Local-only medium-risk confirmation entrypoint. It must exist as a package
71
+ // binary but must not be exposed as an MCP tool.
72
+ try {
73
+ const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
74
+ check("patchwarden-confirm package binary", packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js", packageJson.bin?.["patchwarden-confirm"] || "missing");
75
+ check("patchwarden-confirm compiled entrypoint", existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")), "run npm.cmd run build");
76
+ }
77
+ catch (error) {
78
+ check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
79
+ }
69
80
  // 3. Git
70
81
  const gitVer = cmd("git --version");
71
82
  warnCheck("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work");
@@ -81,9 +92,15 @@ async function main() {
81
92
  break;
82
93
  }
83
94
  }
84
- check("Config file exists", configPathUsed !== "", configPathUsed
95
+ const configDetail = configPathUsed
85
96
  ? configPathUsed
86
- : 'Create one: cp examples/config.example.json patchwarden.config.json');
97
+ : 'Create one: cp examples/config.example.json patchwarden.config.json';
98
+ if (allowDefaultConfig) {
99
+ warnCheck("Config file exists", configPathUsed !== "", configDetail);
100
+ }
101
+ else {
102
+ check("Config file exists", configPathUsed !== "", configDetail);
103
+ }
87
104
  // 5. PATCHWARDEN_CONFIG env
88
105
  if (process.env.PATCHWARDEN_CONFIG) {
89
106
  results.push(`[OK] PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`);
@@ -198,15 +215,26 @@ async function main() {
198
215
  try {
199
216
  process.env.PATCHWARDEN_TOOL_PROFILE = "full";
200
217
  const fullTools = getToolDefs();
201
- const coreTools = selectToolsForProfile(fullTools, "chatgpt_core");
218
+ const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
202
219
  const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
203
220
  const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
204
- check("Full tool profile exposes 22 tools", fullTools.length === 22, `${fullTools.length} tools`);
221
+ check("Full tool profile exposes 30 tools", fullTools.length === 30, `${fullTools.length} tools`);
205
222
  check(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", "));
206
223
  check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
207
224
  createSchema?.properties?.verify_commands &&
208
225
  waitSchema?.properties?.timeout_seconds &&
209
226
  waitSchema?.properties?.wait_seconds));
227
+ // chatgpt_direct profile checks
228
+ const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
229
+ check("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`);
230
+ if (config?.enableDirectProfile) {
231
+ const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
232
+ check(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", "));
233
+ }
234
+ else {
235
+ results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
236
+ ok++;
237
+ }
210
238
  }
211
239
  finally {
212
240
  if (previousProfile === undefined)
@@ -259,6 +287,31 @@ async function main() {
259
287
  const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
260
288
  check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
261
289
  }
290
+ // Direct tool module checks
291
+ const directToolModules = [
292
+ "createDirectSession",
293
+ "searchWorkspace",
294
+ "applyPatch",
295
+ "runVerification",
296
+ "finalizeDirectSession",
297
+ "auditSession",
298
+ ];
299
+ for (const t of directToolModules) {
300
+ const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
301
+ check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
302
+ }
303
+ // Direct support module checks
304
+ const directSupportModules = [
305
+ "directSessionStore",
306
+ "directGuards",
307
+ "directPatch",
308
+ "directVerification",
309
+ "directAudit",
310
+ ];
311
+ for (const t of directSupportModules) {
312
+ const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
313
+ check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
314
+ }
262
315
  // Task directory writable
263
316
  if (config) {
264
317
  const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
@@ -334,11 +387,26 @@ async function main() {
334
387
  }
335
388
  }
336
389
  }
390
+ // 14b. Direct profile config checks
391
+ if (config) {
392
+ const directCmds = config.directAllowedCommands || [];
393
+ warnCheck("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
394
+ // npm run doctor should NOT be in default Direct whitelist
395
+ const hasDoctor = directCmds.some((c) => c === "npm run doctor");
396
+ check("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
397
+ check("directSessionTtlSeconds is valid", config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400, `${config.directSessionTtlSeconds}s`);
398
+ check("directMaxPatchBytes is positive", config.directMaxPatchBytes > 0, `${config.directMaxPatchBytes}`);
399
+ check("directMaxFileBytes is positive", config.directMaxFileBytes > 0, `${config.directMaxFileBytes}`);
400
+ }
337
401
  // 15. Tunnel example files check
338
402
  const tunnelFiles = [
339
403
  "examples/openai-tunnel/README.md",
340
404
  "examples/openai-tunnel/tunnel-client.example.yaml",
341
405
  "examples/openai-tunnel/chatgpt-test-prompt.md",
406
+ "scripts/patchwarden-mcp-direct.cmd",
407
+ "PatchWarden.cmd",
408
+ "scripts/manage-patchwarden.ps1",
409
+ "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
342
410
  ];
343
411
  for (const tf of tunnelFiles) {
344
412
  const full = resolve(process.cwd(), tf);
@@ -0,0 +1,52 @@
1
+ export type LogLevel = "info" | "warn" | "error" | "audit";
2
+ export interface LogEntry {
3
+ timestamp: string;
4
+ level: LogLevel;
5
+ message: string;
6
+ [key: string]: unknown;
7
+ }
8
+ /**
9
+ * Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
10
+ * Verbose mode enables logging of sanitized tool-call arguments.
11
+ */
12
+ export declare function isVerboseLogging(): boolean;
13
+ /**
14
+ * Structured logger that emits JSON lines to stderr.
15
+ *
16
+ * Each entry contains `timestamp`, `level`, `message`, plus any optional
17
+ * context fields supplied by the caller.
18
+ */
19
+ export declare class Logger {
20
+ info(message: string, context?: Record<string, unknown>): void;
21
+ warn(message: string, context?: Record<string, unknown>): void;
22
+ error(message: string, context?: Record<string, unknown>): void;
23
+ /**
24
+ * Emit a tool-call audit log entry.
25
+ *
26
+ * Required fields: `tool`, `ok`, `duration_ms`.
27
+ * Optional fields: `error_reason`, `task_id`.
28
+ *
29
+ * By default raw arguments are NOT logged. When verbose mode is enabled
30
+ * (PATCHWARDEN_VERBOSE_LOG=true) and `args` is provided, the arguments
31
+ * are serialized and sanitized via `redactSensitiveContent` before being
32
+ * included in the `args` field.
33
+ */
34
+ audit(tool: string, ok: boolean, durationMs: number, errorReason?: string, taskId?: string, args?: unknown): void;
35
+ }
36
+ /** Default singleton logger instance. */
37
+ export declare const logger: Logger;
38
+ /**
39
+ * Produce a structured error log entry for an unhandled rejection or
40
+ * uncaught exception. Writes JSON to stderr.
41
+ */
42
+ export declare function logUnhandledError(error: unknown): void;
43
+ /**
44
+ * Register process-level handlers for `unhandledRejection` and
45
+ * `uncaughtException`.
46
+ *
47
+ * Both handlers log structured error output to stderr. The
48
+ * `uncaughtException` handler does NOT swallow the fatal error — after
49
+ * logging it exits with a non-zero status code to preserve the default
50
+ * crash behaviour required for truly fatal failures.
51
+ */
52
+ export declare function installGlobalHandlers(): void;
@@ -0,0 +1,123 @@
1
+ import { redactSensitiveContent } from "./security/contentRedaction.js";
2
+ // ── Helpers ───────────────────────────────────────────────────────
3
+ /**
4
+ * Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
5
+ * Verbose mode enables logging of sanitized tool-call arguments.
6
+ */
7
+ export function isVerboseLogging() {
8
+ return process.env.PATCHWARDEN_VERBOSE_LOG === "true";
9
+ }
10
+ /**
11
+ * Serialize a value to a JSON string, falling back to String() on failure
12
+ * (e.g. circular references, functions, symbols).
13
+ */
14
+ function safeStringify(value) {
15
+ try {
16
+ const result = JSON.stringify(value);
17
+ return result === undefined ? String(value) : result;
18
+ }
19
+ catch {
20
+ return String(value);
21
+ }
22
+ }
23
+ /**
24
+ * Write a single JSON log line to stderr.
25
+ *
26
+ * All log output goes to stderr — NEVER stdout — so that MCP JSON-RPC
27
+ * traffic on stdout is never polluted by log messages.
28
+ */
29
+ function emit(entry) {
30
+ process.stderr.write(JSON.stringify(entry) + "\n");
31
+ }
32
+ // ── Logger ────────────────────────────────────────────────────────
33
+ /**
34
+ * Structured logger that emits JSON lines to stderr.
35
+ *
36
+ * Each entry contains `timestamp`, `level`, `message`, plus any optional
37
+ * context fields supplied by the caller.
38
+ */
39
+ export class Logger {
40
+ info(message, context) {
41
+ emit({ timestamp: new Date().toISOString(), level: "info", message, ...context });
42
+ }
43
+ warn(message, context) {
44
+ emit({ timestamp: new Date().toISOString(), level: "warn", message, ...context });
45
+ }
46
+ error(message, context) {
47
+ emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
48
+ }
49
+ /**
50
+ * Emit a tool-call audit log entry.
51
+ *
52
+ * Required fields: `tool`, `ok`, `duration_ms`.
53
+ * Optional fields: `error_reason`, `task_id`.
54
+ *
55
+ * By default raw arguments are NOT logged. When verbose mode is enabled
56
+ * (PATCHWARDEN_VERBOSE_LOG=true) and `args` is provided, the arguments
57
+ * are serialized and sanitized via `redactSensitiveContent` before being
58
+ * included in the `args` field.
59
+ */
60
+ audit(tool, ok, durationMs, errorReason, taskId, args) {
61
+ const entry = {
62
+ timestamp: new Date().toISOString(),
63
+ level: "audit",
64
+ message: "tool_call_audit",
65
+ tool,
66
+ ok,
67
+ duration_ms: durationMs,
68
+ };
69
+ if (errorReason !== undefined)
70
+ entry.error_reason = errorReason;
71
+ if (taskId !== undefined)
72
+ entry.task_id = taskId;
73
+ if (args !== undefined && isVerboseLogging()) {
74
+ const redacted = redactSensitiveContent(safeStringify(args));
75
+ entry.args = redacted.content;
76
+ }
77
+ emit(entry);
78
+ }
79
+ }
80
+ /** Default singleton logger instance. */
81
+ export const logger = new Logger();
82
+ // ── Unhandled error helpers ───────────────────────────────────────
83
+ /**
84
+ * Produce a structured error log entry for an unhandled rejection or
85
+ * uncaught exception. Writes JSON to stderr.
86
+ */
87
+ export function logUnhandledError(error) {
88
+ const entry = {
89
+ timestamp: new Date().toISOString(),
90
+ level: "error",
91
+ message: "unhandled_error",
92
+ error: error instanceof Error
93
+ ? error.message
94
+ : typeof error === "string"
95
+ ? error
96
+ : safeStringify(error),
97
+ error_name: error instanceof Error ? error.name : typeof error,
98
+ };
99
+ if (error instanceof Error && error.stack) {
100
+ entry.stack = error.stack;
101
+ }
102
+ emit(entry);
103
+ }
104
+ /**
105
+ * Register process-level handlers for `unhandledRejection` and
106
+ * `uncaughtException`.
107
+ *
108
+ * Both handlers log structured error output to stderr. The
109
+ * `uncaughtException` handler does NOT swallow the fatal error — after
110
+ * logging it exits with a non-zero status code to preserve the default
111
+ * crash behaviour required for truly fatal failures.
112
+ */
113
+ export function installGlobalHandlers() {
114
+ process.on("unhandledRejection", (reason) => {
115
+ logUnhandledError(reason);
116
+ });
117
+ process.on("uncaughtException", (error) => {
118
+ logUnhandledError(error);
119
+ // Do not swallow: exit with failure so the process does not continue
120
+ // in an undefined state.
121
+ process.exit(1);
122
+ });
123
+ }
@@ -0,0 +1,23 @@
1
+ import { PatchWardenConfig } from "../config.js";
2
+ export interface AgentInvocation {
3
+ command: string;
4
+ args: string[];
5
+ cwd: string;
6
+ commandLabel: string;
7
+ promptMode: "inline" | "file";
8
+ promptFilePath?: string;
9
+ }
10
+ /**
11
+ * Build agent invocation parameters from config.
12
+ * Replaces {repo}, {prompt}, and {prompt_file} placeholders.
13
+ * runTask and agentAssessor share this function to ensure consistent agent startup.
14
+ */
15
+ export declare function buildAgentInvocation(agentName: string, repoPath: string, prompt: string, config: PatchWardenConfig, promptFilePath?: string): AgentInvocation;
16
+ /**
17
+ * Build task execution prompt. Mechanically extracted from runTask.ts.
18
+ */
19
+ export declare function buildExecutionPrompt(plan: string, repoPath: string, testCommand: string): string;
20
+ /**
21
+ * Build agentAssessor inspect-only prompt.
22
+ */
23
+ export declare function buildAssessmentPrompt(goal: string, planContent: string, repoPath: string): string;
@@ -0,0 +1,106 @@
1
+ import { basename } from "node:path";
2
+ import { guardAgentCommand, sanitizePromptArg } from "../security/commandGuard.js";
3
+ /**
4
+ * Build agent invocation parameters from config.
5
+ * Replaces {repo}, {prompt}, and {prompt_file} placeholders.
6
+ * runTask and agentAssessor share this function to ensure consistent agent startup.
7
+ */
8
+ export function buildAgentInvocation(agentName, repoPath, prompt, config, promptFilePath) {
9
+ const agentCmd = guardAgentCommand(agentName, config);
10
+ const sanitizedPrompt = sanitizePromptArg(prompt);
11
+ const hasPromptFilePlaceholder = agentCmd.args.includes("{prompt_file}");
12
+ const promptMode = hasPromptFilePlaceholder && promptFilePath ? "file" : "inline";
13
+ const resolvedArgs = agentCmd.args.map((arg) => {
14
+ if (arg === "{repo}")
15
+ return repoPath;
16
+ if (arg === "{prompt}")
17
+ return sanitizedPrompt;
18
+ if (arg === "{prompt_file}" && promptMode === "file" && promptFilePath)
19
+ return promptFilePath;
20
+ return arg;
21
+ });
22
+ return {
23
+ command: agentCmd.command,
24
+ args: resolvedArgs,
25
+ cwd: repoPath,
26
+ commandLabel: `${basename(agentCmd.command)} (configured agent command)`,
27
+ promptMode,
28
+ ...(promptMode === "file" && promptFilePath ? { promptFilePath } : {}),
29
+ };
30
+ }
31
+ /**
32
+ * Build task execution prompt. Mechanically extracted from runTask.ts.
33
+ */
34
+ export function buildExecutionPrompt(plan, repoPath, testCommand) {
35
+ let prompt = `You are executing a pre-written plan in a local repository.
36
+
37
+ ## Repository
38
+ ${repoPath}
39
+
40
+ ## Plan
41
+ ${plan}
42
+
43
+ ## Instructions
44
+ 1. Read the plan carefully.
45
+ 2. Implement the changes in this repository only.
46
+ 3. Do NOT modify files outside this repository.
47
+ 4. Leave repository changes uncommitted for review; remote operations are outside this task.
48
+ 5. After implementing, describe what you changed.
49
+ 6. Output a summary with what was done, files modified, and issues encountered.
50
+ `;
51
+ if (testCommand) {
52
+ prompt += `\n7. You may run ${testCommand}; PatchWarden will independently run it again for verification.`;
53
+ }
54
+ return prompt;
55
+ }
56
+ /**
57
+ * Build agentAssessor inspect-only prompt.
58
+ */
59
+ export function buildAssessmentPrompt(goal, planContent, repoPath) {
60
+ return `You are performing a READ-ONLY risk assessment of a planned task. Do NOT modify any files.
61
+
62
+ ## Repository
63
+ ${repoPath}
64
+
65
+ ## Goal
66
+ ${goal}
67
+
68
+ ## Plan
69
+ ${planContent}
70
+
71
+ ## Instructions
72
+ 1. Read the plan and goal carefully.
73
+ 2. Inspect the repository to understand the scope of changes.
74
+ 3. Do NOT create, edit, delete, rename, or generate any files.
75
+ 4. Assess the risk level of executing this plan.
76
+ 5. Identify affected file paths within the repository.
77
+ 6. Check for destructive actions, sensitive file access, or out-of-scope changes.
78
+ 7. Output your assessment as JSON after the marker ===ASSESSMENT_JSON=== on a new line.
79
+
80
+ ## Required JSON output format
81
+ After your analysis, output exactly this format:
82
+
83
+ ===ASSESSMENT_JSON===
84
+ {
85
+ "risk_level": "low" | "medium" | "high",
86
+ "reason_codes": ["short", "descriptive", "codes"],
87
+ "affected_paths": ["relative/path/to/file"],
88
+ "destructive_actions": ["description of any destructive actions"],
89
+ "requires_user_confirm": false,
90
+ "confidence": 0.0,
91
+ "notes": "Brief summary of findings"
92
+ }
93
+
94
+ ## Risk level guidelines
95
+ - low: Small source change, no sensitive files, no destructive actions
96
+ - medium: Multiple files, build artifacts, dependency changes, or uncertain scope
97
+ - high: Destructive actions, sensitive file access, out-of-scope changes, or large-scale deletion
98
+
99
+ ## Constraints
100
+ - affected_paths must be relative paths within the repository (no absolute paths)
101
+ - reason_codes: max 50 entries, each max 100 chars
102
+ - destructive_actions: max 20 entries, each max 200 chars
103
+ - notes: max 2000 chars
104
+ - confidence: number from 0.0 to 1.0
105
+ `;
106
+ }
@@ -1,6 +1,8 @@
1
1
  export interface FileFingerprint {
2
2
  size: number;
3
3
  sha256: string;
4
+ tracked: boolean;
5
+ ignored: boolean;
4
6
  }
5
7
  export interface RepoSnapshot {
6
8
  captured_at: string;
@@ -9,6 +11,7 @@ export interface RepoSnapshot {
9
11
  status: string;
10
12
  workspace_dirty: boolean;
11
13
  files: Record<string, FileFingerprint>;
14
+ dirty_paths: string[];
12
15
  warnings: string[];
13
16
  }
14
17
  export interface ChangedFile {
@@ -17,6 +20,31 @@ export interface ChangedFile {
17
20
  old_path?: string;
18
21
  before_sha256: string | null;
19
22
  after_sha256: string | null;
23
+ tracked: boolean;
24
+ ignored: boolean;
25
+ kind: "source" | "build_artifact" | "runtime_generated";
26
+ }
27
+ export interface ClassifiedChange {
28
+ path: string;
29
+ change: ChangedFile["change"];
30
+ tracked: boolean;
31
+ ignored: boolean;
32
+ kind: ChangedFile["kind"];
33
+ reason: string;
34
+ }
35
+ export interface ArtifactHygiene {
36
+ counts: {
37
+ source_changes: number;
38
+ tracked_build_artifacts: number;
39
+ ignored_untracked_artifacts: number;
40
+ runtime_generated_files: number;
41
+ suspicious_changes: number;
42
+ };
43
+ source_changes: ClassifiedChange[];
44
+ tracked_build_artifacts: ClassifiedChange[];
45
+ ignored_untracked_artifacts: ClassifiedChange[];
46
+ runtime_generated_files: ClassifiedChange[];
47
+ suspicious_changes: ClassifiedChange[];
20
48
  }
21
49
  export interface ChangeArtifacts {
22
50
  changed_files: ChangedFile[];
@@ -36,8 +64,50 @@ export interface ChangeArtifacts {
36
64
  workspace_dirty_after: boolean;
37
65
  patch_mode: "textual" | "no_changes" | "hash_only";
38
66
  unavailable_reason: string | null;
67
+ artifact_hygiene: ArtifactHygiene;
39
68
  }
40
69
  export declare function captureRepoSnapshot(repoPath: string): RepoSnapshot;
41
70
  export declare function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void;
42
71
  export declare function buildChangeArtifacts(repoPath: string, before: RepoSnapshot, after: RepoSnapshot): ChangeArtifacts;
43
72
  export declare function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[];
73
+ export declare function emptyArtifactHygiene(): ArtifactHygiene;
74
+ export interface ExternalDirtyFile {
75
+ path: string;
76
+ change: ChangedFile["change"];
77
+ before_sha256: string | null;
78
+ after_sha256: string | null;
79
+ }
80
+ /**
81
+ * Extract files that are dirty in the workspace but outside the target repo.
82
+ * Used to establish a baseline before task execution.
83
+ */
84
+ export declare function extractExternalDirtyFiles(workspaceSnapshot: RepoSnapshot, repoPath: string, workspaceRoot: string): ExternalDirtyFile[];
85
+ /**
86
+ * Compare external dirty files between baseline and post-task snapshots.
87
+ * Returns files that are NEW (not present in baseline) or CHANGED
88
+ * (same path but different sha256, meaning the task modified them).
89
+ */
90
+ export declare function findNewExternalDirtyFiles(baseline: ExternalDirtyFile[], current: ExternalDirtyFile[]): ExternalDirtyFile[];
91
+ export interface ArtifactManifestEntry {
92
+ path: string;
93
+ type: string;
94
+ size: number;
95
+ sha256: string;
96
+ generated_by: string;
97
+ created_at: string;
98
+ }
99
+ export interface ArtifactManifest {
100
+ task_id: string | null;
101
+ generated_at: string;
102
+ artifacts: ArtifactManifestEntry[];
103
+ }
104
+ export declare function buildArtifactManifest(changedFiles: ChangedFile[], repoPath: string, taskId?: string): ArtifactManifest;
105
+ export interface ChangedFileGroups {
106
+ source_changes: ChangedFile[];
107
+ docs_changes: ChangedFile[];
108
+ config_changes: ChangedFile[];
109
+ test_changes: ChangedFile[];
110
+ release_artifacts: ChangedFile[];
111
+ runtime_generated_files: ChangedFile[];
112
+ }
113
+ export declare function groupChangedFiles(changedFiles: ChangedFile[]): ChangedFileGroups;