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,139 @@
1
+ import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { getTasksDir, getPlansDir, getConfig } from "../config.js";
4
+ import { guardPath } from "../security/pathGuard.js";
5
+ import { readTaskRuntime } from "../taskRuntime.js";
6
+ import type { TaskPhase, TaskStatus } from "./createTask.js";
7
+ import {
8
+ derivePendingReason,
9
+ readWatcherStatus,
10
+ type PendingReason,
11
+ type WatcherState,
12
+ type WatcherStatusSnapshot,
13
+ } from "../watcherStatus.js";
14
+
15
+ export interface TaskEntry {
16
+ task_id: string;
17
+ plan_id: string;
18
+ title: string;
19
+ agent: string;
20
+ status: TaskStatus;
21
+ phase: TaskPhase;
22
+ created_at: string;
23
+ updated_at: string;
24
+ workspace_root: string;
25
+ repo_path: string;
26
+ resolved_repo_path: string;
27
+ test_command: string;
28
+ verify_commands: string[];
29
+ error: string | null;
30
+ last_heartbeat_at: string;
31
+ current_command: string | null;
32
+ timeout_seconds: number;
33
+ pending_reason: PendingReason;
34
+ watcher_status: WatcherState;
35
+ }
36
+
37
+ export interface ListTasksInput {
38
+ status?: string;
39
+ repo_path?: string;
40
+ active_only?: boolean;
41
+ limit?: number;
42
+ }
43
+
44
+ export interface ListTasksOutput {
45
+ tasks: TaskEntry[];
46
+ total: number;
47
+ returned: number;
48
+ watcher: WatcherStatusSnapshot;
49
+ }
50
+
51
+ export function listTasks(input?: ListTasksInput): ListTasksOutput {
52
+ const config = getConfig();
53
+ const tasksDir = getTasksDir(config);
54
+ const plansDir = getPlansDir(config);
55
+ const limit = input?.limit && input.limit > 0 ? Math.min(input.limit, 100) : 20;
56
+ const filterStatus = input?.status || null;
57
+ const filterRepo = input?.repo_path?.trim().replace(/\\/g, "/") || null;
58
+ const watcher = readWatcherStatus(config);
59
+
60
+ if (!existsSync(tasksDir)) {
61
+ return { tasks: [], total: 0, returned: 0, watcher };
62
+ }
63
+
64
+ const entries = readdirSync(tasksDir, { withFileTypes: true })
65
+ .filter((e) => e.isDirectory())
66
+ .sort((a, b) => {
67
+ // Sort by mtime descending (newest first)
68
+ try {
69
+ const sa = statSync(join(tasksDir, a.name, "status.json"));
70
+ const sb = statSync(join(tasksDir, b.name, "status.json"));
71
+ return sb.mtimeMs - sa.mtimeMs;
72
+ } catch {
73
+ return b.name.localeCompare(a.name);
74
+ }
75
+ });
76
+
77
+ const tasks: TaskEntry[] = [];
78
+ let totalMatched = 0;
79
+
80
+ for (const entry of entries) {
81
+ const taskId = entry.name;
82
+ const taskDir = join(tasksDir, taskId);
83
+ const statusFile = join(taskDir, "status.json");
84
+
85
+ if (!existsSync(statusFile)) continue;
86
+
87
+ try {
88
+ const data = JSON.parse(readFileSync(statusFile, "utf-8"));
89
+ const runtime = readTaskRuntime(taskDir);
90
+ if (filterStatus && data.status !== filterStatus) continue;
91
+ if (input?.active_only && data.status !== "pending" && data.status !== "running") continue;
92
+ const normalizedRepo = String(data.repo_path || ".").replace(/\\/g, "/");
93
+ const normalizedResolvedRepo = String(data.resolved_repo_path || "").replace(/\\/g, "/");
94
+ if (filterRepo && normalizedRepo !== filterRepo && normalizedResolvedRepo !== filterRepo) continue;
95
+ totalMatched++;
96
+ if (tasks.length >= limit) continue;
97
+
98
+ // Read plan title from plans directory (not task dir)
99
+ let title = `Plan: ${data.plan_id || "unknown"}`;
100
+ if (data.plan_id) {
101
+ const planFile = join(plansDir, data.plan_id, "plan.md");
102
+ if (existsSync(planFile)) {
103
+ try {
104
+ const planContent = readFileSync(planFile, "utf-8");
105
+ const titleMatch = planContent.match(/^#\s*(.+)/m);
106
+ if (titleMatch) title = titleMatch[1];
107
+ } catch { /* keep default */ }
108
+ }
109
+ }
110
+
111
+ const phase = runtime.phase || data.phase || "queued";
112
+ tasks.push({
113
+ task_id: taskId,
114
+ plan_id: data.plan_id || "",
115
+ title,
116
+ agent: data.agent || "",
117
+ status: data.status || "pending",
118
+ phase,
119
+ created_at: data.created_at || "",
120
+ updated_at: data.updated_at || "",
121
+ workspace_root: data.workspace_root || config.workspaceRoot,
122
+ repo_path: data.repo_path || ".",
123
+ resolved_repo_path: data.resolved_repo_path || data.repo_path || config.workspaceRoot,
124
+ test_command: data.test_command || "",
125
+ verify_commands: Array.isArray(data.verify_commands) ? data.verify_commands : [],
126
+ error: data.error || null,
127
+ last_heartbeat_at: runtime.last_heartbeat_at || data.last_heartbeat_at || data.updated_at || "",
128
+ current_command: runtime.current_command ?? data.current_command ?? null,
129
+ timeout_seconds: data.timeout_seconds || config.defaultTaskTimeoutSeconds,
130
+ pending_reason: derivePendingReason({ status: data.status, phase }, watcher),
131
+ watcher_status: watcher.status,
132
+ });
133
+ } catch {
134
+ // skip corrupted entries
135
+ }
136
+ }
137
+
138
+ return { tasks, total: totalMatched, returned: tasks.length, watcher };
139
+ }
@@ -0,0 +1,71 @@
1
+ import { readdirSync, statSync, existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { getConfig } from "../config.js";
4
+ import { guardPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+
7
+ export interface ListEntry {
8
+ name: string;
9
+ type: "file" | "directory";
10
+ size?: number;
11
+ }
12
+
13
+ export interface ListWorkspaceOutput {
14
+ path: string;
15
+ entries: ListEntry[];
16
+ }
17
+
18
+ const MAX_LIST_FILES = 200;
19
+
20
+ export function listWorkspace(relativePath?: string): ListWorkspaceOutput {
21
+ const config = getConfig();
22
+ const targetPath = relativePath
23
+ ? resolve(config.workspaceRoot, relativePath)
24
+ : config.workspaceRoot;
25
+
26
+ const guarded = guardPath(targetPath, config.workspaceRoot);
27
+
28
+ if (!existsSync(guarded)) {
29
+ throw new Error(`Path not found: "${relativePath || "."}"`);
30
+ }
31
+
32
+ const stat = statSync(guarded);
33
+ if (!stat.isDirectory()) {
34
+ throw new Error(`Not a directory: "${relativePath || "."}"`);
35
+ }
36
+
37
+ const entries: ListEntry[] = [];
38
+ const raw = readdirSync(guarded);
39
+
40
+ for (const name of raw) {
41
+ // Skip hidden files and sensitive paths
42
+ const fullPath = resolve(guarded, name);
43
+ try {
44
+ guardSensitivePath(fullPath);
45
+ } catch {
46
+ continue; // skip sensitive files silently
47
+ }
48
+
49
+ try {
50
+ const s = statSync(fullPath);
51
+ entries.push({
52
+ name,
53
+ type: s.isDirectory() ? "directory" : "file",
54
+ size: s.isFile() ? s.size : undefined,
55
+ });
56
+ } catch {
57
+ // can't stat, skip
58
+ }
59
+
60
+ if (entries.length >= MAX_LIST_FILES) break;
61
+ }
62
+
63
+ return {
64
+ path: relativePath || ".",
65
+ entries: entries.sort((a, b) => {
66
+ // directories first, then alphabetically
67
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
68
+ return a.name.localeCompare(b.name);
69
+ }),
70
+ };
71
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { getConfig } from "../config.js";
3
+ import { guardReadPath } from "../security/pathGuard.js";
4
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
5
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
6
+
7
+ export interface ReadWorkspaceFileOutput {
8
+ path: string;
9
+ content: string;
10
+ size: number;
11
+ redacted?: boolean;
12
+ redaction_categories?: string[];
13
+ }
14
+
15
+ export function readWorkspaceFile(relativePath: string): ReadWorkspaceFileOutput {
16
+ const config = getConfig();
17
+
18
+ const safePath = guardReadPath(relativePath, config.workspaceRoot);
19
+ guardSensitivePath(safePath);
20
+
21
+ const stat = statSync(safePath);
22
+ if (stat.size > config.maxReadFileBytes) {
23
+ throw new Error(
24
+ `File is ${stat.size} bytes, exceeds max of ${config.maxReadFileBytes} bytes.`
25
+ );
26
+ }
27
+
28
+ // Only allow text-like files
29
+ if (stat.size > 5_000_000) {
30
+ throw new Error("File exceeds 5 MB size limit.");
31
+ }
32
+
33
+ const redaction = redactSensitiveContent(readFileSync(safePath, "utf-8"));
34
+ return {
35
+ path: safePath,
36
+ content: redaction.content,
37
+ size: stat.size,
38
+ redacted: redaction.redacted,
39
+ redaction_categories: redaction.redaction_categories,
40
+ };
41
+ }