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.
- package/Check-PatchWarden-Health.cmd +6 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/Restart-PatchWarden.cmd +19 -0
- package/Start-PatchWarden-Tunnel.cmd +7 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +131 -0
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +388 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +27 -0
- package/dist/httpServer.d.ts +17 -0
- package/dist/httpServer.js +255 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +24 -0
- package/dist/runner/changeCapture.d.ts +43 -0
- package/dist/runner/changeCapture.js +245 -0
- package/dist/runner/cli.d.ts +9 -0
- package/dist/runner/cli.js +26 -0
- package/dist/runner/runTask.d.ts +8 -0
- package/dist/runner/runTask.js +821 -0
- package/dist/runner/watch.d.ts +19 -0
- package/dist/runner/watch.js +136 -0
- package/dist/security/commandGuard.d.ts +22 -0
- package/dist/security/commandGuard.js +56 -0
- package/dist/security/contentRedaction.d.ts +12 -0
- package/dist/security/contentRedaction.js +65 -0
- package/dist/security/pathGuard.d.ts +3 -0
- package/dist/security/pathGuard.js +98 -0
- package/dist/security/planGuard.d.ts +1 -0
- package/dist/security/planGuard.js +65 -0
- package/dist/security/sensitiveGuard.d.ts +2 -0
- package/dist/security/sensitiveGuard.js +60 -0
- package/dist/smoke-test.d.ts +15 -0
- package/dist/smoke-test.js +989 -0
- package/dist/taskProgress.d.ts +6 -0
- package/dist/taskProgress.js +36 -0
- package/dist/taskRuntime.d.ts +10 -0
- package/dist/taskRuntime.js +25 -0
- package/dist/tools/auditTask.d.ts +19 -0
- package/dist/tools/auditTask.js +307 -0
- package/dist/tools/cancelTask.d.ts +30 -0
- package/dist/tools/cancelTask.js +64 -0
- package/dist/tools/createTask.d.ts +42 -0
- package/dist/tools/createTask.js +188 -0
- package/dist/tools/getPlan.d.ts +12 -0
- package/dist/tools/getPlan.js +29 -0
- package/dist/tools/getTaskFile.d.ts +24 -0
- package/dist/tools/getTaskFile.js +84 -0
- package/dist/tools/getTaskProgress.d.ts +1 -0
- package/dist/tools/getTaskProgress.js +4 -0
- package/dist/tools/getTaskStatus.d.ts +46 -0
- package/dist/tools/getTaskStatus.js +36 -0
- package/dist/tools/getTaskStdoutTail.d.ts +9 -0
- package/dist/tools/getTaskStdoutTail.js +56 -0
- package/dist/tools/getTaskSummary.d.ts +66 -0
- package/dist/tools/getTaskSummary.js +237 -0
- package/dist/tools/healthCheck.d.ts +79 -0
- package/dist/tools/healthCheck.js +230 -0
- package/dist/tools/killTask.d.ts +15 -0
- package/dist/tools/killTask.js +4 -0
- package/dist/tools/listAgents.d.ts +12 -0
- package/dist/tools/listAgents.js +42 -0
- package/dist/tools/listTasks.d.ts +36 -0
- package/dist/tools/listTasks.js +94 -0
- package/dist/tools/listWorkspace.d.ts +10 -0
- package/dist/tools/listWorkspace.js +54 -0
- package/dist/tools/readWorkspaceFile.d.ts +8 -0
- package/dist/tools/readWorkspaceFile.js +26 -0
- package/dist/tools/registry.d.ts +24 -0
- package/dist/tools/registry.js +566 -0
- package/dist/tools/retryTask.d.ts +8 -0
- package/dist/tools/retryTask.js +45 -0
- package/dist/tools/savePlan.d.ts +10 -0
- package/dist/tools/savePlan.js +28 -0
- package/dist/tools/taskOutputs.d.ts +49 -0
- package/dist/tools/taskOutputs.js +136 -0
- package/dist/tools/taskTemplates.d.ts +15 -0
- package/dist/tools/taskTemplates.js +116 -0
- package/dist/tools/toolCatalog.d.ts +19 -0
- package/dist/tools/toolCatalog.js +71 -0
- package/dist/tools/waitForTask.d.ts +25 -0
- package/dist/tools/waitForTask.js +59 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/watcherStatus.d.ts +20 -0
- package/dist/watcherStatus.js +77 -0
- package/docs/assets/patchwarden-chatgpt-demo.svg +30 -0
- package/docs/demo.md +58 -0
- package/docs/migration-from-safe-bifrost.md +31 -0
- package/docs/release-v0.3.0.md +43 -0
- package/docs/release-v0.4.0.md +74 -0
- package/examples/config.example.json +40 -0
- package/examples/openai-tunnel/README.md +48 -0
- package/examples/openai-tunnel/chatgpt-test-prompt.md +53 -0
- package/examples/openai-tunnel/tunnel-client.example.yaml +39 -0
- package/package.json +73 -0
- package/scripts/brand-check.js +44 -0
- package/scripts/doctor-smoke.js +52 -0
- package/scripts/get-patchwarden-health.ps1 +114 -0
- package/scripts/http-mcp-smoke.js +351 -0
- package/scripts/lifecycle-smoke.js +453 -0
- package/scripts/mcp-manifest-check.js +120 -0
- package/scripts/mcp-smoke.js +336 -0
- package/scripts/pack-clean.js +142 -0
- package/scripts/patchwarden-mcp-stdio.cmd +7 -0
- package/scripts/restart-patchwarden.ps1 +242 -0
- package/scripts/start-patchwarden-tunnel.ps1 +503 -0
- package/scripts/tunnel-supervisor-smoke.js +77 -0
- package/scripts/watcher-supervisor-smoke.js +195 -0
- package/src/config.ts +165 -0
- package/src/doctor.ts +438 -0
- package/src/errors.ts +26 -0
- package/src/httpServer.ts +277 -0
- package/src/index.ts +34 -0
- package/src/runner/changeCapture.ts +304 -0
- package/src/runner/cli.ts +34 -0
- package/src/runner/runTask.ts +935 -0
- package/src/runner/watch.ts +152 -0
- package/src/security/commandGuard.ts +97 -0
- package/src/security/contentRedaction.ts +85 -0
- package/src/security/pathGuard.ts +175 -0
- package/src/security/planGuard.ts +93 -0
- package/src/security/sensitiveGuard.ts +72 -0
- package/src/smoke-test.ts +1078 -0
- package/src/taskProgress.ts +41 -0
- package/src/taskRuntime.ts +37 -0
- package/src/tools/auditTask.ts +340 -0
- package/src/tools/cancelTask.ts +70 -0
- package/src/tools/createTask.ts +337 -0
- package/src/tools/getPlan.ts +48 -0
- package/src/tools/getTaskFile.ts +111 -0
- package/src/tools/getTaskProgress.ts +5 -0
- package/src/tools/getTaskStatus.ts +85 -0
- package/src/tools/getTaskStdoutTail.ts +61 -0
- package/src/tools/getTaskSummary.ts +307 -0
- package/src/tools/healthCheck.ts +234 -0
- package/src/tools/killTask.ts +5 -0
- package/src/tools/listAgents.ts +54 -0
- package/src/tools/listTasks.ts +139 -0
- package/src/tools/listWorkspace.ts +71 -0
- package/src/tools/readWorkspaceFile.ts +41 -0
- package/src/tools/registry.ts +666 -0
- package/src/tools/retryTask.ts +53 -0
- package/src/tools/savePlan.ts +46 -0
- package/src/tools/taskOutputs.ts +155 -0
- package/src/tools/taskTemplates.ts +162 -0
- package/src/tools/toolCatalog.ts +92 -0
- package/src/tools/waitForTask.ts +90 -0
- package/src/version.ts +2 -0
- package/src/watcherStatus.ts +102 -0
- 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
|
+
}
|