patchwarden 0.4.0 → 0.6.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/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +29 -0
- package/dist/runner/changeCapture.js +107 -5
- package/dist/runner/runTask.js +70 -44
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +94 -2
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +47 -0
- package/dist/tools/getTaskSummary.js +64 -1
- package/dist/tools/healthCheck.d.ts +22 -12
- package/dist/tools/healthCheck.js +23 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +217 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +3 -2
- package/dist/tools/toolCatalog.js +32 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/chatgpt-usage.md +101 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +12 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +66 -6
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +100 -58
- package/scripts/mcp-smoke.js +157 -2
- package/scripts/pack-clean.js +3 -4
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +140 -5
- package/src/runner/runTask.ts +61 -43
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +100 -2
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +111 -1
- package/src/tools/healthCheck.ts +23 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +242 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +35 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
package/package.json
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchwarden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A secure local MCP bridge: clients save plans, local agents execute tasks, and results are returned through workspace-scoped files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
9
|
"patchwarden": "dist/index.js",
|
|
10
|
+
"patchwarden-confirm": "dist/assessments/confirmCli.js",
|
|
10
11
|
"patchwarden-runner": "dist/runner/cli.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"dist/",
|
|
14
15
|
"docs/",
|
|
16
|
+
"!docs/optimization-proposal.md",
|
|
15
17
|
"src/",
|
|
16
18
|
"examples/",
|
|
17
19
|
"scripts/",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"Reset-PatchWarden-Tunnel-Key.cmd",
|
|
20
|
+
"!scripts/kill-patchwarden.ps1",
|
|
21
|
+
"!scripts/launchers/Kill-PatchWarden.cmd",
|
|
22
|
+
"PatchWarden.cmd",
|
|
22
23
|
"README.md",
|
|
24
|
+
"README.en.md",
|
|
23
25
|
"LICENSE",
|
|
24
26
|
"tsconfig.json"
|
|
25
27
|
],
|
|
@@ -31,25 +33,27 @@
|
|
|
31
33
|
"runner": "node dist/runner/cli.js",
|
|
32
34
|
"watch": "node dist/runner/watch.js",
|
|
33
35
|
"doctor": "node dist/doctor.js",
|
|
34
|
-
"
|
|
36
|
+
"doctor:ci": "node dist/doctor.js --allow-default-config",
|
|
37
|
+
"test": "node dist/smoke-test.js && node scripts/lifecycle-smoke.js && node scripts/doctor-smoke.js && node scripts/tunnel-supervisor-smoke.js && node scripts/watcher-supervisor-smoke.js && node scripts/control-smoke.js && node scripts/mcp-manifest-check.js && node scripts/brand-check.js",
|
|
35
38
|
"test:lifecycle": "npm run build && node scripts/lifecycle-smoke.js",
|
|
36
39
|
"test:doctor": "npm run build && node scripts/doctor-smoke.js",
|
|
37
40
|
"test:tunnel-supervisor": "npm run build && node scripts/tunnel-supervisor-smoke.js",
|
|
38
41
|
"test:watcher-supervisor": "npm run build && node scripts/watcher-supervisor-smoke.js",
|
|
39
42
|
"check:tool-manifest": "npm run build && node scripts/mcp-manifest-check.js",
|
|
43
|
+
"check:direct-tool-manifest": "npm run build && node scripts/mcp-manifest-check.js --profile chatgpt_direct",
|
|
40
44
|
"check:brand": "node scripts/brand-check.js",
|
|
41
45
|
"test:mcp": "npm run build && node scripts/mcp-smoke.js",
|
|
42
46
|
"test:http-mcp": "npm run build && node scripts/http-mcp-smoke.js",
|
|
43
47
|
"pretest": "npm run build",
|
|
44
48
|
"pack:clean": "npm run build && node scripts/pack-clean.js",
|
|
45
|
-
"verify:package": "npm run build &&
|
|
49
|
+
"verify:package": "npm run build && node scripts/package-manifest-check.js"
|
|
46
50
|
},
|
|
47
51
|
"dependencies": {
|
|
48
52
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
49
53
|
},
|
|
50
54
|
"devDependencies": {
|
|
51
55
|
"@types/node": "^20.0.0",
|
|
52
|
-
"typescript": "^
|
|
56
|
+
"typescript": "^6.0.3"
|
|
53
57
|
},
|
|
54
58
|
"engines": {
|
|
55
59
|
"node": ">=18.0.0"
|
package/scripts/brand-check.js
CHANGED
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
4
5
|
|
|
5
6
|
const allowedLegacyFiles = new Set([
|
|
6
7
|
".gitignore",
|
|
7
8
|
".npmignore",
|
|
8
9
|
"README.md",
|
|
10
|
+
"README.en.md",
|
|
9
11
|
"docs/migration-from-safe-bifrost.md",
|
|
10
12
|
"docs/release-v0.3.0.md",
|
|
11
13
|
"docs/release-v0.4.0.md",
|
|
14
|
+
"docs/release-v0.6.0.md",
|
|
12
15
|
"scripts/brand-check.js",
|
|
13
16
|
"scripts/pack-clean.js",
|
|
14
17
|
]);
|
|
15
18
|
const legacyPattern = /safe-bifrost|Safe-Bifrost|SAFE_BIFROST|SafeBifrost|safe_bifrost/;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
|
|
20
|
+
const EXCLUDE_DIRS = new Set(["node_modules", ".npm-cache", "dist", "release", ".patchwarden", ".git", "logs", "tmp", "coverage", "build", "out", ".next"]);
|
|
21
|
+
|
|
22
|
+
let trackedFiles;
|
|
23
|
+
let inGit = false;
|
|
24
|
+
try {
|
|
25
|
+
trackedFiles = execFileSync(
|
|
26
|
+
"git",
|
|
27
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
28
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
29
|
+
)
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
inGit = true;
|
|
33
|
+
} catch {
|
|
34
|
+
// Not a git repository — fall back to filesystem walk
|
|
35
|
+
console.warn("[brand-check] Not a Git repository; using filesystem walk.");
|
|
36
|
+
trackedFiles = walkFiles(".");
|
|
37
|
+
if (trackedFiles.length === 0) {
|
|
38
|
+
console.warn("[brand-check] WARNING: no files found in non-Git walk. Exiting cleanly.");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
const failures = [];
|
|
24
44
|
|
|
25
45
|
for (const file of trackedFiles) {
|
|
@@ -29,9 +49,13 @@ for (const file of trackedFiles) {
|
|
|
29
49
|
failures.push(`${normalized}: legacy brand in path`);
|
|
30
50
|
continue;
|
|
31
51
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
try {
|
|
53
|
+
const content = readFileSync(file);
|
|
54
|
+
if (!content.includes(0) && legacyPattern.test(content.toString("utf-8"))) {
|
|
55
|
+
failures.push(`${normalized}: legacy brand in content`);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// skip unreadable files
|
|
35
59
|
}
|
|
36
60
|
}
|
|
37
61
|
|
|
@@ -41,4 +65,26 @@ if (failures.length > 0) {
|
|
|
41
65
|
process.exit(1);
|
|
42
66
|
}
|
|
43
67
|
|
|
44
|
-
|
|
68
|
+
const label = inGit ? "tracked files checked" : "files scanned (non-Git fallback)";
|
|
69
|
+
console.log(`[brand-check] OK: ${trackedFiles.length} ${label}.`);
|
|
70
|
+
|
|
71
|
+
function walkFiles(root) {
|
|
72
|
+
const results = [];
|
|
73
|
+
const visit = (dir) => {
|
|
74
|
+
if (results.length > 10000) return;
|
|
75
|
+
let entries;
|
|
76
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
if (results.length > 10000) break;
|
|
79
|
+
if (e.isDirectory()) {
|
|
80
|
+
if (EXCLUDE_DIRS.has(e.name)) continue;
|
|
81
|
+
visit(join(dir, e.name));
|
|
82
|
+
} else if (e.isFile()) {
|
|
83
|
+
const rel = relative(root, join(dir, e.name)).replace(/\\/g, "/");
|
|
84
|
+
results.push(rel);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
visit(root);
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
if (process.platform !== "win32") {
|
|
9
|
+
console.log("ok - PatchWarden control smoke skipped outside Windows");
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
14
|
+
const root = resolve(scriptDir, "..");
|
|
15
|
+
const manager = join(scriptDir, "manage-patchwarden.ps1");
|
|
16
|
+
const temp = mkdtempSync(join(tmpdir(), "patchwarden-control-smoke-"));
|
|
17
|
+
const mockConfig = join(temp, "patchwarden.config.json");
|
|
18
|
+
writeFileSync(mockConfig, JSON.stringify({
|
|
19
|
+
workspaceRoot: temp,
|
|
20
|
+
plansDir: ".patchwarden/plans",
|
|
21
|
+
tasksDir: ".patchwarden/tasks",
|
|
22
|
+
}), "utf8");
|
|
23
|
+
const env = {
|
|
24
|
+
...process.env,
|
|
25
|
+
LOCALAPPDATA: join(temp, "LocalAppData"),
|
|
26
|
+
APPDATA: join(temp, "AppData"),
|
|
27
|
+
TEMP: join(temp, "Temp"),
|
|
28
|
+
TMP: join(temp, "Temp"),
|
|
29
|
+
PATCHWARDEN_CONFIG: mockConfig,
|
|
30
|
+
};
|
|
31
|
+
let fakeTunnel = null;
|
|
32
|
+
let fakeWatcher = null;
|
|
33
|
+
let healthServer = null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const statusOutput = run(["status", "all", "-Json"]);
|
|
37
|
+
const statuses = JSON.parse(statusOutput);
|
|
38
|
+
if (!Array.isArray(statuses) || statuses.length !== 2) {
|
|
39
|
+
throw new Error(`expected two status rows, got: ${statusOutput}`);
|
|
40
|
+
}
|
|
41
|
+
const byMode = new Map(statuses.map((entry) => [entry.mode, entry]));
|
|
42
|
+
if (byMode.get("core")?.tool_profile !== "chatgpt_core") {
|
|
43
|
+
throw new Error("Core status did not report chatgpt_core");
|
|
44
|
+
}
|
|
45
|
+
if (byMode.get("direct")?.tool_profile !== "chatgpt_direct") {
|
|
46
|
+
throw new Error("Direct status did not report chatgpt_direct");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const startPlan = run(["start", "all", "-WhatIf"]);
|
|
50
|
+
requireText(startPlan, "start:core");
|
|
51
|
+
requireText(startPlan, "start:direct");
|
|
52
|
+
|
|
53
|
+
const restartPlan = run(["restart", "direct", "-WhatIf", "-SkipBuild"]);
|
|
54
|
+
requireText(restartPlan, "stop:direct");
|
|
55
|
+
requireText(restartPlan, "start:direct");
|
|
56
|
+
|
|
57
|
+
const fakeDirectory = join(temp, "fake-tunnel");
|
|
58
|
+
const fakeExecutable = join(fakeDirectory, "tunnel-client.exe");
|
|
59
|
+
const fakeScript = join(fakeDirectory, "sleep.ps1");
|
|
60
|
+
const systemPowerShell = join(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
61
|
+
mkdirSync(fakeDirectory, { recursive: true });
|
|
62
|
+
copyFileSync(systemPowerShell, fakeExecutable);
|
|
63
|
+
writeFileSync(fakeScript, "Start-Sleep -Seconds 120\r\n", "utf8");
|
|
64
|
+
fakeTunnel = spawn(
|
|
65
|
+
fakeExecutable,
|
|
66
|
+
["-NoProfile", "-File", fakeScript, "run", "--profile", "patchwarden-direct"],
|
|
67
|
+
{ stdio: "ignore", windowsHide: true }
|
|
68
|
+
);
|
|
69
|
+
await delay(500);
|
|
70
|
+
if (fakeTunnel.exitCode !== null) throw new Error("fake Tunnel process exited before stop test");
|
|
71
|
+
const directRuntime = join(env.LOCALAPPDATA, "patchwarden", "runtime-direct");
|
|
72
|
+
mkdirSync(directRuntime, { recursive: true });
|
|
73
|
+
mkdirSync(env.TEMP, { recursive: true });
|
|
74
|
+
writeFileSync(join(directRuntime, "tunnel-status.json"), JSON.stringify({
|
|
75
|
+
status: "stopped",
|
|
76
|
+
ready: false,
|
|
77
|
+
pid: null,
|
|
78
|
+
tool_profile: "chatgpt_direct",
|
|
79
|
+
tool_count: 9,
|
|
80
|
+
tools_ready: true,
|
|
81
|
+
}), "utf8");
|
|
82
|
+
writeFileSync(join(directRuntime, "tunnel-client.pid"), String(fakeTunnel.pid), "utf8");
|
|
83
|
+
writeFileSync(join(directRuntime, "tunnel-health-url.txt"), "http://127.0.0.1:8081", "utf8");
|
|
84
|
+
const legacyDirectPid = join(env.TEMP, "patchwarden-direct.pid");
|
|
85
|
+
const legacyDirectUrl = join(env.TEMP, "patchwarden-direct-health.url");
|
|
86
|
+
writeFileSync(legacyDirectPid, String(fakeTunnel.pid), "utf8");
|
|
87
|
+
writeFileSync(legacyDirectUrl, "http://127.0.0.1:8081", "utf8");
|
|
88
|
+
const stopOutput = run(["stop", "direct"]);
|
|
89
|
+
requireText(stopOutput, `Stopped PID ${fakeTunnel.pid}`);
|
|
90
|
+
await waitForExit(fakeTunnel, 5000);
|
|
91
|
+
if (fakeTunnel.exitCode === null) throw new Error("manager did not stop the owned Direct fixture process");
|
|
92
|
+
for (const stalePath of [join(directRuntime, "tunnel-client.pid"), join(directRuntime, "tunnel-health-url.txt"), legacyDirectPid, legacyDirectUrl]) {
|
|
93
|
+
if (existsSync(stalePath)) throw new Error(`manager did not clean stale runtime file: ${stalePath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fakeWatcher = spawn(
|
|
97
|
+
process.execPath,
|
|
98
|
+
["-e", "setTimeout(()=>{},120000)", join(root, "dist", "runner", "watch.js")],
|
|
99
|
+
{ stdio: "ignore", windowsHide: true }
|
|
100
|
+
);
|
|
101
|
+
await delay(500);
|
|
102
|
+
if (fakeWatcher.exitCode !== null) throw new Error("fake watcher exited before kill test");
|
|
103
|
+
const killOutput = run(["kill", "core"]);
|
|
104
|
+
requireText(killOutput, `Stopped PID ${fakeWatcher.pid}`);
|
|
105
|
+
await waitForExit(fakeWatcher, 5000);
|
|
106
|
+
if (fakeWatcher.exitCode === null) throw new Error("kill core did not stop the project-scoped watcher fixture");
|
|
107
|
+
|
|
108
|
+
const coreRuntime = join(env.LOCALAPPDATA, "patchwarden", "runtime");
|
|
109
|
+
mkdirSync(coreRuntime, { recursive: true });
|
|
110
|
+
writeFileSync(join(coreRuntime, "tunnel-status.json"), JSON.stringify({
|
|
111
|
+
status: "stopped",
|
|
112
|
+
ready: false,
|
|
113
|
+
pid: null,
|
|
114
|
+
reason_code: "stale_fixture",
|
|
115
|
+
last_error: "stale failure",
|
|
116
|
+
tool_profile: "chatgpt_core",
|
|
117
|
+
tool_count: 16,
|
|
118
|
+
tools_ready: true,
|
|
119
|
+
}), "utf8");
|
|
120
|
+
healthServer = spawn(
|
|
121
|
+
process.execPath,
|
|
122
|
+
["-e", "require('http').createServer((req,res)=>{res.writeHead(200,{'content-type':'application/json'});res.end(JSON.stringify({ok:true}))}).listen(8080,'127.0.0.1')"],
|
|
123
|
+
{ stdio: "ignore", windowsHide: true }
|
|
124
|
+
);
|
|
125
|
+
await delay(750);
|
|
126
|
+
if (healthServer.exitCode !== null) throw new Error("health fallback fixture could not listen on 127.0.0.1:8080");
|
|
127
|
+
const coreStatusRaw = run(["status", "core", "-Json"]);
|
|
128
|
+
const coreStatusValue = JSON.parse(coreStatusRaw);
|
|
129
|
+
const coreStatus = Array.isArray(coreStatusValue) ? coreStatusValue[0] : coreStatusValue;
|
|
130
|
+
if (coreStatus.status !== "running" || coreStatus.ready !== true || coreStatus.health_alive !== true || coreStatus.reason_code !== "health_endpoint_ready") {
|
|
131
|
+
throw new Error(`health fallback did not override stale runtime JSON: ${coreStatusRaw}`);
|
|
132
|
+
}
|
|
133
|
+
const conflict = runFailure(["restart", "core", "-WhatIf", "-SkipBuild"]);
|
|
134
|
+
requireText(`${conflict.stdout}\n${conflict.stderr}`, "Unsafe health-port conflict");
|
|
135
|
+
if (healthServer.exitCode !== null) throw new Error("manager killed an unrelated health-port owner");
|
|
136
|
+
const scopedKill = run(["kill", "core"]);
|
|
137
|
+
requireText(scopedKill, "No matching Core Agent process");
|
|
138
|
+
if (healthServer.exitCode !== null) throw new Error("kill core terminated an unrelated process");
|
|
139
|
+
|
|
140
|
+
const expectedFiles = [
|
|
141
|
+
"PatchWarden.cmd",
|
|
142
|
+
"scripts/manage-patchwarden.ps1",
|
|
143
|
+
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
144
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
145
|
+
];
|
|
146
|
+
for (const relativePath of expectedFiles) {
|
|
147
|
+
if (!existsSync(join(root, relativePath))) {
|
|
148
|
+
throw new Error(`missing consolidated control file: ${relativePath}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const rootEntry = readFileSync(join(root, "PatchWarden.cmd"), "utf8");
|
|
152
|
+
if (!rootEntry.includes("manage-patchwarden.ps1")) {
|
|
153
|
+
throw new Error("PatchWarden.cmd does not invoke the consolidated manager");
|
|
154
|
+
}
|
|
155
|
+
console.log("ok - control handles orphan cleanup, scoped kill, port conflicts, health fallback, and Core/Direct lifecycle actions");
|
|
156
|
+
} finally {
|
|
157
|
+
if (fakeTunnel?.exitCode === null) fakeTunnel.kill();
|
|
158
|
+
if (fakeWatcher?.exitCode === null) fakeWatcher.kill();
|
|
159
|
+
if (healthServer?.exitCode === null) healthServer.kill();
|
|
160
|
+
rmSync(temp, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function run(args) {
|
|
164
|
+
const result = spawnSync(
|
|
165
|
+
"powershell.exe",
|
|
166
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", manager, ...args],
|
|
167
|
+
{ cwd: root, env, encoding: "utf8" }
|
|
168
|
+
);
|
|
169
|
+
if (result.status !== 0) {
|
|
170
|
+
throw new Error(`manager failed (${args.join(" ")}):\n${result.stdout}\n${result.stderr}`);
|
|
171
|
+
}
|
|
172
|
+
return result.stdout.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runFailure(args) {
|
|
176
|
+
const result = spawnSync(
|
|
177
|
+
"powershell.exe",
|
|
178
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", manager, ...args],
|
|
179
|
+
{ cwd: root, env, encoding: "utf8" }
|
|
180
|
+
);
|
|
181
|
+
if (result.status === 0) {
|
|
182
|
+
throw new Error(`manager unexpectedly succeeded (${args.join(" ")}):\n${result.stdout}\n${result.stderr}`);
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function requireText(value, expected) {
|
|
188
|
+
if (!value.toLowerCase().includes(expected.toLowerCase())) {
|
|
189
|
+
throw new Error(`expected ${JSON.stringify(expected)} in output:\n${value}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function delay(milliseconds) {
|
|
194
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function waitForExit(child, timeoutMilliseconds) {
|
|
198
|
+
if (child.exitCode !== null) return Promise.resolve();
|
|
199
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
200
|
+
const timer = setTimeout(() => rejectPromise(new Error("timed out waiting for fixture process to exit")), timeoutMilliseconds);
|
|
201
|
+
child.once("exit", () => {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
resolvePromise();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
cd /d "%~dp0..\.."
|
|
4
|
+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\start-patchwarden-tunnel.ps1" -ToolProfile chatgpt_direct -Profile patchwarden-direct -HealthListenAddr 127.0.0.1:8081 -SkipWatcher
|
|
5
|
+
echo.
|
|
6
|
+
echo PatchWarden Direct tunnel launcher exited.
|
|
7
|
+
pause
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
cd /d "%~dp0..\.."
|
|
4
|
+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\start-patchwarden-tunnel.ps1" -ToolProfile chatgpt_core -Profile patchwarden -HealthListenAddr 127.0.0.1:8080
|
|
5
|
+
echo.
|
|
6
|
+
echo PatchWarden Core tunnel launcher exited.
|
|
7
|
+
pause
|
|
@@ -53,6 +53,18 @@ async function waitForRunning(getTaskStatus, taskId) {
|
|
|
53
53
|
throw new Error(`Task ${taskId} did not enter running state`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
async function raceWithTimeout(promise, ms, msg) {
|
|
57
|
+
let timer;
|
|
58
|
+
const timeout = new Promise((_, reject) => {
|
|
59
|
+
timer = setTimeout(() => reject(new Error(msg)), ms);
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
return await Promise.race([promise, timeout]);
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
console.log("\n=== PatchWarden Lifecycle Smoke Tests ===\n");
|
|
57
69
|
|
|
58
70
|
try {
|
|
@@ -70,10 +82,14 @@ try {
|
|
|
70
82
|
writeFileSync(join(repoPath, "main.js"), "console.log('fixture');\n", "utf-8");
|
|
71
83
|
writeFileSync(join(repoPath, "second.js"), "console.log('second');\n", "utf-8");
|
|
72
84
|
writeFileSync(join(repoPath, "delete-me.txt"), "delete fixture\n", "utf-8");
|
|
85
|
+
writeFileSync(join(repoPath, ".gitignore"), "dist/\nrelease/\nsync-store.json\n*.log\n", "utf-8");
|
|
86
|
+
mkdirSync(join(repoPath, "release"), { recursive: true });
|
|
87
|
+
writeFileSync(join(repoPath, "release", "tracked.txt"), "tracked artifact fixture\n", "utf-8");
|
|
73
88
|
mkdirSync(plainRepoPath, { recursive: true });
|
|
74
89
|
writeFileSync(join(plainRepoPath, "README.md"), "# Non-Git fixture\n", "utf-8");
|
|
75
90
|
git(["init"]);
|
|
76
|
-
git(["add", "README.md", "main.js", "second.js", "delete-me.txt"]);
|
|
91
|
+
git(["add", "README.md", "main.js", "second.js", "delete-me.txt", ".gitignore"]);
|
|
92
|
+
git(["add", "-f", "release/tracked.txt"]);
|
|
77
93
|
git(["-c", "user.name=PatchWarden Test", "-c", "user.email=test@example.invalid", "commit", "-m", "fixture"]);
|
|
78
94
|
|
|
79
95
|
writeFileSync(
|
|
@@ -114,6 +130,13 @@ try {
|
|
|
114
130
|
command: process.execPath,
|
|
115
131
|
args: ["-e", "require('fs').writeFileSync('fixture.bin',Buffer.from([0,1,2,3,255,0,10]))"],
|
|
116
132
|
},
|
|
133
|
+
artifactwriter: {
|
|
134
|
+
command: process.execPath,
|
|
135
|
+
args: [
|
|
136
|
+
"-e",
|
|
137
|
+
"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.writeFileSync('dist/app.exe','build');fs.writeFileSync('sync-store.json','{}');fs.appendFileSync('release/tracked.txt','changed\\n');fs.writeFileSync('generated.exe','review')",
|
|
138
|
+
],
|
|
139
|
+
},
|
|
117
140
|
},
|
|
118
141
|
allowedTestCommands: ["node --check main.js", "node --check second.js", "node --check missing.js"],
|
|
119
142
|
maxReadFileBytes: 200000,
|
|
@@ -133,12 +156,13 @@ try {
|
|
|
133
156
|
const { getDiff } = await import("../dist/tools/taskOutputs.js");
|
|
134
157
|
const { listAgents } = await import("../dist/tools/listAgents.js");
|
|
135
158
|
const { getTaskSummary } = await import("../dist/tools/getTaskSummary.js");
|
|
159
|
+
const { auditTask } = await import("../dist/tools/auditTask.js");
|
|
136
160
|
const { waitForTask } = await import("../dist/tools/waitForTask.js");
|
|
137
161
|
const { runTask } = await import("../dist/runner/runTask.js");
|
|
138
162
|
|
|
139
163
|
await test("list_agents reports configured executables", async () => {
|
|
140
164
|
const result = listAgents();
|
|
141
|
-
if (result.total !==
|
|
165
|
+
if (result.total !== 8 || result.agents.some((agent) => !agent.available)) {
|
|
142
166
|
throw new Error(`Unexpected agent availability: ${JSON.stringify(result)}`);
|
|
143
167
|
}
|
|
144
168
|
});
|
|
@@ -295,6 +319,35 @@ try {
|
|
|
295
319
|
}
|
|
296
320
|
});
|
|
297
321
|
|
|
322
|
+
await test("artifact hygiene separates source, ignored output, runtime data, and suspicious changes", async () => {
|
|
323
|
+
const plan = savePlan({ title: "Artifact hygiene", content: "Generate representative task outputs." });
|
|
324
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "artifactwriter", repo_path: "repo" });
|
|
325
|
+
const result = await runTask(task.task_id);
|
|
326
|
+
if (result.status !== "done") throw new Error(`Artifact task failed: ${JSON.stringify(result)}`);
|
|
327
|
+
const standard = getTaskSummary(task.task_id);
|
|
328
|
+
const compact = getTaskSummary(task.task_id, { view: "compact", max_items: 1 });
|
|
329
|
+
const counts = standard.artifact_hygiene?.counts || {};
|
|
330
|
+
if (
|
|
331
|
+
counts.tracked_build_artifacts < 1 ||
|
|
332
|
+
counts.ignored_untracked_artifacts < 2 ||
|
|
333
|
+
counts.runtime_generated_files < 1 ||
|
|
334
|
+
counts.suspicious_changes < 2
|
|
335
|
+
) {
|
|
336
|
+
throw new Error(`Artifact classification mismatch: ${JSON.stringify(standard.artifact_hygiene)}`);
|
|
337
|
+
}
|
|
338
|
+
if (compact.view !== "compact" || "log_tails" in compact || compact.artifact_hygiene.max_items !== 1) {
|
|
339
|
+
throw new Error(`Compact summary leaked standard detail: ${JSON.stringify(compact)}`);
|
|
340
|
+
}
|
|
341
|
+
if (!Array.isArray(standard.changed_files) || standard.changed_files.length < 4) {
|
|
342
|
+
throw new Error("Standard summary no longer preserves full changed-file evidence");
|
|
343
|
+
}
|
|
344
|
+
const audit = auditTask(task.task_id);
|
|
345
|
+
const hygieneCheck = audit.checks.find((check) => check.name === "artifact_hygiene");
|
|
346
|
+
if (!hygieneCheck || hygieneCheck.result !== "warn") {
|
|
347
|
+
throw new Error(`Audit did not surface suspicious artifact evidence: ${JSON.stringify(audit)}`);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
298
351
|
await test("deleted tracked files are identified with file stats", async () => {
|
|
299
352
|
const plan = savePlan({ title: "Delete fixture", content: "Delete the designated fixture file." });
|
|
300
353
|
const task = createTask({ plan_id: plan.plan_id, agent: "deleter", repo_path: "repo" });
|
|
@@ -311,10 +364,13 @@ try {
|
|
|
311
364
|
const task = createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "repo" });
|
|
312
365
|
const running = runTask(task.task_id);
|
|
313
366
|
const waited = await waitForTask(task.task_id, 5);
|
|
314
|
-
await running;
|
|
367
|
+
await raceWithTimeout(running, 15000, "wait_for_task loop did not terminate within 15s");
|
|
315
368
|
if (!waited.terminal || waited.continuation_required || !waited.summary) {
|
|
316
369
|
throw new Error(`Unexpected wait response: ${JSON.stringify(waited)}`);
|
|
317
370
|
}
|
|
371
|
+
if (waited.summary.view !== "compact" || "log_tails" in waited.summary) {
|
|
372
|
+
throw new Error(`Terminal wait should embed compact evidence: ${JSON.stringify(waited.summary)}`);
|
|
373
|
+
}
|
|
318
374
|
});
|
|
319
375
|
|
|
320
376
|
await test("wait_for_task explicitly requires another call when the task is not terminal", async () => {
|
|
@@ -338,7 +394,7 @@ try {
|
|
|
338
394
|
throw new Error(`Running summary incomplete: ${JSON.stringify(summary)}`);
|
|
339
395
|
}
|
|
340
396
|
cancelTask(task.task_id);
|
|
341
|
-
await running;
|
|
397
|
+
await raceWithTimeout(running, 15000, "running summary cancel did not terminate within 15s");
|
|
342
398
|
});
|
|
343
399
|
|
|
344
400
|
await test("verification failure produces failed_verification and structured evidence", async () => {
|
|
@@ -423,7 +479,7 @@ try {
|
|
|
423
479
|
if (!request.cancel_requested || request.force_kill_requested) {
|
|
424
480
|
throw new Error(`Unexpected cancel response: ${JSON.stringify(request)}`);
|
|
425
481
|
}
|
|
426
|
-
const result = await running;
|
|
482
|
+
const result = await raceWithTimeout(running, 15000, "cancel_task did not terminate within 15s");
|
|
427
483
|
if (result.status !== "canceled") throw new Error(`Expected canceled, got ${JSON.stringify(result)}`);
|
|
428
484
|
});
|
|
429
485
|
|
|
@@ -434,7 +490,7 @@ try {
|
|
|
434
490
|
await waitForRunning(getTaskStatus, task.task_id);
|
|
435
491
|
const request = killTask(task.task_id);
|
|
436
492
|
if (!request.force_kill_requested) throw new Error(`Unexpected kill response: ${JSON.stringify(request)}`);
|
|
437
|
-
const result = await running;
|
|
493
|
+
const result = await raceWithTimeout(running, 15000, "kill_task did not terminate within 15s");
|
|
438
494
|
if (result.status !== "canceled" || !result.error?.includes("kill_task")) {
|
|
439
495
|
throw new Error(`Expected killed/canceled result, got ${JSON.stringify(result)}`);
|
|
440
496
|
}
|
|
@@ -451,3 +507,7 @@ console.log(`${"=".repeat(50)}\n`);
|
|
|
451
507
|
|
|
452
508
|
if (failed > 0) process.exit(1);
|
|
453
509
|
console.log("ALL LIFECYCLE TESTS PASSED\n");
|
|
510
|
+
|
|
511
|
+
// Safety: force exit after cleanup in case any stray timer/handle keeps the event loop alive.
|
|
512
|
+
// All child processes are killed by the runner; this only guards against OS-level pipe drains.
|
|
513
|
+
process.exit(0);
|