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,41 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { TaskPhase } from "./tools/createTask.js";
|
|
4
|
+
|
|
5
|
+
const PHASES: Array<{ phase: TaskPhase; label: string }> = [
|
|
6
|
+
{ phase: "queued", label: "Queued" },
|
|
7
|
+
{ phase: "preparing", label: "Preparing repository snapshot" },
|
|
8
|
+
{ phase: "executing_agent", label: "Executing local agent" },
|
|
9
|
+
{ phase: "running_tests", label: "Running configured verification" },
|
|
10
|
+
{ phase: "collecting_artifacts", label: "Collecting diff and result artifacts" },
|
|
11
|
+
{ phase: "completed", label: "Completed" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function writeTaskProgress(
|
|
15
|
+
taskDir: string,
|
|
16
|
+
phase: TaskPhase,
|
|
17
|
+
options?: { currentCommand?: string | null; note?: string; heartbeatAt?: string }
|
|
18
|
+
): void {
|
|
19
|
+
const currentIndex = PHASES.findIndex((item) => item.phase === phase);
|
|
20
|
+
const isTerminalException = phase === "failed" || phase === "canceled";
|
|
21
|
+
const lines = ["# PatchWarden Task Progress", ""];
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < PHASES.length; index++) {
|
|
24
|
+
const item = PHASES[index];
|
|
25
|
+
let marker = " ";
|
|
26
|
+
if (currentIndex >= 0 && index < currentIndex) marker = "x";
|
|
27
|
+
if (currentIndex >= 0 && index === currentIndex) marker = phase === "completed" ? "x" : ">";
|
|
28
|
+
if (isTerminalException && index < PHASES.length - 1) marker = "x";
|
|
29
|
+
lines.push(`- [${marker}] ${item.label}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isTerminalException) {
|
|
33
|
+
lines.push("", `Final state: ${phase}`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("", `Phase: ${phase}`);
|
|
36
|
+
lines.push(`Last heartbeat: ${options?.heartbeatAt || new Date().toISOString()}`);
|
|
37
|
+
if (options?.currentCommand) lines.push(`Current command: ${options.currentCommand}`);
|
|
38
|
+
if (options?.note) lines.push("", `Note: ${options.note}`);
|
|
39
|
+
|
|
40
|
+
writeFileSync(join(taskDir, "progress.md"), `${lines.join("\n")}\n`, "utf-8");
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { TaskPhase } from "./tools/createTask.js";
|
|
4
|
+
|
|
5
|
+
export interface TaskRuntimeData {
|
|
6
|
+
phase: TaskPhase;
|
|
7
|
+
last_heartbeat_at: string;
|
|
8
|
+
current_command: string | null;
|
|
9
|
+
runner_pid?: number;
|
|
10
|
+
child_pid?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readTaskRuntime(taskDir: string): Partial<TaskRuntimeData> {
|
|
14
|
+
const runtimeFile = join(taskDir, "runtime.json");
|
|
15
|
+
if (!existsSync(runtimeFile)) return {};
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(runtimeFile, "utf-8")) as Partial<TaskRuntimeData>;
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeTaskRuntime(
|
|
24
|
+
taskDir: string,
|
|
25
|
+
patch: Partial<TaskRuntimeData>
|
|
26
|
+
): TaskRuntimeData {
|
|
27
|
+
const current = readTaskRuntime(taskDir);
|
|
28
|
+
const next = {
|
|
29
|
+
phase: "preparing" as TaskPhase,
|
|
30
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
31
|
+
current_command: null,
|
|
32
|
+
...current,
|
|
33
|
+
...patch,
|
|
34
|
+
};
|
|
35
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify(next, null, 2), "utf-8");
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve, relative } from "node:path";
|
|
3
|
+
import { getTasksDir, getConfig } from "../config.js";
|
|
4
|
+
import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
5
|
+
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
|
+
|
|
7
|
+
interface AuditCheck {
|
|
8
|
+
name: string;
|
|
9
|
+
result: "pass" | "warn" | "fail";
|
|
10
|
+
detail: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AuditRisk {
|
|
14
|
+
severity: "low" | "medium" | "high";
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AuditTaskOutput {
|
|
19
|
+
task_id: string;
|
|
20
|
+
verdict: "pass" | "warn" | "fail";
|
|
21
|
+
summary: string;
|
|
22
|
+
checks: AuditCheck[];
|
|
23
|
+
risks: AuditRisk[];
|
|
24
|
+
recommended_next_actions: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Release claim patterns — anything that claims remote publish/release/deploy
|
|
28
|
+
const RELEASE_PATTERNS = [
|
|
29
|
+
/npm\s+package\s+version\s+published/i,
|
|
30
|
+
/npm\s+(publish|published)/i,
|
|
31
|
+
/npm\s+release/i,
|
|
32
|
+
/github\s+release\s+created/i,
|
|
33
|
+
/git\s+tag\s+(pushed|created)/i,
|
|
34
|
+
/release\s+zip\s+uploaded/i,
|
|
35
|
+
/npm\s+publish\s+completed/i,
|
|
36
|
+
/deploy(ed|ment)?\s+(to|on)\s+(npm|registry|github)/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function scanForReleaseClaims(text: string): string[] {
|
|
40
|
+
const found: string[] = [];
|
|
41
|
+
for (const pattern of RELEASE_PATTERNS) {
|
|
42
|
+
const match = text.match(pattern);
|
|
43
|
+
if (match) found.push(match[0]);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findMdFiles(dir: string, maxDepth = 3): string[] {
|
|
49
|
+
const results: string[] = [];
|
|
50
|
+
if (!existsSync(dir) || maxDepth <= 0) return results;
|
|
51
|
+
try {
|
|
52
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
const full = join(dir, e.name);
|
|
55
|
+
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
|
|
56
|
+
results.push(...findMdFiles(full, maxDepth - 1));
|
|
57
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
58
|
+
results.push(full);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch { /* skip unreadable dirs */ }
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function auditTask(taskId: string): AuditTaskOutput {
|
|
66
|
+
const config = getConfig();
|
|
67
|
+
const tasksDir = getTasksDir(config);
|
|
68
|
+
const taskDir = join(tasksDir, taskId);
|
|
69
|
+
const statusFile = join(taskDir, "status.json");
|
|
70
|
+
|
|
71
|
+
guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
|
|
72
|
+
|
|
73
|
+
if (!existsSync(statusFile)) {
|
|
74
|
+
throw new Error(`Task not found: "${taskId}"`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const statusData = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
78
|
+
const checks: AuditCheck[] = [];
|
|
79
|
+
const risks: AuditRisk[] = [];
|
|
80
|
+
const actions: string[] = [];
|
|
81
|
+
|
|
82
|
+
// ── 1. Task status ──
|
|
83
|
+
const taskStatus = statusData.status || "unknown";
|
|
84
|
+
const failedStatuses = new Set(["failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled"]);
|
|
85
|
+
checks.push({
|
|
86
|
+
name: "task_status",
|
|
87
|
+
result: taskStatus === "done" ? "pass" : failedStatuses.has(taskStatus) ? "fail" : "warn",
|
|
88
|
+
detail: `Task status is "${taskStatus}".`,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── 2. result.md ──
|
|
92
|
+
const resultFile = join(taskDir, "result.md");
|
|
93
|
+
const hasResult = existsSync(resultFile);
|
|
94
|
+
checks.push({
|
|
95
|
+
name: "result_md_exists",
|
|
96
|
+
result: hasResult ? "pass" : "fail",
|
|
97
|
+
detail: hasResult ? "result.md found." : "result.md is missing.",
|
|
98
|
+
});
|
|
99
|
+
if (!hasResult) risks.push({ severity: "high", description: "No result.md — cannot verify what agent did." });
|
|
100
|
+
|
|
101
|
+
const resultJsonFile = join(taskDir, "result.json");
|
|
102
|
+
checks.push({
|
|
103
|
+
name: "result_json_exists",
|
|
104
|
+
result: existsSync(resultJsonFile) ? "pass" : "fail",
|
|
105
|
+
detail: existsSync(resultJsonFile) ? "result.json found." : "result.json is missing.",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const verifyJsonFile = join(taskDir, "verify.json");
|
|
109
|
+
checks.push({
|
|
110
|
+
name: "verify_json_exists",
|
|
111
|
+
result: existsSync(verifyJsonFile) ? "pass" : "warn",
|
|
112
|
+
detail: existsSync(verifyJsonFile) ? "verify.json found." : "verify.json is missing.",
|
|
113
|
+
});
|
|
114
|
+
if (existsSync(verifyJsonFile)) {
|
|
115
|
+
try {
|
|
116
|
+
const verify = JSON.parse(readFileSync(verifyJsonFile, "utf-8"));
|
|
117
|
+
checks.push({
|
|
118
|
+
name: "verify_status",
|
|
119
|
+
result: verify.status === "passed" ? "pass" : verify.status === "failed" ? "fail" : "warn",
|
|
120
|
+
detail: `Structured verification status is "${verify.status || "unknown"}".`,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
checks.push({ name: "verify_status", result: "fail", detail: "verify.json is invalid JSON." });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const outOfScope = Array.isArray(statusData.out_of_scope_changes) ? statusData.out_of_scope_changes : [];
|
|
128
|
+
checks.push({
|
|
129
|
+
name: "scope_changes",
|
|
130
|
+
result: outOfScope.length > 0 ? "fail" : "pass",
|
|
131
|
+
detail: outOfScope.length > 0
|
|
132
|
+
? `${outOfScope.length} out-of-scope change(s) detected.`
|
|
133
|
+
: "No out-of-scope changes recorded.",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── 3. test.log ──
|
|
137
|
+
const testLogFile = join(taskDir, "test.log");
|
|
138
|
+
const hasTestLog = existsSync(testLogFile);
|
|
139
|
+
checks.push({
|
|
140
|
+
name: "test_log_exists",
|
|
141
|
+
result: hasTestLog ? "pass" : "warn",
|
|
142
|
+
detail: hasTestLog ? "test.log found." : "test.log is missing.",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── 4. git.diff ──
|
|
146
|
+
const diffFile = join(taskDir, "git.diff");
|
|
147
|
+
checks.push({
|
|
148
|
+
name: "git_diff_exists",
|
|
149
|
+
result: existsSync(diffFile) ? "pass" : "warn",
|
|
150
|
+
detail: existsSync(diffFile) ? "git.diff found." : "git.diff is missing.",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── 5. repo_path consistency — use resolved_repo_path, NOT resolve() ──
|
|
154
|
+
let repoPathSafe = "";
|
|
155
|
+
let repoConsistent = false;
|
|
156
|
+
try {
|
|
157
|
+
// Prefer the pre-resolved absolute path from task metadata
|
|
158
|
+
const resolvedRepoPath = statusData.resolved_repo_path;
|
|
159
|
+
if (resolvedRepoPath && typeof resolvedRepoPath === "string") {
|
|
160
|
+
repoPathSafe = guardWorkspacePath(resolvedRepoPath, config.workspaceRoot);
|
|
161
|
+
} else if (statusData.repo_path) {
|
|
162
|
+
repoPathSafe = guardWorkspacePath(statusData.repo_path, config.workspaceRoot);
|
|
163
|
+
} else {
|
|
164
|
+
repoPathSafe = resolve(config.workspaceRoot);
|
|
165
|
+
}
|
|
166
|
+
repoConsistent = true;
|
|
167
|
+
} catch {
|
|
168
|
+
repoConsistent = false;
|
|
169
|
+
repoPathSafe = statusData.resolved_repo_path || statusData.repo_path || config.workspaceRoot;
|
|
170
|
+
}
|
|
171
|
+
checks.push({
|
|
172
|
+
name: "repo_path_consistency",
|
|
173
|
+
result: repoConsistent ? "pass" : "fail",
|
|
174
|
+
detail: repoConsistent
|
|
175
|
+
? `repo_path "${statusData.repo_path || "."}" resolves within workspace.`
|
|
176
|
+
: `repo_path "${statusData.repo_path}" is outside workspace.`,
|
|
177
|
+
});
|
|
178
|
+
if (!repoConsistent) risks.push({ severity: "high", description: "repo_path inconsistent with workspace." });
|
|
179
|
+
|
|
180
|
+
// ── 6. package.json scripts ──
|
|
181
|
+
const pkgJsonPath = join(repoPathSafe, "package.json");
|
|
182
|
+
let pkgScripts: string[] = [];
|
|
183
|
+
if (existsSync(pkgJsonPath)) {
|
|
184
|
+
try {
|
|
185
|
+
guardSensitivePath(pkgJsonPath);
|
|
186
|
+
const pkgData = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
187
|
+
pkgScripts = Object.keys(pkgData.scripts || {});
|
|
188
|
+
checks.push({
|
|
189
|
+
name: "package_json_scripts",
|
|
190
|
+
result: "pass",
|
|
191
|
+
detail: `package.json found with ${pkgScripts.length} scripts: ${pkgScripts.join(", ") || "(none)"}.`,
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
checks.push({ name: "package_json_scripts", result: "warn", detail: "package.json exists but could not be read (may be sensitive)." });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── 7. Scan all docs (result.md, README.md, docs/**/*.md) ──
|
|
199
|
+
const docsToScan: string[] = [];
|
|
200
|
+
if (hasResult) docsToScan.push(resultFile);
|
|
201
|
+
const readmePath = join(repoPathSafe, "README.md");
|
|
202
|
+
if (existsSync(readmePath)) docsToScan.push(readmePath);
|
|
203
|
+
const docsDir = join(repoPathSafe, "docs");
|
|
204
|
+
if (existsSync(docsDir)) {
|
|
205
|
+
docsToScan.push(...findMdFiles(docsDir));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Collect all npm run references from all docs
|
|
209
|
+
const allNpmRunRefs = new Set<string>();
|
|
210
|
+
const allReleaseClaims: string[] = [];
|
|
211
|
+
|
|
212
|
+
for (const docPath of docsToScan) {
|
|
213
|
+
let content: string;
|
|
214
|
+
try {
|
|
215
|
+
if (docPath !== resultFile) {
|
|
216
|
+
guardReadPath(docPath, config.workspaceRoot);
|
|
217
|
+
guardSensitivePath(docPath);
|
|
218
|
+
}
|
|
219
|
+
content = readFileSync(docPath, "utf-8");
|
|
220
|
+
} catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Extract npm run xxx
|
|
225
|
+
const refs = content.match(/npm(?:\.cmd)?\s+run\s+([a-zA-Z0-9:_-]+)/gi) || [];
|
|
226
|
+
for (const ref of refs) {
|
|
227
|
+
const scriptName = ref.replace(/npm\s+run\s+/i, "").replace(/[^a-zA-Z0-9:_-]/g, "");
|
|
228
|
+
if (scriptName) allNpmRunRefs.add(scriptName);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check release claims
|
|
232
|
+
const claims = scanForReleaseClaims(content);
|
|
233
|
+
for (const c of claims) allReleaseClaims.push(`[${relative(repoPathSafe, docPath) || docPath}] ${c}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Cross-check npm run refs against package.json scripts
|
|
237
|
+
for (const scriptName of allNpmRunRefs) {
|
|
238
|
+
if (pkgScripts.length > 0 && !pkgScripts.includes(scriptName)) {
|
|
239
|
+
checks.push({
|
|
240
|
+
name: `npm_script_${scriptName}`,
|
|
241
|
+
result: "warn",
|
|
242
|
+
detail: `Docs mention "npm run ${scriptName}" but this script is missing from package.json.`,
|
|
243
|
+
});
|
|
244
|
+
risks.push({
|
|
245
|
+
severity: "medium",
|
|
246
|
+
description: `Command "npm run ${scriptName}" referenced in docs but not found in package.json scripts.`,
|
|
247
|
+
});
|
|
248
|
+
actions.push(`Verify whether "npm run ${scriptName}" should exist or if the agent fabricated it.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (allNpmRunRefs.size > 0 && pkgScripts.length > 0) {
|
|
252
|
+
const missing = [...allNpmRunRefs].filter(s => !pkgScripts.includes(s));
|
|
253
|
+
if (missing.length === 0) {
|
|
254
|
+
checks.push({
|
|
255
|
+
name: "npm_scripts_crosscheck",
|
|
256
|
+
result: "pass",
|
|
257
|
+
detail: `All ${allNpmRunRefs.size} npm run references in docs exist in package.json.`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── 8. Release claims always flagged as unverified ──
|
|
263
|
+
if (allReleaseClaims.length > 0) {
|
|
264
|
+
checks.push({
|
|
265
|
+
name: "release_claims_unverified",
|
|
266
|
+
result: "warn",
|
|
267
|
+
detail: `Found ${allReleaseClaims.length} remote publish/release claim(s): ${allReleaseClaims.slice(0, 3).join("; ")}${allReleaseClaims.length > 3 ? "..." : ""}. These are UNVERIFIED.`,
|
|
268
|
+
});
|
|
269
|
+
risks.push({
|
|
270
|
+
severity: "high",
|
|
271
|
+
description: `${allReleaseClaims.length} remote publish/release/deploy claim(s) found in docs. PatchWarden cannot independently verify npm/GitHub actions. Manual confirmation required.`,
|
|
272
|
+
});
|
|
273
|
+
actions.push("Manually verify all npm publish / GitHub release / git tag claims before accepting the task as complete.");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── 9. test.log Exit code check ──
|
|
277
|
+
if (hasTestLog) {
|
|
278
|
+
const testLogContent = readFileSync(testLogFile, "utf-8");
|
|
279
|
+
|
|
280
|
+
// Check test_command visibility
|
|
281
|
+
if (statusData.test_command && testLogContent.includes(statusData.test_command)) {
|
|
282
|
+
checks.push({ name: "test_command_in_log", result: "pass", detail: "test.log contains the configured test command." });
|
|
283
|
+
} else if (statusData.test_command) {
|
|
284
|
+
checks.push({ name: "test_command_in_log", result: "warn", detail: `test.log does not clearly show "${statusData.test_command}".` });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Extract Exit code
|
|
288
|
+
const exitMatch = testLogContent.match(/Exit\s*code:\s*(\d+)/i);
|
|
289
|
+
if (exitMatch) {
|
|
290
|
+
const exitCode = parseInt(exitMatch[1]);
|
|
291
|
+
if (exitCode === 0) {
|
|
292
|
+
checks.push({ name: "test_exit_code", result: "pass", detail: "Test exit code is 0." });
|
|
293
|
+
} else {
|
|
294
|
+
checks.push({ name: "test_exit_code", result: "fail", detail: `Test exit code is ${exitCode} (non-zero).` });
|
|
295
|
+
risks.push({ severity: "high", description: `Tests failed with exit code ${exitCode}.` });
|
|
296
|
+
actions.push("Review test.log failures and fix before accepting this task.");
|
|
297
|
+
}
|
|
298
|
+
} else if (statusData.test_command) {
|
|
299
|
+
checks.push({ name: "test_exit_code", result: "warn", detail: "test.log does not contain 'Exit code:' line — cannot verify test result." });
|
|
300
|
+
risks.push({ severity: "medium", description: "Test command was configured but test.log has no exit code." });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── 10. Summarize ──
|
|
305
|
+
const failCount = checks.filter((c) => c.result === "fail").length;
|
|
306
|
+
const warnCount = checks.filter((c) => c.result === "warn").length;
|
|
307
|
+
const passCount = checks.filter((c) => c.result === "pass").length;
|
|
308
|
+
const verdict: AuditTaskOutput["verdict"] = failCount > 0 ? "fail" : warnCount > 0 ? "warn" : "pass";
|
|
309
|
+
|
|
310
|
+
let summary = `Audit complete: ${passCount} pass, ${warnCount} warn, ${failCount} fail across ${checks.length} checks. `;
|
|
311
|
+
summary += risks.length > 0 ? `${risks.length} risk(s) identified.` : "No risks identified.";
|
|
312
|
+
|
|
313
|
+
if (actions.length === 0) {
|
|
314
|
+
actions.push("No specific actions recommended.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Write independent-review.md
|
|
318
|
+
const reviewMd = [
|
|
319
|
+
"# Independent Review",
|
|
320
|
+
"",
|
|
321
|
+
`**Task**: ${taskId}`,
|
|
322
|
+
`**Verdict**: ${verdict.toUpperCase()}`,
|
|
323
|
+
"",
|
|
324
|
+
"## Summary",
|
|
325
|
+
summary,
|
|
326
|
+
"",
|
|
327
|
+
"## Checks",
|
|
328
|
+
...checks.map((c) => `- [${c.result === "pass" ? "x" : " "}] **${c.name}**: ${c.detail}`),
|
|
329
|
+
"",
|
|
330
|
+
"## Risks",
|
|
331
|
+
...risks.map((r) => `- [${r.severity}] ${r.description}`),
|
|
332
|
+
"",
|
|
333
|
+
"## Recommended Actions",
|
|
334
|
+
...actions.map((a) => `- ${a}`),
|
|
335
|
+
].join("\n");
|
|
336
|
+
|
|
337
|
+
writeFileSync(join(taskDir, "independent-review.md"), reviewMd, "utf-8");
|
|
338
|
+
|
|
339
|
+
return { task_id: taskId, verdict, summary, checks, risks, recommended_next_actions: actions };
|
|
340
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getTasksDir, getConfig } from "../config.js";
|
|
4
|
+
import { guardReadPath } from "../security/pathGuard.js";
|
|
5
|
+
import { writeTaskProgress } from "../taskProgress.js";
|
|
6
|
+
import type { TaskStatus } from "./createTask.js";
|
|
7
|
+
|
|
8
|
+
export function cancelTask(taskId: string) {
|
|
9
|
+
return requestTaskTermination(taskId, false);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function requestTaskTermination(taskId: string, force: boolean) {
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
const taskDir = join(getTasksDir(config), taskId);
|
|
15
|
+
const statusFile = join(taskDir, "status.json");
|
|
16
|
+
guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
|
|
17
|
+
|
|
18
|
+
if (!existsSync(statusFile)) throw new Error(`Task not found: "${taskId}"`);
|
|
19
|
+
|
|
20
|
+
const data = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
21
|
+
const currentStatus: TaskStatus = data.status;
|
|
22
|
+
if (["done", "failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled"].includes(currentStatus)) {
|
|
23
|
+
return {
|
|
24
|
+
task_id: taskId,
|
|
25
|
+
previous_status: currentStatus,
|
|
26
|
+
new_status: currentStatus,
|
|
27
|
+
message: `Task is already ${currentStatus}. No action taken.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
if (currentStatus === "pending") {
|
|
33
|
+
data.status = "canceled";
|
|
34
|
+
data.phase = "canceled";
|
|
35
|
+
data.canceled_at = now;
|
|
36
|
+
data.cancel_reason = force ? "Killed before execution by user request." : "Canceled by user request.";
|
|
37
|
+
data.updated_at = now;
|
|
38
|
+
writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
|
|
39
|
+
writeTaskProgress(taskDir, "canceled", { note: data.cancel_reason, heartbeatAt: now });
|
|
40
|
+
return {
|
|
41
|
+
task_id: taskId,
|
|
42
|
+
previous_status: "pending",
|
|
43
|
+
new_status: "canceled",
|
|
44
|
+
message: "Pending task canceled. It will not be executed by watcher.",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
data.cancel_requested = true;
|
|
49
|
+
data.cancel_requested_at = now;
|
|
50
|
+
data.force_kill_requested = force;
|
|
51
|
+
if (force) data.kill_requested_at = now;
|
|
52
|
+
data.phase = force ? "terminating" : "canceling";
|
|
53
|
+
data.updated_at = now;
|
|
54
|
+
writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
|
|
55
|
+
writeTaskProgress(taskDir, data.phase, {
|
|
56
|
+
note: force ? "Immediate termination requested." : "Graceful cancellation requested.",
|
|
57
|
+
heartbeatAt: now,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
task_id: taskId,
|
|
62
|
+
previous_status: "running",
|
|
63
|
+
new_status: "running",
|
|
64
|
+
cancel_requested: true,
|
|
65
|
+
force_kill_requested: force,
|
|
66
|
+
message: force
|
|
67
|
+
? "Kill requested. The runner that owns the child process will terminate it."
|
|
68
|
+
: "Cancel requested. The runner will stop the child process safely.",
|
|
69
|
+
};
|
|
70
|
+
}
|