patchwarden 1.5.0 → 1.5.1
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/README.en.md +43 -2
- package/README.md +43 -2
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directGuards.js +30 -8
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +257 -251
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.js +95 -1
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +6 -0
- package/dist/tools/evidencePack.js +213 -6
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directGuards.ts +28 -7
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +252 -250
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +110 -1
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
package/src/httpServer.ts
CHANGED
|
@@ -25,29 +25,30 @@ import { registerTools } from "./tools/registry.js";
|
|
|
25
25
|
import { healthCheck } from "./tools/healthCheck.js";
|
|
26
26
|
import { getToolCatalogSnapshot } from "./tools/registry.js";
|
|
27
27
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
28
|
+
import { logger } from "./logging.js";
|
|
28
29
|
|
|
29
30
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
30
31
|
|
|
31
32
|
const config = loadConfig();
|
|
32
33
|
const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
|
|
33
|
-
|
|
34
|
+
config.httpPort ||
|
|
34
35
|
7331;
|
|
35
36
|
const host = "127.0.0.1";
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
logger.info(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
|
|
39
|
+
logger.info(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
|
|
40
|
+
logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
|
|
40
41
|
|
|
41
42
|
// ── Owner token (optional) ────────────────────────────────────────
|
|
42
43
|
|
|
43
|
-
const httpCfg =
|
|
44
|
+
const httpCfg = config.http || {};
|
|
44
45
|
const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
|
|
45
46
|
const ownerToken = process.env[ownerTokenEnv] || "";
|
|
46
47
|
|
|
47
48
|
if (ownerToken) {
|
|
48
|
-
|
|
49
|
+
logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
|
|
49
50
|
} else {
|
|
50
|
-
|
|
51
|
+
logger.info(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function checkOwnerToken(req: IncomingMessage): boolean {
|
|
@@ -142,7 +143,9 @@ async function handleMcpRequest(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
142
143
|
await mcpServer.connect(transport);
|
|
143
144
|
await transport.handleRequest(req, res);
|
|
144
145
|
} catch (err) {
|
|
145
|
-
|
|
146
|
+
logger.error("[patchwarden-http] Request error", {
|
|
147
|
+
error: err instanceof Error ? err.message : String(err),
|
|
148
|
+
});
|
|
146
149
|
if (!res.headersSent) {
|
|
147
150
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
148
151
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -253,22 +256,22 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
|
|
|
253
256
|
|
|
254
257
|
httpServer.on("error", (err: NodeJS.ErrnoException) => {
|
|
255
258
|
if (err.code === "EADDRINUSE") {
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
logger.fatal(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
|
|
260
|
+
logger.fatal("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
|
|
258
261
|
} else {
|
|
259
|
-
|
|
262
|
+
logger.fatal(`[patchwarden-http] Fatal: ${err.message}`);
|
|
260
263
|
}
|
|
261
264
|
process.exit(1);
|
|
262
265
|
});
|
|
263
266
|
|
|
264
267
|
httpServer.listen(port, host, () => {
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
logger.info(`[patchwarden-http] ✅ Ready`);
|
|
269
|
+
logger.info(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
|
|
267
270
|
});
|
|
268
271
|
|
|
269
272
|
// Graceful shutdown
|
|
270
273
|
process.on("SIGINT", () => {
|
|
271
|
-
|
|
274
|
+
logger.info("[patchwarden-http] Shutting down...");
|
|
272
275
|
httpServer.close(() => process.exit(0));
|
|
273
276
|
});
|
|
274
277
|
|
package/src/index.ts
CHANGED
|
@@ -12,11 +12,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
12
12
|
import { loadConfig } from "./config.js";
|
|
13
13
|
import { registerTools } from "./tools/registry.js";
|
|
14
14
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
15
|
+
import { logger } from "./logging.js";
|
|
15
16
|
|
|
16
17
|
const config = loadConfig();
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
logger.info(`[patchwarden] Workspace: ${config.workspaceRoot}`);
|
|
20
|
+
logger.info("[patchwarden] Transport: stdio");
|
|
20
21
|
|
|
21
22
|
const server = new Server(
|
|
22
23
|
{ name: "patchwarden", version: PATCHWARDEN_VERSION },
|
|
@@ -27,8 +28,10 @@ registerTools(server);
|
|
|
27
28
|
|
|
28
29
|
const transport = new StdioServerTransport();
|
|
29
30
|
server.connect(transport).catch((err) => {
|
|
30
|
-
|
|
31
|
+
logger.fatal("[patchwarden] Fatal", {
|
|
32
|
+
error: err instanceof Error ? err.message : String(err),
|
|
33
|
+
});
|
|
31
34
|
process.exit(1);
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
logger.info("[patchwarden] MCP server ready on stdio");
|
package/src/logging.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { getConfig } from "./config.js";
|
|
|
7
7
|
|
|
8
8
|
// ── Types ─────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
export type LogLevel = "info" | "warn" | "error" | "audit";
|
|
10
|
+
export type LogLevel = "info" | "warn" | "error" | "fatal" | "audit";
|
|
11
11
|
|
|
12
12
|
export interface LogEntry {
|
|
13
13
|
timestamp: string;
|
|
@@ -90,6 +90,15 @@ export class Logger {
|
|
|
90
90
|
emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Emit a fatal-level log entry. Use this immediately before a process
|
|
95
|
+
* exits with a non-zero status, to record the unrecoverable condition.
|
|
96
|
+
* Level is higher than `error`.
|
|
97
|
+
*/
|
|
98
|
+
fatal(message: string, context?: Record<string, unknown>): void {
|
|
99
|
+
emit({ timestamp: new Date().toISOString(), level: "fatal", message, ...context });
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
/**
|
|
94
103
|
* Emit a tool-call audit log entry.
|
|
95
104
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
createReadStream,
|
|
3
4
|
existsSync,
|
|
4
5
|
lstatSync,
|
|
5
6
|
readFileSync,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
writeFileSync,
|
|
9
10
|
} from "node:fs";
|
|
10
11
|
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
11
|
-
import {
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
12
13
|
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
13
14
|
|
|
14
15
|
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
@@ -90,9 +91,10 @@ export interface ChangeArtifacts {
|
|
|
90
91
|
artifact_hygiene: ArtifactHygiene;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
94
|
+
export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapshot> {
|
|
94
95
|
const warnings: string[] = [];
|
|
95
|
-
const
|
|
96
|
+
const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
97
|
+
const isGit = isGitResult.stdout.trim() === "true";
|
|
96
98
|
let head: string | null = null;
|
|
97
99
|
let status = "";
|
|
98
100
|
let paths: string[] = [];
|
|
@@ -101,9 +103,17 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
|
101
103
|
|
|
102
104
|
const dirtyPaths = new Set<string>();
|
|
103
105
|
if (isGit) {
|
|
104
|
-
|
|
106
|
+
// The following five git calls are independent of each other and can run in parallel.
|
|
107
|
+
const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
|
|
108
|
+
runGit(repoPath, ["rev-parse", "HEAD"]),
|
|
109
|
+
runGit(repoPath, ["status", "--porcelain=v1", "-uall"]),
|
|
110
|
+
runGit(repoPath, ["ls-files", "-z"]),
|
|
111
|
+
runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]),
|
|
112
|
+
runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]),
|
|
113
|
+
]);
|
|
114
|
+
|
|
105
115
|
if (headResult.status === 0) head = headResult.stdout.trim() || null;
|
|
106
|
-
status =
|
|
116
|
+
status = statusResult.stdout.trimEnd();
|
|
107
117
|
// Parse git status --porcelain to collect all dirty paths
|
|
108
118
|
for (const line of status.split("\n")) {
|
|
109
119
|
if (line.length < 4) continue;
|
|
@@ -125,20 +135,17 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
for (const path of tracked.stdout.split("\0").filter(Boolean)) trackedPaths.add(normalizePath(path));
|
|
138
|
+
if (trackedResult.status === 0) {
|
|
139
|
+
for (const path of trackedResult.stdout.split("\0").filter(Boolean)) trackedPaths.add(normalizePath(path));
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for (const path of ignored.stdout.split("\0").filter(Boolean)) ignoredPaths.add(normalizePath(path));
|
|
141
|
+
if (ignoredResult.status === 0) {
|
|
142
|
+
for (const path of ignoredResult.stdout.split("\0").filter(Boolean)) ignoredPaths.add(normalizePath(path));
|
|
135
143
|
} else {
|
|
136
144
|
warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
if (listed.status === 0) {
|
|
146
|
+
if (listedResult.status === 0) {
|
|
140
147
|
paths = [...new Set([
|
|
141
|
-
...
|
|
148
|
+
...listedResult.stdout.split("\0").filter(Boolean),
|
|
142
149
|
...walkWorkspace(repoPath),
|
|
143
150
|
])];
|
|
144
151
|
} else {
|
|
@@ -193,11 +200,11 @@ export function writeSnapshot(taskDir: string, filename: string, snapshot: RepoS
|
|
|
193
200
|
writeFileSync(join(taskDir, filename), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
export function buildChangeArtifacts(
|
|
203
|
+
export async function buildChangeArtifacts(
|
|
197
204
|
repoPath: string,
|
|
198
205
|
before: RepoSnapshot,
|
|
199
206
|
after: RepoSnapshot
|
|
200
|
-
): ChangeArtifacts {
|
|
207
|
+
): Promise<ChangeArtifacts> {
|
|
201
208
|
const changedFiles = compareSnapshots(before, after);
|
|
202
209
|
const artifactHygiene = classifyArtifactHygiene(changedFiles);
|
|
203
210
|
const sections: string[] = [];
|
|
@@ -205,18 +212,18 @@ export function buildChangeArtifacts(
|
|
|
205
212
|
|
|
206
213
|
if (before.is_git && after.is_git && scopedPaths.length > 0) {
|
|
207
214
|
if (before.head && after.head && before.head !== after.head) {
|
|
208
|
-
const committed = runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
|
|
215
|
+
const committed = await runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
|
|
209
216
|
if (committed.stdout.trim()) sections.push("# Changes committed during task\n", committed.stdout.trimEnd());
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
const base = after.head || "HEAD";
|
|
213
|
-
const working = runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
|
|
220
|
+
const working = await runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
|
|
214
221
|
if (working.stdout.trim()) sections.push("# Staged and unstaged changes\n", working.stdout.trimEnd());
|
|
215
222
|
|
|
216
223
|
for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
|
|
217
|
-
const tracked = runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
|
|
224
|
+
const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
|
|
218
225
|
if (tracked.status === 0) continue;
|
|
219
|
-
const untracked = runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
|
|
226
|
+
const untracked = await runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
|
|
220
227
|
if (untracked.stdout.trim()) sections.push("# Untracked file\n", untracked.stdout.trimEnd());
|
|
221
228
|
}
|
|
222
229
|
}
|
|
@@ -232,7 +239,7 @@ export function buildChangeArtifacts(
|
|
|
232
239
|
const fullDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
|
|
233
240
|
const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
234
241
|
const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
235
|
-
const fileStats = buildFileStats(repoPath, before, after, changedFiles);
|
|
242
|
+
const fileStats = await buildFileStats(repoPath, before, after, changedFiles);
|
|
236
243
|
|
|
237
244
|
return {
|
|
238
245
|
changed_files: changedFiles,
|
|
@@ -255,13 +262,13 @@ export function buildChangeArtifacts(
|
|
|
255
262
|
};
|
|
256
263
|
}
|
|
257
264
|
|
|
258
|
-
function buildFileStats(
|
|
265
|
+
async function buildFileStats(
|
|
259
266
|
repoPath: string,
|
|
260
267
|
before: RepoSnapshot,
|
|
261
268
|
after: RepoSnapshot,
|
|
262
269
|
changedFiles: ChangedFile[]
|
|
263
|
-
): ChangeArtifacts["file_stats"] {
|
|
264
|
-
|
|
270
|
+
): Promise<ChangeArtifacts["file_stats"]> {
|
|
271
|
+
const results = await Promise.all(changedFiles.map(async (file) => {
|
|
265
272
|
let additions = 0;
|
|
266
273
|
let deletions = 0;
|
|
267
274
|
const paths = file.old_path ? [file.old_path, file.path] : [file.path];
|
|
@@ -272,8 +279,10 @@ function buildFileStats(
|
|
|
272
279
|
ranges.push([before.head, after.head]);
|
|
273
280
|
}
|
|
274
281
|
ranges.push([after.head || "HEAD"]);
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
const rangeResults = await Promise.all(
|
|
283
|
+
ranges.map((range) => runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths]))
|
|
284
|
+
);
|
|
285
|
+
for (const result of rangeResults) {
|
|
277
286
|
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
278
287
|
const [added, removed] = line.split(/\s+/);
|
|
279
288
|
if (/^\d+$/.test(added)) additions += Number(added);
|
|
@@ -286,11 +295,12 @@ function buildFileStats(
|
|
|
286
295
|
try {
|
|
287
296
|
const content = readFileSync(resolve(repoPath, file.path), "utf-8");
|
|
288
297
|
additions = countLines(content);
|
|
289
|
-
} catch {}
|
|
298
|
+
} catch {} // probe failure handled by return value (additions stays 0)
|
|
290
299
|
}
|
|
291
300
|
|
|
292
301
|
return { path: file.path, status: file.change, additions, deletions };
|
|
293
|
-
});
|
|
302
|
+
}));
|
|
303
|
+
return results;
|
|
294
304
|
}
|
|
295
305
|
|
|
296
306
|
function countLines(content: string): number {
|
|
@@ -441,11 +451,11 @@ export interface ArtifactManifest {
|
|
|
441
451
|
artifacts: ArtifactManifestEntry[];
|
|
442
452
|
}
|
|
443
453
|
|
|
444
|
-
export function buildArtifactManifest(
|
|
454
|
+
export async function buildArtifactManifest(
|
|
445
455
|
changedFiles: ChangedFile[],
|
|
446
456
|
repoPath: string,
|
|
447
457
|
taskId?: string
|
|
448
|
-
): ArtifactManifest {
|
|
458
|
+
): Promise<ArtifactManifest> {
|
|
449
459
|
const entries: ArtifactManifestEntry[] = [];
|
|
450
460
|
for (const file of changedFiles) {
|
|
451
461
|
if (file.kind !== "build_artifact") continue;
|
|
@@ -457,7 +467,7 @@ export function buildArtifactManifest(
|
|
|
457
467
|
if (stat.isFile()) {
|
|
458
468
|
size = stat.size;
|
|
459
469
|
if (size <= MAX_HASH_BYTES) {
|
|
460
|
-
sha256 =
|
|
470
|
+
sha256 = await hashFileAsync(absolutePath);
|
|
461
471
|
}
|
|
462
472
|
}
|
|
463
473
|
} catch {
|
|
@@ -635,16 +645,34 @@ function walkWorkspace(root: string): string[] {
|
|
|
635
645
|
return result;
|
|
636
646
|
}
|
|
637
647
|
|
|
638
|
-
function runGit(repoPath: string, args: string[]) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
648
|
+
function runGit(repoPath: string, args: string[]): Promise<{ status: number | null; stdout: string; stderr: string }> {
|
|
649
|
+
return new Promise((resolve) => {
|
|
650
|
+
execFile("git", args, {
|
|
651
|
+
cwd: repoPath,
|
|
652
|
+
encoding: "utf-8",
|
|
653
|
+
timeout: 30_000,
|
|
654
|
+
maxBuffer: MAX_DIFF_BYTES,
|
|
655
|
+
}, (err, stdout, stderr) => {
|
|
656
|
+
let status: number | null;
|
|
657
|
+
if (err) {
|
|
658
|
+
// err.code is the exit code (number) when the process exited with a non-zero status;
|
|
659
|
+
// it is a string (e.g. "ENOENT") when spawning failed or the buffer limit was hit.
|
|
660
|
+
status = typeof err.code === "number" ? err.code : null;
|
|
661
|
+
} else {
|
|
662
|
+
status = 0;
|
|
663
|
+
}
|
|
664
|
+
resolve({ status, stdout: stdout || "", stderr: stderr || "" });
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function hashFileAsync(filePath: string): Promise<string> {
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
671
|
+
const hash = createHash("sha256");
|
|
672
|
+
const stream = createReadStream(filePath);
|
|
673
|
+
stream.on("error", reject);
|
|
674
|
+
hash.on("error", reject);
|
|
675
|
+
hash.on("finish", () => resolve(hash.digest("hex")));
|
|
676
|
+
stream.pipe(hash);
|
|
644
677
|
});
|
|
645
|
-
return {
|
|
646
|
-
status: result.status,
|
|
647
|
-
stdout: result.stdout || "",
|
|
648
|
-
stderr: result.stderr || "",
|
|
649
|
-
};
|
|
650
678
|
}
|
package/src/runner/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { runTask } from "./runTask.js";
|
|
11
11
|
import { loadConfig } from "../config.js";
|
|
12
|
+
import { logger } from "../logging.js";
|
|
12
13
|
|
|
13
14
|
// Load config early
|
|
14
15
|
loadConfig();
|
|
@@ -16,19 +17,19 @@ loadConfig();
|
|
|
16
17
|
const taskId = process.argv[2] || process.env.PATCHWARDEN_TASK_ID;
|
|
17
18
|
|
|
18
19
|
if (!taskId) {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
logger.info("Usage: node dist/runner/cli.js <task_id>");
|
|
21
|
+
logger.info(" or: npm run runner -- <task_id>");
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
logger.info(`[runner] Starting task: ${taskId}`);
|
|
25
26
|
|
|
26
27
|
const result = await runTask(taskId);
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
logger.info(`[runner] Task ${result.task_id}: ${result.status}`);
|
|
29
30
|
if (result.error) {
|
|
30
|
-
|
|
31
|
+
logger.fatal(`[runner] Error: ${result.error}`);
|
|
31
32
|
process.exit(1);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
logger.info("[runner] Done.");
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { platform } from "node:os";
|
|
5
|
+
|
|
6
|
+
const isWindows = platform() === "win32";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Remove a file or directory, working around Node.js fs.rmSync issues on Windows
|
|
10
|
+
* where recursive directory deletion silently fails (Node v22+).
|
|
11
|
+
* Falls back to `rmdir /s /q` on Windows if rmSync doesn't remove the path.
|
|
12
|
+
*/
|
|
13
|
+
function safeRemoveSync(targetPath: string): void {
|
|
14
|
+
rmSync(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
15
|
+
if (isWindows && existsSync(targetPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const stat = statSync(targetPath);
|
|
18
|
+
if (stat.isDirectory()) {
|
|
19
|
+
execFileSync("cmd", ["/c", "rmdir", "/s", "/q", targetPath], { windowsHide: true });
|
|
20
|
+
} else {
|
|
21
|
+
execFileSync("cmd", ["/c", "del", "/f", "/q", targetPath], { windowsHide: true });
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Best effort — caller will verify via existsSync if needed
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
|
|
6
30
|
const CLEANUP_DIRS = new Set(["__pycache__", "dist", "release_packages", "temp_files"]);
|
|
@@ -42,7 +66,7 @@ export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskC
|
|
|
42
66
|
continue;
|
|
43
67
|
}
|
|
44
68
|
try {
|
|
45
|
-
|
|
69
|
+
safeRemoveSync(candidate.path);
|
|
46
70
|
report.removed.push({ path: rel, reason: candidate.reason });
|
|
47
71
|
} catch (error) {
|
|
48
72
|
report.skipped.push({
|