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/dist/index.js
CHANGED
|
@@ -11,14 +11,17 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
11
11
|
import { loadConfig } from "./config.js";
|
|
12
12
|
import { registerTools } from "./tools/registry.js";
|
|
13
13
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
14
|
+
import { logger } from "./logging.js";
|
|
14
15
|
const config = loadConfig();
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
logger.info(`[patchwarden] Workspace: ${config.workspaceRoot}`);
|
|
17
|
+
logger.info("[patchwarden] Transport: stdio");
|
|
17
18
|
const server = new Server({ name: "patchwarden", version: PATCHWARDEN_VERSION }, { capabilities: { tools: {} } });
|
|
18
19
|
registerTools(server);
|
|
19
20
|
const transport = new StdioServerTransport();
|
|
20
21
|
server.connect(transport).catch((err) => {
|
|
21
|
-
|
|
22
|
+
logger.fatal("[patchwarden] Fatal", {
|
|
23
|
+
error: err instanceof Error ? err.message : String(err),
|
|
24
|
+
});
|
|
22
25
|
process.exit(1);
|
|
23
26
|
});
|
|
24
|
-
|
|
27
|
+
logger.info("[patchwarden] MCP server ready on stdio");
|
package/dist/logging.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LogLevel = "info" | "warn" | "error" | "audit";
|
|
1
|
+
export type LogLevel = "info" | "warn" | "error" | "fatal" | "audit";
|
|
2
2
|
export interface LogEntry {
|
|
3
3
|
timestamp: string;
|
|
4
4
|
level: LogLevel;
|
|
@@ -39,6 +39,12 @@ export declare class Logger {
|
|
|
39
39
|
info(message: string, context?: Record<string, unknown>): void;
|
|
40
40
|
warn(message: string, context?: Record<string, unknown>): void;
|
|
41
41
|
error(message: string, context?: Record<string, unknown>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Emit a fatal-level log entry. Use this immediately before a process
|
|
44
|
+
* exits with a non-zero status, to record the unrecoverable condition.
|
|
45
|
+
* Level is higher than `error`.
|
|
46
|
+
*/
|
|
47
|
+
fatal(message: string, context?: Record<string, unknown>): void;
|
|
42
48
|
/**
|
|
43
49
|
* Emit a tool-call audit log entry.
|
|
44
50
|
*
|
package/dist/logging.js
CHANGED
|
@@ -51,6 +51,14 @@ export class Logger {
|
|
|
51
51
|
error(message, context) {
|
|
52
52
|
emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Emit a fatal-level log entry. Use this immediately before a process
|
|
56
|
+
* exits with a non-zero status, to record the unrecoverable condition.
|
|
57
|
+
* Level is higher than `error`.
|
|
58
|
+
*/
|
|
59
|
+
fatal(message, context) {
|
|
60
|
+
emit({ timestamp: new Date().toISOString(), level: "fatal", message, ...context });
|
|
61
|
+
}
|
|
54
62
|
/**
|
|
55
63
|
* Emit a tool-call audit log entry.
|
|
56
64
|
*
|
|
@@ -66,9 +66,9 @@ export interface ChangeArtifacts {
|
|
|
66
66
|
unavailable_reason: string | null;
|
|
67
67
|
artifact_hygiene: ArtifactHygiene;
|
|
68
68
|
}
|
|
69
|
-
export declare function captureRepoSnapshot(repoPath: string): RepoSnapshot
|
|
69
|
+
export declare function captureRepoSnapshot(repoPath: string): Promise<RepoSnapshot>;
|
|
70
70
|
export declare function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void;
|
|
71
|
-
export declare function buildChangeArtifacts(repoPath: string, before: RepoSnapshot, after: RepoSnapshot): ChangeArtifacts
|
|
71
|
+
export declare function buildChangeArtifacts(repoPath: string, before: RepoSnapshot, after: RepoSnapshot): Promise<ChangeArtifacts>;
|
|
72
72
|
export declare function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[];
|
|
73
73
|
export declare function emptyArtifactHygiene(): ArtifactHygiene;
|
|
74
74
|
export interface ExternalDirtyFile {
|
|
@@ -101,7 +101,7 @@ export interface ArtifactManifest {
|
|
|
101
101
|
generated_at: string;
|
|
102
102
|
artifacts: ArtifactManifestEntry[];
|
|
103
103
|
}
|
|
104
|
-
export declare function buildArtifactManifest(changedFiles: ChangedFile[], repoPath: string, taskId?: string): ArtifactManifest
|
|
104
|
+
export declare function buildArtifactManifest(changedFiles: ChangedFile[], repoPath: string, taskId?: string): Promise<ArtifactManifest>;
|
|
105
105
|
export interface ChangedFileGroups {
|
|
106
106
|
source_changes: ChangedFile[];
|
|
107
107
|
docs_changes: ChangedFile[];
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstatSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { createReadStream, lstatSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
5
|
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
6
|
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
7
7
|
const MAX_SNAPSHOT_FILES = 5000;
|
|
8
8
|
const MAX_DIFF_BYTES = 20 * 1024 * 1024;
|
|
9
9
|
const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
|
|
10
|
-
export function captureRepoSnapshot(repoPath) {
|
|
10
|
+
export async function captureRepoSnapshot(repoPath) {
|
|
11
11
|
const warnings = [];
|
|
12
|
-
const
|
|
12
|
+
const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
13
|
+
const isGit = isGitResult.stdout.trim() === "true";
|
|
13
14
|
let head = null;
|
|
14
15
|
let status = "";
|
|
15
16
|
let paths = [];
|
|
@@ -17,10 +18,17 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
17
18
|
const ignoredPaths = new Set();
|
|
18
19
|
const dirtyPaths = new Set();
|
|
19
20
|
if (isGit) {
|
|
20
|
-
|
|
21
|
+
// The following five git calls are independent of each other and can run in parallel.
|
|
22
|
+
const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
|
|
23
|
+
runGit(repoPath, ["rev-parse", "HEAD"]),
|
|
24
|
+
runGit(repoPath, ["status", "--porcelain=v1", "-uall"]),
|
|
25
|
+
runGit(repoPath, ["ls-files", "-z"]),
|
|
26
|
+
runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]),
|
|
27
|
+
runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]),
|
|
28
|
+
]);
|
|
21
29
|
if (headResult.status === 0)
|
|
22
30
|
head = headResult.stdout.trim() || null;
|
|
23
|
-
status =
|
|
31
|
+
status = statusResult.stdout.trimEnd();
|
|
24
32
|
// Parse git status --porcelain to collect all dirty paths
|
|
25
33
|
for (const line of status.split("\n")) {
|
|
26
34
|
if (line.length < 4)
|
|
@@ -45,23 +53,20 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (const path of tracked.stdout.split("\0").filter(Boolean))
|
|
56
|
+
if (trackedResult.status === 0) {
|
|
57
|
+
for (const path of trackedResult.stdout.split("\0").filter(Boolean))
|
|
51
58
|
trackedPaths.add(normalizePath(path));
|
|
52
59
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const path of ignored.stdout.split("\0").filter(Boolean))
|
|
60
|
+
if (ignoredResult.status === 0) {
|
|
61
|
+
for (const path of ignoredResult.stdout.split("\0").filter(Boolean))
|
|
56
62
|
ignoredPaths.add(normalizePath(path));
|
|
57
63
|
}
|
|
58
64
|
else {
|
|
59
65
|
warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
if (listed.status === 0) {
|
|
67
|
+
if (listedResult.status === 0) {
|
|
63
68
|
paths = [...new Set([
|
|
64
|
-
...
|
|
69
|
+
...listedResult.stdout.split("\0").filter(Boolean),
|
|
65
70
|
...walkWorkspace(repoPath),
|
|
66
71
|
])];
|
|
67
72
|
}
|
|
@@ -116,26 +121,26 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
116
121
|
export function writeSnapshot(taskDir, filename, snapshot) {
|
|
117
122
|
writeFileSync(join(taskDir, filename), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
118
123
|
}
|
|
119
|
-
export function buildChangeArtifacts(repoPath, before, after) {
|
|
124
|
+
export async function buildChangeArtifacts(repoPath, before, after) {
|
|
120
125
|
const changedFiles = compareSnapshots(before, after);
|
|
121
126
|
const artifactHygiene = classifyArtifactHygiene(changedFiles);
|
|
122
127
|
const sections = [];
|
|
123
128
|
const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
|
|
124
129
|
if (before.is_git && after.is_git && scopedPaths.length > 0) {
|
|
125
130
|
if (before.head && after.head && before.head !== after.head) {
|
|
126
|
-
const committed = runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
|
|
131
|
+
const committed = await runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
|
|
127
132
|
if (committed.stdout.trim())
|
|
128
133
|
sections.push("# Changes committed during task\n", committed.stdout.trimEnd());
|
|
129
134
|
}
|
|
130
135
|
const base = after.head || "HEAD";
|
|
131
|
-
const working = runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
|
|
136
|
+
const working = await runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
|
|
132
137
|
if (working.stdout.trim())
|
|
133
138
|
sections.push("# Staged and unstaged changes\n", working.stdout.trimEnd());
|
|
134
139
|
for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
|
|
135
|
-
const tracked = runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
|
|
140
|
+
const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
|
|
136
141
|
if (tracked.status === 0)
|
|
137
142
|
continue;
|
|
138
|
-
const untracked = runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
|
|
143
|
+
const untracked = await runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
|
|
139
144
|
if (untracked.stdout.trim())
|
|
140
145
|
sections.push("# Untracked file\n", untracked.stdout.trimEnd());
|
|
141
146
|
}
|
|
@@ -151,7 +156,7 @@ export function buildChangeArtifacts(repoPath, before, after) {
|
|
|
151
156
|
const fullDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
|
|
152
157
|
const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
153
158
|
const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
154
|
-
const fileStats = buildFileStats(repoPath, before, after, changedFiles);
|
|
159
|
+
const fileStats = await buildFileStats(repoPath, before, after, changedFiles);
|
|
155
160
|
return {
|
|
156
161
|
changed_files: changedFiles,
|
|
157
162
|
diff: fullDiff,
|
|
@@ -172,8 +177,8 @@ export function buildChangeArtifacts(repoPath, before, after) {
|
|
|
172
177
|
artifact_hygiene: artifactHygiene,
|
|
173
178
|
};
|
|
174
179
|
}
|
|
175
|
-
function buildFileStats(repoPath, before, after, changedFiles) {
|
|
176
|
-
|
|
180
|
+
async function buildFileStats(repoPath, before, after, changedFiles) {
|
|
181
|
+
const results = await Promise.all(changedFiles.map(async (file) => {
|
|
177
182
|
let additions = 0;
|
|
178
183
|
let deletions = 0;
|
|
179
184
|
const paths = file.old_path ? [file.old_path, file.path] : [file.path];
|
|
@@ -183,8 +188,8 @@ function buildFileStats(repoPath, before, after, changedFiles) {
|
|
|
183
188
|
ranges.push([before.head, after.head]);
|
|
184
189
|
}
|
|
185
190
|
ranges.push([after.head || "HEAD"]);
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
const rangeResults = await Promise.all(ranges.map((range) => runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths])));
|
|
192
|
+
for (const result of rangeResults) {
|
|
188
193
|
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
189
194
|
const [added, removed] = line.split(/\s+/);
|
|
190
195
|
if (/^\d+$/.test(added))
|
|
@@ -199,10 +204,11 @@ function buildFileStats(repoPath, before, after, changedFiles) {
|
|
|
199
204
|
const content = readFileSync(resolve(repoPath, file.path), "utf-8");
|
|
200
205
|
additions = countLines(content);
|
|
201
206
|
}
|
|
202
|
-
catch { }
|
|
207
|
+
catch { } // probe failure handled by return value (additions stays 0)
|
|
203
208
|
}
|
|
204
209
|
return { path: file.path, status: file.change, additions, deletions };
|
|
205
|
-
});
|
|
210
|
+
}));
|
|
211
|
+
return results;
|
|
206
212
|
}
|
|
207
213
|
function countLines(content) {
|
|
208
214
|
if (content.length === 0)
|
|
@@ -318,7 +324,7 @@ export function findNewExternalDirtyFiles(baseline, current) {
|
|
|
318
324
|
return false;
|
|
319
325
|
});
|
|
320
326
|
}
|
|
321
|
-
export function buildArtifactManifest(changedFiles, repoPath, taskId) {
|
|
327
|
+
export async function buildArtifactManifest(changedFiles, repoPath, taskId) {
|
|
322
328
|
const entries = [];
|
|
323
329
|
for (const file of changedFiles) {
|
|
324
330
|
if (file.kind !== "build_artifact")
|
|
@@ -331,7 +337,7 @@ export function buildArtifactManifest(changedFiles, repoPath, taskId) {
|
|
|
331
337
|
if (stat.isFile()) {
|
|
332
338
|
size = stat.size;
|
|
333
339
|
if (size <= MAX_HASH_BYTES) {
|
|
334
|
-
sha256 =
|
|
340
|
+
sha256 = await hashFileAsync(absolutePath);
|
|
335
341
|
}
|
|
336
342
|
}
|
|
337
343
|
}
|
|
@@ -503,15 +509,33 @@ function walkWorkspace(root) {
|
|
|
503
509
|
return result;
|
|
504
510
|
}
|
|
505
511
|
function runGit(repoPath, args) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
512
|
+
return new Promise((resolve) => {
|
|
513
|
+
execFile("git", args, {
|
|
514
|
+
cwd: repoPath,
|
|
515
|
+
encoding: "utf-8",
|
|
516
|
+
timeout: 30_000,
|
|
517
|
+
maxBuffer: MAX_DIFF_BYTES,
|
|
518
|
+
}, (err, stdout, stderr) => {
|
|
519
|
+
let status;
|
|
520
|
+
if (err) {
|
|
521
|
+
// err.code is the exit code (number) when the process exited with a non-zero status;
|
|
522
|
+
// it is a string (e.g. "ENOENT") when spawning failed or the buffer limit was hit.
|
|
523
|
+
status = typeof err.code === "number" ? err.code : null;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
status = 0;
|
|
527
|
+
}
|
|
528
|
+
resolve({ status, stdout: stdout || "", stderr: stderr || "" });
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function hashFileAsync(filePath) {
|
|
533
|
+
return new Promise((resolve, reject) => {
|
|
534
|
+
const hash = createHash("sha256");
|
|
535
|
+
const stream = createReadStream(filePath);
|
|
536
|
+
stream.on("error", reject);
|
|
537
|
+
hash.on("error", reject);
|
|
538
|
+
hash.on("finish", () => resolve(hash.digest("hex")));
|
|
539
|
+
stream.pipe(hash);
|
|
511
540
|
});
|
|
512
|
-
return {
|
|
513
|
-
status: result.status,
|
|
514
|
-
stdout: result.stdout || "",
|
|
515
|
-
stderr: result.stderr || "",
|
|
516
|
-
};
|
|
517
541
|
}
|
package/dist/runner/cli.js
CHANGED
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { runTask } from "./runTask.js";
|
|
10
10
|
import { loadConfig } from "../config.js";
|
|
11
|
+
import { logger } from "../logging.js";
|
|
11
12
|
// Load config early
|
|
12
13
|
loadConfig();
|
|
13
14
|
const taskId = process.argv[2] || process.env.PATCHWARDEN_TASK_ID;
|
|
14
15
|
if (!taskId) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
logger.info("Usage: node dist/runner/cli.js <task_id>");
|
|
17
|
+
logger.info(" or: npm run runner -- <task_id>");
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
logger.info(`[runner] Starting task: ${taskId}`);
|
|
20
21
|
const result = await runTask(taskId);
|
|
21
|
-
|
|
22
|
+
logger.info(`[runner] Task ${result.task_id}: ${result.status}`);
|
|
22
23
|
if (result.error) {
|
|
23
|
-
|
|
24
|
+
logger.fatal(`[runner] Error: ${result.error}`);
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
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
|
+
const isWindows = platform() === "win32";
|
|
6
|
+
/**
|
|
7
|
+
* Remove a file or directory, working around Node.js fs.rmSync issues on Windows
|
|
8
|
+
* where recursive directory deletion silently fails (Node v22+).
|
|
9
|
+
* Falls back to `rmdir /s /q` on Windows if rmSync doesn't remove the path.
|
|
10
|
+
*/
|
|
11
|
+
function safeRemoveSync(targetPath) {
|
|
12
|
+
rmSync(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
13
|
+
if (isWindows && existsSync(targetPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const stat = statSync(targetPath);
|
|
16
|
+
if (stat.isDirectory()) {
|
|
17
|
+
execFileSync("cmd", ["/c", "rmdir", "/s", "/q", targetPath], { windowsHide: true });
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
execFileSync("cmd", ["/c", "del", "/f", "/q", targetPath], { windowsHide: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Best effort — caller will verify via existsSync if needed
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
|
|
5
29
|
const CLEANUP_DIRS = new Set(["__pycache__", "dist", "release_packages", "temp_files"]);
|
|
6
30
|
const CLEANUP_FILES = [/\.pyc$/i];
|
|
@@ -28,7 +52,7 @@ export function runPostTaskCleanup(repoPath, taskDir) {
|
|
|
28
52
|
continue;
|
|
29
53
|
}
|
|
30
54
|
try {
|
|
31
|
-
|
|
55
|
+
safeRemoveSync(candidate.path);
|
|
32
56
|
report.removed.push({ path: rel, reason: candidate.reason });
|
|
33
57
|
}
|
|
34
58
|
catch (error) {
|