patchwarden 0.4.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 (153) hide show
  1. package/Check-PatchWarden-Health.cmd +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +526 -0
  4. package/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  5. package/Restart-PatchWarden.cmd +19 -0
  6. package/Start-PatchWarden-Tunnel.cmd +7 -0
  7. package/dist/config.d.ts +24 -0
  8. package/dist/config.js +131 -0
  9. package/dist/doctor.d.ts +10 -0
  10. package/dist/doctor.js +388 -0
  11. package/dist/errors.d.ts +16 -0
  12. package/dist/errors.js +27 -0
  13. package/dist/httpServer.d.ts +17 -0
  14. package/dist/httpServer.js +255 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.js +24 -0
  17. package/dist/runner/changeCapture.d.ts +43 -0
  18. package/dist/runner/changeCapture.js +245 -0
  19. package/dist/runner/cli.d.ts +9 -0
  20. package/dist/runner/cli.js +26 -0
  21. package/dist/runner/runTask.d.ts +8 -0
  22. package/dist/runner/runTask.js +821 -0
  23. package/dist/runner/watch.d.ts +19 -0
  24. package/dist/runner/watch.js +136 -0
  25. package/dist/security/commandGuard.d.ts +22 -0
  26. package/dist/security/commandGuard.js +56 -0
  27. package/dist/security/contentRedaction.d.ts +12 -0
  28. package/dist/security/contentRedaction.js +65 -0
  29. package/dist/security/pathGuard.d.ts +3 -0
  30. package/dist/security/pathGuard.js +98 -0
  31. package/dist/security/planGuard.d.ts +1 -0
  32. package/dist/security/planGuard.js +65 -0
  33. package/dist/security/sensitiveGuard.d.ts +2 -0
  34. package/dist/security/sensitiveGuard.js +60 -0
  35. package/dist/smoke-test.d.ts +15 -0
  36. package/dist/smoke-test.js +989 -0
  37. package/dist/taskProgress.d.ts +6 -0
  38. package/dist/taskProgress.js +36 -0
  39. package/dist/taskRuntime.d.ts +10 -0
  40. package/dist/taskRuntime.js +25 -0
  41. package/dist/tools/auditTask.d.ts +19 -0
  42. package/dist/tools/auditTask.js +307 -0
  43. package/dist/tools/cancelTask.d.ts +30 -0
  44. package/dist/tools/cancelTask.js +64 -0
  45. package/dist/tools/createTask.d.ts +42 -0
  46. package/dist/tools/createTask.js +188 -0
  47. package/dist/tools/getPlan.d.ts +12 -0
  48. package/dist/tools/getPlan.js +29 -0
  49. package/dist/tools/getTaskFile.d.ts +24 -0
  50. package/dist/tools/getTaskFile.js +84 -0
  51. package/dist/tools/getTaskProgress.d.ts +1 -0
  52. package/dist/tools/getTaskProgress.js +4 -0
  53. package/dist/tools/getTaskStatus.d.ts +46 -0
  54. package/dist/tools/getTaskStatus.js +36 -0
  55. package/dist/tools/getTaskStdoutTail.d.ts +9 -0
  56. package/dist/tools/getTaskStdoutTail.js +56 -0
  57. package/dist/tools/getTaskSummary.d.ts +66 -0
  58. package/dist/tools/getTaskSummary.js +237 -0
  59. package/dist/tools/healthCheck.d.ts +79 -0
  60. package/dist/tools/healthCheck.js +230 -0
  61. package/dist/tools/killTask.d.ts +15 -0
  62. package/dist/tools/killTask.js +4 -0
  63. package/dist/tools/listAgents.d.ts +12 -0
  64. package/dist/tools/listAgents.js +42 -0
  65. package/dist/tools/listTasks.d.ts +36 -0
  66. package/dist/tools/listTasks.js +94 -0
  67. package/dist/tools/listWorkspace.d.ts +10 -0
  68. package/dist/tools/listWorkspace.js +54 -0
  69. package/dist/tools/readWorkspaceFile.d.ts +8 -0
  70. package/dist/tools/readWorkspaceFile.js +26 -0
  71. package/dist/tools/registry.d.ts +24 -0
  72. package/dist/tools/registry.js +566 -0
  73. package/dist/tools/retryTask.d.ts +8 -0
  74. package/dist/tools/retryTask.js +45 -0
  75. package/dist/tools/savePlan.d.ts +10 -0
  76. package/dist/tools/savePlan.js +28 -0
  77. package/dist/tools/taskOutputs.d.ts +49 -0
  78. package/dist/tools/taskOutputs.js +136 -0
  79. package/dist/tools/taskTemplates.d.ts +15 -0
  80. package/dist/tools/taskTemplates.js +116 -0
  81. package/dist/tools/toolCatalog.d.ts +19 -0
  82. package/dist/tools/toolCatalog.js +71 -0
  83. package/dist/tools/waitForTask.d.ts +25 -0
  84. package/dist/tools/waitForTask.js +59 -0
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.js +2 -0
  87. package/dist/watcherStatus.d.ts +20 -0
  88. package/dist/watcherStatus.js +77 -0
  89. package/docs/assets/patchwarden-chatgpt-demo.svg +30 -0
  90. package/docs/demo.md +58 -0
  91. package/docs/migration-from-safe-bifrost.md +31 -0
  92. package/docs/release-v0.3.0.md +43 -0
  93. package/docs/release-v0.4.0.md +74 -0
  94. package/examples/config.example.json +40 -0
  95. package/examples/openai-tunnel/README.md +48 -0
  96. package/examples/openai-tunnel/chatgpt-test-prompt.md +53 -0
  97. package/examples/openai-tunnel/tunnel-client.example.yaml +39 -0
  98. package/package.json +73 -0
  99. package/scripts/brand-check.js +44 -0
  100. package/scripts/doctor-smoke.js +52 -0
  101. package/scripts/get-patchwarden-health.ps1 +114 -0
  102. package/scripts/http-mcp-smoke.js +351 -0
  103. package/scripts/lifecycle-smoke.js +453 -0
  104. package/scripts/mcp-manifest-check.js +120 -0
  105. package/scripts/mcp-smoke.js +336 -0
  106. package/scripts/pack-clean.js +142 -0
  107. package/scripts/patchwarden-mcp-stdio.cmd +7 -0
  108. package/scripts/restart-patchwarden.ps1 +242 -0
  109. package/scripts/start-patchwarden-tunnel.ps1 +503 -0
  110. package/scripts/tunnel-supervisor-smoke.js +77 -0
  111. package/scripts/watcher-supervisor-smoke.js +195 -0
  112. package/src/config.ts +165 -0
  113. package/src/doctor.ts +438 -0
  114. package/src/errors.ts +26 -0
  115. package/src/httpServer.ts +277 -0
  116. package/src/index.ts +34 -0
  117. package/src/runner/changeCapture.ts +304 -0
  118. package/src/runner/cli.ts +34 -0
  119. package/src/runner/runTask.ts +935 -0
  120. package/src/runner/watch.ts +152 -0
  121. package/src/security/commandGuard.ts +97 -0
  122. package/src/security/contentRedaction.ts +85 -0
  123. package/src/security/pathGuard.ts +175 -0
  124. package/src/security/planGuard.ts +93 -0
  125. package/src/security/sensitiveGuard.ts +72 -0
  126. package/src/smoke-test.ts +1078 -0
  127. package/src/taskProgress.ts +41 -0
  128. package/src/taskRuntime.ts +37 -0
  129. package/src/tools/auditTask.ts +340 -0
  130. package/src/tools/cancelTask.ts +70 -0
  131. package/src/tools/createTask.ts +337 -0
  132. package/src/tools/getPlan.ts +48 -0
  133. package/src/tools/getTaskFile.ts +111 -0
  134. package/src/tools/getTaskProgress.ts +5 -0
  135. package/src/tools/getTaskStatus.ts +85 -0
  136. package/src/tools/getTaskStdoutTail.ts +61 -0
  137. package/src/tools/getTaskSummary.ts +307 -0
  138. package/src/tools/healthCheck.ts +234 -0
  139. package/src/tools/killTask.ts +5 -0
  140. package/src/tools/listAgents.ts +54 -0
  141. package/src/tools/listTasks.ts +139 -0
  142. package/src/tools/listWorkspace.ts +71 -0
  143. package/src/tools/readWorkspaceFile.ts +41 -0
  144. package/src/tools/registry.ts +666 -0
  145. package/src/tools/retryTask.ts +53 -0
  146. package/src/tools/savePlan.ts +46 -0
  147. package/src/tools/taskOutputs.ts +155 -0
  148. package/src/tools/taskTemplates.ts +162 -0
  149. package/src/tools/toolCatalog.ts +92 -0
  150. package/src/tools/waitForTask.ts +90 -0
  151. package/src/version.ts +2 -0
  152. package/src/watcherStatus.ts +102 -0
  153. package/tsconfig.json +17 -0
@@ -0,0 +1,49 @@
1
+ import { GetTaskFileOutput } from "./getTaskFile.js";
2
+ export declare function getResult(taskId: string): GetTaskFileOutput;
3
+ export declare function getResultJson(taskId: string): GetTaskFileOutput;
4
+ export declare function getDiff(taskId: string): GetTaskFileOutput;
5
+ export declare function getTestLog(taskId: string, options?: {
6
+ tailLines?: number;
7
+ maxBytes?: number;
8
+ }): GetTaskFileOutput;
9
+ /**
10
+ * Read the tail of a task log file with automatic redaction.
11
+ * Supports test.log, stdout.log, stderr.log, verify.log.
12
+ */
13
+ export declare function getTaskLogTail(taskId: string, file: "stdout" | "stderr" | "test" | "verify", options?: {
14
+ lines?: number;
15
+ redact?: boolean;
16
+ }): {
17
+ file: "stdout" | "stderr" | "test" | "verify";
18
+ lines: number;
19
+ total_bytes: number;
20
+ task_id: string;
21
+ filename: string;
22
+ content: string;
23
+ path: string;
24
+ available: boolean;
25
+ reason?: "task_not_terminal" | "artifact_missing";
26
+ task_status?: string;
27
+ phase?: string;
28
+ pending_reason?: string | null;
29
+ watcher?: unknown;
30
+ next_action?: string;
31
+ next_tool_call?: {
32
+ name: string;
33
+ arguments: Record<string, unknown>;
34
+ };
35
+ redacted?: boolean;
36
+ redaction_categories?: string[];
37
+ truncated?: undefined;
38
+ } | {
39
+ task_id: string;
40
+ file: "stdout" | "stderr" | "test" | "verify";
41
+ filename: string;
42
+ content: string;
43
+ available: boolean;
44
+ lines: number;
45
+ total_bytes: number;
46
+ truncated: boolean;
47
+ redacted: boolean;
48
+ redaction_categories: string[];
49
+ };
@@ -0,0 +1,136 @@
1
+ import { getTaskFile } from "./getTaskFile.js";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { getConfig, getTasksDir } from "../config.js";
5
+ import { guardPath } from "../security/pathGuard.js";
6
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
7
+ export function getResult(taskId) {
8
+ return getTaskFile(taskId, "result.md");
9
+ }
10
+ export function getResultJson(taskId) {
11
+ return getTaskFile(taskId, "result.json");
12
+ }
13
+ export function getDiff(taskId) {
14
+ const config = getConfig();
15
+ const taskDir = resolve(getTasksDir(config), taskId);
16
+ const preferred = join(taskDir, "diff.patch");
17
+ const fallback = join(taskDir, "git.diff");
18
+ if (!existsSync(preferred) && !existsSync(fallback)) {
19
+ return getTaskFile(taskId, "diff.patch");
20
+ }
21
+ const filePath = existsSync(preferred) ? preferred : fallback;
22
+ guardPath(filePath, config.workspaceRoot, config.tasksDir);
23
+ const size = statSync(filePath).size;
24
+ const raw = readFileSync(filePath, "utf-8");
25
+ const patchHead = raw.slice(0, config.maxReadFileBytes);
26
+ const rawReturnedBytes = Buffer.byteLength(patchHead, "utf-8");
27
+ const redaction = redactSensitiveContent(patchHead);
28
+ const content = redaction.content;
29
+ const diff = {
30
+ task_id: taskId,
31
+ filename: existsSync(preferred) ? "diff.patch" : "git.diff",
32
+ content,
33
+ path: filePath,
34
+ available: true,
35
+ truncated: size > rawReturnedBytes,
36
+ total_bytes: size,
37
+ returned_bytes: Buffer.byteLength(content, "utf-8"),
38
+ patch_head: content,
39
+ diff_patch_path: filePath,
40
+ redacted: redaction.redacted,
41
+ redaction_categories: redaction.redaction_categories,
42
+ };
43
+ try {
44
+ const evidence = JSON.parse(getTaskFile(taskId, "changed-files.json").content);
45
+ return {
46
+ ...diff,
47
+ changed_files: evidence.changed_files || [],
48
+ diff_available: Boolean(evidence.diff_available),
49
+ workspace_dirty_before: Boolean(evidence.workspace_dirty_before),
50
+ workspace_dirty_after: Boolean(evidence.workspace_dirty_after),
51
+ diff_truncated: Boolean(evidence.diff_truncated || diff.truncated),
52
+ diff_size_bytes: Number(evidence.diff_size_bytes || size),
53
+ additions: Number(evidence.additions || 0),
54
+ deletions: Number(evidence.deletions || 0),
55
+ file_stats: evidence.file_stats || [],
56
+ patch_mode: evidence.patch_mode || (evidence.diff_available ? "hash_only" : "no_changes"),
57
+ unavailable_reason: evidence.unavailable_reason || null,
58
+ message: evidence.diff_available ? "Task diff available" : "No task file changes detected",
59
+ };
60
+ }
61
+ catch {
62
+ return diff;
63
+ }
64
+ }
65
+ export function getTestLog(taskId, options) {
66
+ const maxBytes = options?.maxBytes ?? 0;
67
+ const tailLines = options?.tailLines ?? 0;
68
+ if (tailLines > 0 || maxBytes > 0) {
69
+ return getTaskFileTail(taskId, "test.log", { tailLines, maxBytes });
70
+ }
71
+ return getTaskFile(taskId, "test.log");
72
+ }
73
+ /**
74
+ * Read the tail of a task log file with automatic redaction.
75
+ * Supports test.log, stdout.log, stderr.log, verify.log.
76
+ */
77
+ export function getTaskLogTail(taskId, file, options) {
78
+ const config = getConfig();
79
+ const taskDir = resolve(getTasksDir(config), taskId);
80
+ const filename = file === "stdout" ? "stdout.log"
81
+ : file === "stderr" ? "stderr.log"
82
+ : file === "test" ? "test.log"
83
+ : "verify.log";
84
+ const filePath = join(taskDir, filename);
85
+ guardPath(filePath, config.workspaceRoot, config.tasksDir);
86
+ const maxLines = Math.min(options?.lines ?? 80, 200);
87
+ const applyRedact = options?.redact !== false; // default true
88
+ if (!existsSync(filePath)) {
89
+ return { ...getTaskFile(taskId, filename), file, lines: 0, total_bytes: 0 };
90
+ }
91
+ const stat = statSync(filePath);
92
+ const totalBytes = stat.size;
93
+ const raw = readFileSync(filePath, "utf-8");
94
+ const tail = raw.split("\n").slice(-maxLines).join("\n");
95
+ const content = applyRedact ? redactSensitiveContent(tail) : { content: tail, redacted: false, redaction_categories: [] };
96
+ return {
97
+ task_id: taskId,
98
+ file,
99
+ filename,
100
+ content: content.content,
101
+ available: true,
102
+ lines: tail.split("\n").length,
103
+ total_bytes: totalBytes,
104
+ truncated: totalBytes > Buffer.byteLength(tail, "utf-8"),
105
+ redacted: content.redacted,
106
+ redaction_categories: content.redaction_categories,
107
+ };
108
+ }
109
+ function getTaskFileTail(taskId, filename, options) {
110
+ const config = getConfig();
111
+ const taskDir = resolve(getTasksDir(config), taskId);
112
+ const filePath = join(taskDir, filename);
113
+ guardPath(filePath, config.workspaceRoot, config.tasksDir);
114
+ if (!existsSync(filePath)) {
115
+ return getTaskFile(taskId, filename);
116
+ }
117
+ const stat = statSync(filePath);
118
+ const raw = readFileSync(filePath, "utf-8");
119
+ let content = raw;
120
+ if (options.tailLines > 0) {
121
+ content = raw.split("\n").slice(-options.tailLines).join("\n");
122
+ }
123
+ if (options.maxBytes > 0 && Buffer.byteLength(content, "utf-8") > options.maxBytes) {
124
+ content = content.slice(0, options.maxBytes);
125
+ }
126
+ const redaction = redactSensitiveContent(content);
127
+ return {
128
+ task_id: taskId,
129
+ filename,
130
+ content: redaction.content,
131
+ path: filePath,
132
+ available: true,
133
+ redacted: redaction.redacted,
134
+ redaction_categories: redaction.redaction_categories,
135
+ };
136
+ }
@@ -0,0 +1,15 @@
1
+ export declare const TASK_TEMPLATE_NAMES: readonly ["inspect_only", "feature_small", "fix_tests", "release_check", "rollback_scope_violation"];
2
+ export type TaskTemplateName = typeof TASK_TEMPLATE_NAMES[number];
3
+ export type ChangePolicy = "repo_scoped_changes" | "no_changes";
4
+ export interface TaskTemplateInput {
5
+ template: TaskTemplateName;
6
+ goal: string;
7
+ source_task_id?: string;
8
+ verify_commands: string[];
9
+ }
10
+ export interface ExpandedTaskTemplate {
11
+ title: string;
12
+ content: string;
13
+ change_policy: ChangePolicy;
14
+ }
15
+ export declare function expandTaskTemplate(input: TaskTemplateInput): ExpandedTaskTemplate;
@@ -0,0 +1,116 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { getConfig, getTasksDir } from "../config.js";
4
+ import { PatchWardenError } from "../errors.js";
5
+ import { guardReadPath } from "../security/pathGuard.js";
6
+ export const TASK_TEMPLATE_NAMES = [
7
+ "inspect_only",
8
+ "feature_small",
9
+ "fix_tests",
10
+ "release_check",
11
+ "rollback_scope_violation",
12
+ ];
13
+ export function expandTaskTemplate(input) {
14
+ const goal = input.goal.trim();
15
+ if (!goal) {
16
+ throw new PatchWardenError("template_goal_required", "Template tasks require a non-empty goal.", "Pass a concise goal describing the desired inspection or repository-local change.");
17
+ }
18
+ const common = [
19
+ `## Goal\n${goal}`,
20
+ "## Safety boundaries",
21
+ "- Work only inside the resolved repository path.",
22
+ "- Do not commit, push, publish, expose credentials, or modify unrelated files.",
23
+ "- Report exact files inspected or changed and any remaining uncertainty.",
24
+ ];
25
+ switch (input.template) {
26
+ case "inspect_only":
27
+ return {
28
+ title: `Inspect only: ${shortTitle(goal)}`,
29
+ change_policy: "no_changes",
30
+ content: [
31
+ ...common,
32
+ "## Execution contract",
33
+ "- Perform read-only inspection only.",
34
+ "- Do not create, edit, delete, rename, format, or generate repository files.",
35
+ "- Return findings, evidence, and the smallest safe next action.",
36
+ ].join("\n\n"),
37
+ };
38
+ case "feature_small":
39
+ return {
40
+ title: `Small feature: ${shortTitle(goal)}`,
41
+ change_policy: "repo_scoped_changes",
42
+ content: [
43
+ ...common,
44
+ "## Execution contract",
45
+ "- Read the repository instructions, README, and package metadata first.",
46
+ "- Make the smallest coherent implementation; avoid framework, dependency, and directory changes unless required.",
47
+ "- Run only the independently configured verification commands.",
48
+ ].join("\n\n"),
49
+ };
50
+ case "fix_tests":
51
+ if (input.verify_commands.length === 0) {
52
+ throw new PatchWardenError("template_verification_required", "The fix_tests template requires at least one allow-listed verify_commands entry.", "Pass the failing test or check command in verify_commands.");
53
+ }
54
+ return {
55
+ title: `Fix tests: ${shortTitle(goal)}`,
56
+ change_policy: "repo_scoped_changes",
57
+ content: [
58
+ ...common,
59
+ "## Execution contract",
60
+ "- Reproduce the relevant failure before editing when possible.",
61
+ "- Fix the root cause without deleting tests, weakening checks, or changing unrelated behavior.",
62
+ `- PatchWarden will independently run: ${input.verify_commands.join(", ")}.`,
63
+ ].join("\n\n"),
64
+ };
65
+ case "release_check":
66
+ return {
67
+ title: `Release check: ${shortTitle(goal)}`,
68
+ change_policy: "repo_scoped_changes",
69
+ content: [
70
+ ...common,
71
+ "## Execution contract",
72
+ "- Perform local release-readiness checks and inspect package contents and generated artifacts.",
73
+ "- Do not publish, push, create tags/releases, or claim remote completion without live remote evidence.",
74
+ "- Clearly separate local readiness from remote publication state.",
75
+ ].join("\n\n"),
76
+ };
77
+ case "rollback_scope_violation": {
78
+ const evidence = readRollbackEvidence(input.source_task_id);
79
+ return {
80
+ title: `Scope violation review: ${shortTitle(goal)}`,
81
+ change_policy: "no_changes",
82
+ content: [
83
+ ...common,
84
+ "## Execution contract",
85
+ `- Review scope-violation evidence from source task ${input.source_task_id}.`,
86
+ "- Do not perform an automatic rollback, deletion, reset, checkout, or cross-repository edit.",
87
+ "- Produce a file-by-file recovery proposal with backup and verification steps for user approval.",
88
+ "## Source task recovery evidence",
89
+ evidence,
90
+ ].join("\n\n"),
91
+ };
92
+ }
93
+ }
94
+ }
95
+ function readRollbackEvidence(sourceTaskId) {
96
+ if (!sourceTaskId?.trim()) {
97
+ throw new PatchWardenError("source_task_required", "rollback_scope_violation requires source_task_id.", "Pass the task ID that ended with failed_scope_violation.");
98
+ }
99
+ const config = getConfig();
100
+ const taskDir = resolve(getTasksDir(config), sourceTaskId);
101
+ const statusFile = join(taskDir, "status.json");
102
+ const rollbackFile = join(taskDir, "rollback_scope_violation_plan.md");
103
+ guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
104
+ guardReadPath(rollbackFile, config.workspaceRoot, config.tasksDir);
105
+ if (!existsSync(statusFile) || !existsSync(rollbackFile)) {
106
+ throw new PatchWardenError("scope_violation_evidence_missing", `Source task "${sourceTaskId}" does not contain scope-violation recovery evidence.`, "Use a task that ended with failed_scope_violation and has rollback_scope_violation_plan.md.");
107
+ }
108
+ const status = JSON.parse(readFileSync(statusFile, "utf-8"));
109
+ if (status.status !== "failed_scope_violation") {
110
+ throw new PatchWardenError("source_task_not_scope_violation", `Source task "${sourceTaskId}" has status "${status.status}", not failed_scope_violation.`, "Use the scope-violating task as source_task_id.");
111
+ }
112
+ return readFileSync(rollbackFile, "utf-8").slice(0, 20_000);
113
+ }
114
+ function shortTitle(goal) {
115
+ return goal.replace(/\s+/g, " ").slice(0, 80);
116
+ }
@@ -0,0 +1,19 @@
1
+ export type ToolProfile = "full" | "chatgpt_core";
2
+ export interface CatalogTool {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: unknown;
6
+ }
7
+ export interface ToolCatalogSnapshot {
8
+ server_version: string;
9
+ schema_epoch: string;
10
+ tool_profile: ToolProfile;
11
+ tool_count: number;
12
+ tool_names: string[];
13
+ tool_manifest_sha256: string;
14
+ }
15
+ export declare const CHATGPT_CORE_TOOL_NAMES: readonly ["health_check", "list_agents", "list_workspace", "read_workspace_file", "save_plan", "create_task", "wait_for_task", "get_task_summary", "get_diff", "get_result", "get_result_json", "get_test_log", "get_task_status", "list_tasks", "cancel_task", "audit_task"];
16
+ export declare function resolveToolProfile(configProfile?: string): ToolProfile;
17
+ export declare function selectToolsForProfile<T extends CatalogTool>(tools: T[], profile: ToolProfile): T[];
18
+ export declare function buildToolCatalogSnapshot(tools: CatalogTool[], profile: ToolProfile): ToolCatalogSnapshot;
19
+ export declare function getLastToolCatalogSnapshot(): ToolCatalogSnapshot | null;
@@ -0,0 +1,71 @@
1
+ import { createHash } from "node:crypto";
2
+ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
3
+ export const CHATGPT_CORE_TOOL_NAMES = [
4
+ "health_check",
5
+ "list_agents",
6
+ "list_workspace",
7
+ "read_workspace_file",
8
+ "save_plan",
9
+ "create_task",
10
+ "wait_for_task",
11
+ "get_task_summary",
12
+ "get_diff",
13
+ "get_result",
14
+ "get_result_json",
15
+ "get_test_log",
16
+ "get_task_status",
17
+ "list_tasks",
18
+ "cancel_task",
19
+ "audit_task",
20
+ ];
21
+ let lastSnapshot = null;
22
+ export function resolveToolProfile(configProfile) {
23
+ const raw = (process.env.PATCHWARDEN_TOOL_PROFILE || configProfile || "full").trim();
24
+ if (raw !== "full" && raw !== "chatgpt_core") {
25
+ throw new Error(`Invalid tool profile "${raw}". Expected "full" or "chatgpt_core".`);
26
+ }
27
+ return raw;
28
+ }
29
+ export function selectToolsForProfile(tools, profile) {
30
+ if (profile === "full")
31
+ return tools;
32
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
33
+ return CHATGPT_CORE_TOOL_NAMES.map((name) => {
34
+ const tool = byName.get(name);
35
+ if (!tool)
36
+ throw new Error(`chatgpt_core tool profile requires missing tool "${name}".`);
37
+ return tool;
38
+ });
39
+ }
40
+ export function buildToolCatalogSnapshot(tools, profile) {
41
+ const manifestInput = tools.map((tool) => ({
42
+ name: tool.name,
43
+ description: tool.description,
44
+ inputSchema: tool.inputSchema,
45
+ }));
46
+ const tool_manifest_sha256 = createHash("sha256")
47
+ .update(stableJson(manifestInput))
48
+ .digest("hex");
49
+ lastSnapshot = {
50
+ server_version: PATCHWARDEN_VERSION,
51
+ schema_epoch: TOOL_SCHEMA_EPOCH,
52
+ tool_profile: profile,
53
+ tool_count: tools.length,
54
+ tool_names: tools.map((tool) => tool.name),
55
+ tool_manifest_sha256,
56
+ };
57
+ return lastSnapshot;
58
+ }
59
+ export function getLastToolCatalogSnapshot() {
60
+ return lastSnapshot ? { ...lastSnapshot, tool_names: [...lastSnapshot.tool_names] } : null;
61
+ }
62
+ function stableJson(value) {
63
+ if (Array.isArray(value))
64
+ return `[${value.map(stableJson).join(",")}]`;
65
+ if (value && typeof value === "object") {
66
+ const entries = Object.entries(value)
67
+ .sort(([left], [right]) => left.localeCompare(right));
68
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
69
+ }
70
+ return JSON.stringify(value);
71
+ }
@@ -0,0 +1,25 @@
1
+ import { type TaskSummaryOutput } from "./getTaskSummary.js";
2
+ export interface WaitForTaskProgressSummary {
3
+ phase: string;
4
+ heartbeat_at: string;
5
+ elapsed_seconds: number;
6
+ current_command: string | null;
7
+ hint: string;
8
+ }
9
+ export interface WaitForTaskOutput {
10
+ task_id: string;
11
+ status: string;
12
+ phase: string;
13
+ terminal: boolean;
14
+ timed_out: boolean;
15
+ continuation_required: boolean;
16
+ waited_ms: number;
17
+ next_action: string;
18
+ next_tool_call: {
19
+ name: "wait_for_task" | "audit_task" | "health_check";
20
+ arguments: Record<string, unknown>;
21
+ };
22
+ summary?: TaskSummaryOutput;
23
+ progress_summary?: WaitForTaskProgressSummary;
24
+ }
25
+ export declare function waitForTask(taskId: string, waitSeconds?: number): Promise<WaitForTaskOutput>;
@@ -0,0 +1,59 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { getTaskStatus } from "./getTaskStatus.js";
3
+ import { getTaskSummary } from "./getTaskSummary.js";
4
+ const TERMINAL_STATUSES = new Set([
5
+ "done",
6
+ "failed",
7
+ "failed_verification",
8
+ "failed_scope_violation",
9
+ "failed_policy_violation",
10
+ "canceled",
11
+ ]);
12
+ export async function waitForTask(taskId, waitSeconds = 25) {
13
+ if (!Number.isInteger(waitSeconds) || waitSeconds < 1 || waitSeconds > 30) {
14
+ throw new Error("wait_seconds must be an integer from 1 to 30.");
15
+ }
16
+ const started = Date.now();
17
+ const deadline = started + waitSeconds * 1000;
18
+ let status = getTaskStatus(taskId);
19
+ while (!TERMINAL_STATUSES.has(status.status) && Date.now() < deadline) {
20
+ await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
21
+ status = getTaskStatus(taskId);
22
+ }
23
+ const terminal = TERMINAL_STATUSES.has(status.status);
24
+ const executionBlocked = !terminal && status.execution_blocked;
25
+ const elapsed = Math.round((Date.now() - started) / 1000);
26
+ const progressSummary = {
27
+ phase: status.phase,
28
+ heartbeat_at: status.last_heartbeat_at || status.updated_at || "",
29
+ elapsed_seconds: elapsed,
30
+ current_command: status.current_command || null,
31
+ hint: executionBlocked
32
+ ? `Watcher is ${status.watcher_status}. Call health_check and restart the owned watcher; do not keep polling this task yet.`
33
+ : status.phase === "queued"
34
+ ? "Watcher is healthy and has not picked up this task yet. Continue waiting."
35
+ : status.phase === "executing_agent"
36
+ ? `Agent "${status.agent}" is running. Continue waiting, or call get_task_status for phase and current_command.`
37
+ : `Phase "${status.phase}". Task is still in progress.`,
38
+ };
39
+ return {
40
+ task_id: taskId,
41
+ status: status.status,
42
+ phase: status.phase,
43
+ terminal,
44
+ timed_out: !terminal,
45
+ continuation_required: !terminal && !executionBlocked,
46
+ waited_ms: Date.now() - started,
47
+ next_action: terminal
48
+ ? "Review the returned summary, then call audit_task for independent acceptance evidence."
49
+ : executionBlocked
50
+ ? "Call health_check and restore the owned watcher. The saved task will remain queued for recovery."
51
+ : `Call wait_for_task again immediately for task_id ${taskId}; do not end the assistant turn while continuation_required is true.`,
52
+ next_tool_call: terminal
53
+ ? { name: "audit_task", arguments: { task_id: taskId } }
54
+ : executionBlocked
55
+ ? { name: "health_check", arguments: {} }
56
+ : { name: "wait_for_task", arguments: { task_id: taskId, timeout_seconds: waitSeconds } },
57
+ ...(terminal ? { summary: getTaskSummary(taskId) } : { progress_summary: progressSummary }),
58
+ };
59
+ }
@@ -0,0 +1,2 @@
1
+ export declare const PATCHWARDEN_VERSION = "0.4.0";
2
+ export declare const TOOL_SCHEMA_EPOCH = "2026-06-21-v3";
@@ -0,0 +1,2 @@
1
+ export const PATCHWARDEN_VERSION = "0.4.0";
2
+ export const TOOL_SCHEMA_EPOCH = "2026-06-21-v3";
@@ -0,0 +1,20 @@
1
+ import { type PatchWardenConfig } from "./config.js";
2
+ export type WatcherState = "healthy" | "stale" | "missing" | "unreadable";
3
+ export type PendingReason = "queued_waiting_for_watcher" | "queued_but_watcher_stale" | "queued_but_watcher_missing" | "queued_but_watcher_unreadable" | "agent_running" | "verification_running" | "preparing" | "collecting_artifacts" | null;
4
+ export interface WatcherStatusSnapshot {
5
+ status: WatcherState;
6
+ available: boolean;
7
+ stale_after_seconds: number;
8
+ last_heartbeat_at: string | null;
9
+ heartbeat_age_seconds: number | null;
10
+ heartbeat_pid: number | null;
11
+ instance_id: string | null;
12
+ launcher_pid: number | null;
13
+ reason: string | null;
14
+ }
15
+ export declare function getWatcherHeartbeatPath(config?: PatchWardenConfig): string;
16
+ export declare function readWatcherStatus(config?: PatchWardenConfig, nowMs?: number): WatcherStatusSnapshot;
17
+ export declare function derivePendingReason(task: {
18
+ status?: string;
19
+ phase?: string;
20
+ }, watcher: WatcherStatusSnapshot): PendingReason;
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getConfig, getTasksDir } from "./config.js";
4
+ export function getWatcherHeartbeatPath(config = getConfig()) {
5
+ return join(dirname(getTasksDir(config)), "watcher-heartbeat.json");
6
+ }
7
+ export function readWatcherStatus(config = getConfig(), nowMs = Date.now()) {
8
+ const staleAfterSeconds = config.watcherStaleSeconds;
9
+ const heartbeatPath = getWatcherHeartbeatPath(config);
10
+ if (!existsSync(heartbeatPath)) {
11
+ return {
12
+ status: "missing",
13
+ available: false,
14
+ stale_after_seconds: staleAfterSeconds,
15
+ last_heartbeat_at: null,
16
+ heartbeat_age_seconds: null,
17
+ heartbeat_pid: null,
18
+ instance_id: null,
19
+ launcher_pid: null,
20
+ reason: "Watcher heartbeat has not been observed. Start or restart the PatchWarden watcher.",
21
+ };
22
+ }
23
+ try {
24
+ const raw = readFileSync(heartbeatPath, "utf-8");
25
+ const data = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
26
+ const heartbeatMs = Date.parse(String(data.last_heartbeat_at || ""));
27
+ if (!Number.isFinite(heartbeatMs))
28
+ throw new Error("invalid heartbeat timestamp");
29
+ const ageMs = Math.max(0, nowMs - heartbeatMs);
30
+ const ageSeconds = Math.round(ageMs / 1000);
31
+ const healthy = ageMs < staleAfterSeconds * 1000;
32
+ return {
33
+ status: healthy ? "healthy" : "stale",
34
+ available: healthy,
35
+ stale_after_seconds: staleAfterSeconds,
36
+ last_heartbeat_at: String(data.last_heartbeat_at),
37
+ heartbeat_age_seconds: ageSeconds,
38
+ heartbeat_pid: Number.isInteger(Number(data.pid)) ? Number(data.pid) : null,
39
+ instance_id: typeof data.instance_id === "string" ? data.instance_id : null,
40
+ launcher_pid: Number.isInteger(Number(data.launcher_pid)) ? Number(data.launcher_pid) : null,
41
+ reason: healthy ? null : "Watcher heartbeat is stale. Restart the PatchWarden watcher.",
42
+ };
43
+ }
44
+ catch {
45
+ return {
46
+ status: "unreadable",
47
+ available: false,
48
+ stale_after_seconds: staleAfterSeconds,
49
+ last_heartbeat_at: null,
50
+ heartbeat_age_seconds: null,
51
+ heartbeat_pid: null,
52
+ instance_id: null,
53
+ launcher_pid: null,
54
+ reason: "Watcher heartbeat file is unreadable.",
55
+ };
56
+ }
57
+ }
58
+ export function derivePendingReason(task, watcher) {
59
+ if (task.status === "pending") {
60
+ if (watcher.status === "stale")
61
+ return "queued_but_watcher_stale";
62
+ if (watcher.status === "missing")
63
+ return "queued_but_watcher_missing";
64
+ if (watcher.status === "unreadable")
65
+ return "queued_but_watcher_unreadable";
66
+ return "queued_waiting_for_watcher";
67
+ }
68
+ if (task.status !== "running")
69
+ return null;
70
+ if (task.phase === "executing_agent")
71
+ return "agent_running";
72
+ if (task.phase === "running_tests")
73
+ return "verification_running";
74
+ if (task.phase === "collecting_artifacts")
75
+ return "collecting_artifacts";
76
+ return "preparing";
77
+ }
@@ -0,0 +1,30 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
2
+ <title id="title">PatchWarden ChatGPT connector demo</title>
3
+ <desc id="desc">A sanitized demo showing ChatGPT calling PatchWarden list_workspace and receiving a workspace file list.</desc>
4
+ <rect width="1280" height="720" fill="#f7f7f5"/>
5
+ <rect x="40" y="38" width="260" height="644" rx="16" fill="#ffffff" stroke="#deded8"/>
6
+ <text x="72" y="88" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="28" font-weight="700" fill="#1f1f1f">ChatGPT</text>
7
+ <rect x="72" y="128" width="188" height="44" rx="10" fill="#eeeeea"/>
8
+ <text x="96" y="157" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" fill="#333">patchwarden</text>
9
+ <rect x="340" y="48" width="900" height="624" rx="22" fill="#ffffff" stroke="#e2e2dc"/>
10
+ <text x="380" y="96" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="22" font-weight="700" fill="#202020">PatchWarden Connector</text>
11
+ <rect x="672" y="126" width="500" height="88" rx="26" fill="#f0f0ec"/>
12
+ <text x="704" y="158" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="20" fill="#202020">Use patchwarden.list_workspace</text>
13
+ <text x="704" y="188" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="20" fill="#202020">to list the configured workspace.</text>
14
+ <rect x="410" y="270" width="620" height="274" rx="18" fill="#fafafa" stroke="#d9d9d2"/>
15
+ <text x="448" y="318" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="24" font-weight="700" fill="#1f1f1f">Workspace files</text>
16
+ <line x1="448" y1="348" x2="990" y2="348" stroke="#ddddda"/>
17
+ <text x="448" y="388" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" font-weight="700" fill="#2a2a2a">Name</text>
18
+ <text x="720" y="388" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" font-weight="700" fill="#2a2a2a">Type</text>
19
+ <text x="850" y="388" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" font-weight="700" fill="#2a2a2a">Size</text>
20
+ <rect x="448" y="414" width="178" height="34" rx="8" fill="#ececea"/>
21
+ <text x="462" y="437" font-family="JetBrains Mono,Consolas,monospace" font-size="17" fill="#222">README.md</text>
22
+ <text x="720" y="437" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" fill="#222">file</text>
23
+ <text x="850" y="437" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" fill="#222">1.2 KB</text>
24
+ <rect x="448" y="464" width="164" height="34" rx="8" fill="#ececea"/>
25
+ <text x="462" y="487" font-family="JetBrains Mono,Consolas,monospace" font-size="17" fill="#222">package.json</text>
26
+ <text x="720" y="487" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" fill="#222">file</text>
27
+ <text x="850" y="487" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" fill="#222">320 B</text>
28
+ <rect x="410" y="584" width="620" height="52" rx="16" fill="#f3f8f5" stroke="#b9dfc7"/>
29
+ <text x="440" y="617" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="18" fill="#176b38">ChatGPT can plan; PatchWarden stores tasks; local agents execute safely.</text>
30
+ </svg>