patchwarden 1.1.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 +83 -8
- package/README.md +78 -10
- 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 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- 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/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -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 +261 -255
- 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.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- 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/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- 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 +45 -0
- package/dist/tools/evidencePack.js +375 -0
- 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/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- 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/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -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 +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -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 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- 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/policy/projectPolicy.ts +344 -0
- 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 +254 -252
- 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 +251 -0
- 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/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- 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 +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- 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
|
*
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface ProjectPolicy {
|
|
2
|
+
allowed_commands: string[];
|
|
3
|
+
auto_cleanup: {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
patterns: string[];
|
|
6
|
+
exclude: string[];
|
|
7
|
+
};
|
|
8
|
+
high_risk_commands: string[];
|
|
9
|
+
protected_paths: string[];
|
|
10
|
+
release_mode: {
|
|
11
|
+
version_source: string;
|
|
12
|
+
required_commands: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ProjectPolicyIssue {
|
|
16
|
+
code: string;
|
|
17
|
+
severity: "error" | "warn";
|
|
18
|
+
field: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ReleaseReadinessSummary {
|
|
22
|
+
version_source: string;
|
|
23
|
+
version: string | null;
|
|
24
|
+
package_json_version: string | null;
|
|
25
|
+
package_name: string | null;
|
|
26
|
+
version_consistent: boolean | null;
|
|
27
|
+
required_commands: Array<{
|
|
28
|
+
command: string;
|
|
29
|
+
allowed: boolean;
|
|
30
|
+
reason: string | null;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export interface ProjectPolicySummary {
|
|
34
|
+
repo_path: string;
|
|
35
|
+
resolved_repo_path: string;
|
|
36
|
+
policy_path: string;
|
|
37
|
+
exists: boolean;
|
|
38
|
+
valid: boolean;
|
|
39
|
+
effective_policy: ProjectPolicy;
|
|
40
|
+
issues: ProjectPolicyIssue[];
|
|
41
|
+
release_readiness: ReleaseReadinessSummary;
|
|
42
|
+
}
|
|
43
|
+
export declare function getProjectPolicySummary(repoPathInput: string): ProjectPolicySummary;
|
|
44
|
+
export declare function getDefaultProjectPolicy(): ProjectPolicy;
|
|
45
|
+
export declare function commandAllowedByProjectPolicy(command: string, summary: ProjectPolicySummary): {
|
|
46
|
+
allowed: boolean;
|
|
47
|
+
reason: string | null;
|
|
48
|
+
};
|
|
49
|
+
export declare function isProtectedByProjectPolicy(relPath: string, policy: ProjectPolicy): boolean;
|
|
50
|
+
export declare function resolveVersionFromPolicy(repoPath: string, policy: ProjectPolicy): string | null;
|
|
51
|
+
export declare function readPackageJson(repoPath: string): {
|
|
52
|
+
name: string | null;
|
|
53
|
+
version: string | null;
|
|
54
|
+
githubRepo: string | null;
|
|
55
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { getConfig, } from "../config.js";
|
|
4
|
+
import { guardTestCommand } from "../security/commandGuard.js";
|
|
5
|
+
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
6
|
+
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
7
|
+
const DEFAULT_POLICY = {
|
|
8
|
+
allowed_commands: [],
|
|
9
|
+
auto_cleanup: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
patterns: ["release-artifact-manifest.json", "frontend/dist", "release_packages"],
|
|
12
|
+
exclude: [".git", ".patchwarden", "node_modules", "docs", "samples"],
|
|
13
|
+
},
|
|
14
|
+
high_risk_commands: ["npm publish", "git push", "git tag", "gh release create"],
|
|
15
|
+
protected_paths: [".env", ".env.*", ".ssh", ".npmrc", ".pypirc", "patchwarden.config.json"],
|
|
16
|
+
release_mode: {
|
|
17
|
+
version_source: "package.json",
|
|
18
|
+
required_commands: ["npm run build", "npm test"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const DANGEROUS_COMMAND_RE = /\b(?:publish|push|tag|release\s+create|deploy)\b/i;
|
|
22
|
+
const DANGEROUS_PATTERN_RE = /(^|[\\/])(?:\.git|node_modules)([\\/]|$)|^\*\*$|^\/|^[A-Za-z]:[\\/]/i;
|
|
23
|
+
export function getProjectPolicySummary(repoPathInput) {
|
|
24
|
+
const config = getConfig();
|
|
25
|
+
const repoPath = guardWorkspacePath(repoPathInput, config.workspaceRoot);
|
|
26
|
+
const policyPath = join(repoPath, ".patchwarden", "project-policy.json");
|
|
27
|
+
const issues = [];
|
|
28
|
+
let rawPolicy = {};
|
|
29
|
+
let exists = false;
|
|
30
|
+
if (existsSync(policyPath)) {
|
|
31
|
+
exists = true;
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(policyPath, "utf-8").replace(/^\uFEFF/, "");
|
|
34
|
+
rawPolicy = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
issues.push({
|
|
38
|
+
code: "policy_json_invalid",
|
|
39
|
+
severity: "error",
|
|
40
|
+
field: ".patchwarden/project-policy.json",
|
|
41
|
+
message: `Project policy is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const effective = normalizePolicy(rawPolicy, issues);
|
|
46
|
+
validateProjectPolicy(repoPath, effective, issues, config);
|
|
47
|
+
return {
|
|
48
|
+
repo_path: repoPathInput,
|
|
49
|
+
resolved_repo_path: repoPath,
|
|
50
|
+
policy_path: ".patchwarden/project-policy.json",
|
|
51
|
+
exists,
|
|
52
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
53
|
+
effective_policy: effective,
|
|
54
|
+
issues,
|
|
55
|
+
release_readiness: buildReleaseReadiness(repoPath, effective, config),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function getDefaultProjectPolicy() {
|
|
59
|
+
return clonePolicy(DEFAULT_POLICY);
|
|
60
|
+
}
|
|
61
|
+
export function commandAllowedByProjectPolicy(command, summary) {
|
|
62
|
+
const trimmed = command.trim();
|
|
63
|
+
if (!trimmed)
|
|
64
|
+
return { allowed: false, reason: "empty_command" };
|
|
65
|
+
if (summary.effective_policy.high_risk_commands.includes(trimmed) || DANGEROUS_COMMAND_RE.test(trimmed)) {
|
|
66
|
+
return { allowed: false, reason: "high_risk_command" };
|
|
67
|
+
}
|
|
68
|
+
if (summary.effective_policy.allowed_commands.length > 0 &&
|
|
69
|
+
!summary.effective_policy.allowed_commands.includes(trimmed)) {
|
|
70
|
+
return { allowed: false, reason: "not_in_project_policy_allowed_commands" };
|
|
71
|
+
}
|
|
72
|
+
const readiness = summary.release_readiness.required_commands.find((entry) => entry.command === trimmed);
|
|
73
|
+
if (readiness && !readiness.allowed) {
|
|
74
|
+
return { allowed: false, reason: readiness.reason || "not_allowlisted_by_patchwarden_config" };
|
|
75
|
+
}
|
|
76
|
+
return { allowed: true, reason: null };
|
|
77
|
+
}
|
|
78
|
+
export function isProtectedByProjectPolicy(relPath, policy) {
|
|
79
|
+
const normalized = normalizeRelPath(relPath);
|
|
80
|
+
if (!normalized)
|
|
81
|
+
return true;
|
|
82
|
+
if (isSensitivePath(normalized))
|
|
83
|
+
return true;
|
|
84
|
+
return policy.protected_paths.some((pattern) => pathMatchesPattern(normalized, pattern));
|
|
85
|
+
}
|
|
86
|
+
export function resolveVersionFromPolicy(repoPath, policy) {
|
|
87
|
+
const source = policy.release_mode.version_source || "package.json";
|
|
88
|
+
if (source === "package.json")
|
|
89
|
+
return readPackageJson(repoPath).version;
|
|
90
|
+
if (source === "src/version.ts") {
|
|
91
|
+
const versionFile = safeRepoFile(repoPath, "src/version.ts");
|
|
92
|
+
if (!versionFile)
|
|
93
|
+
return null;
|
|
94
|
+
const text = readFileSync(versionFile, "utf-8");
|
|
95
|
+
const match = text.match(/PATCHWARDEN_VERSION\s*=\s*["']([^"']+)["']/);
|
|
96
|
+
return match ? match[1] : null;
|
|
97
|
+
}
|
|
98
|
+
const file = safeRepoFile(repoPath, source);
|
|
99
|
+
if (!file)
|
|
100
|
+
return null;
|
|
101
|
+
const text = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").trim();
|
|
102
|
+
return text.split(/\r?\n/)[0]?.trim() || null;
|
|
103
|
+
}
|
|
104
|
+
export function readPackageJson(repoPath) {
|
|
105
|
+
const packagePath = join(repoPath, "package.json");
|
|
106
|
+
if (!existsSync(packagePath))
|
|
107
|
+
return { name: null, version: null, githubRepo: null };
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(readFileSync(packagePath, "utf-8").replace(/^\uFEFF/, ""));
|
|
110
|
+
return {
|
|
111
|
+
name: typeof data.name === "string" ? data.name : null,
|
|
112
|
+
version: typeof data.version === "string" ? data.version : null,
|
|
113
|
+
githubRepo: parseGithubRepo(data.repository),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return { name: null, version: null, githubRepo: null };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function normalizePolicy(raw, issues) {
|
|
121
|
+
const policy = clonePolicy(DEFAULT_POLICY);
|
|
122
|
+
const object = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
123
|
+
policy.allowed_commands = stringArrayOrDefault(object.allowed_commands, policy.allowed_commands, "allowed_commands", issues);
|
|
124
|
+
policy.high_risk_commands = stringArrayOrDefault(object.high_risk_commands, policy.high_risk_commands, "high_risk_commands", issues);
|
|
125
|
+
policy.protected_paths = stringArrayOrDefault(object.protected_paths, policy.protected_paths, "protected_paths", issues);
|
|
126
|
+
if (object.auto_cleanup && typeof object.auto_cleanup === "object" && !Array.isArray(object.auto_cleanup)) {
|
|
127
|
+
const cleanup = object.auto_cleanup;
|
|
128
|
+
if (cleanup.enabled !== undefined && typeof cleanup.enabled !== "boolean") {
|
|
129
|
+
issues.push({ code: "invalid_type", severity: "error", field: "auto_cleanup.enabled", message: "auto_cleanup.enabled must be a boolean." });
|
|
130
|
+
}
|
|
131
|
+
else if (cleanup.enabled !== undefined) {
|
|
132
|
+
policy.auto_cleanup.enabled = cleanup.enabled;
|
|
133
|
+
}
|
|
134
|
+
policy.auto_cleanup.patterns = stringArrayOrDefault(cleanup.patterns, policy.auto_cleanup.patterns, "auto_cleanup.patterns", issues);
|
|
135
|
+
policy.auto_cleanup.exclude = stringArrayOrDefault(cleanup.exclude, policy.auto_cleanup.exclude, "auto_cleanup.exclude", issues);
|
|
136
|
+
}
|
|
137
|
+
if (object.release_mode && typeof object.release_mode === "object" && !Array.isArray(object.release_mode)) {
|
|
138
|
+
const releaseMode = object.release_mode;
|
|
139
|
+
if (releaseMode.version_source !== undefined && typeof releaseMode.version_source !== "string") {
|
|
140
|
+
issues.push({ code: "invalid_type", severity: "error", field: "release_mode.version_source", message: "release_mode.version_source must be a string." });
|
|
141
|
+
}
|
|
142
|
+
else if (releaseMode.version_source) {
|
|
143
|
+
policy.release_mode.version_source = releaseMode.version_source.trim();
|
|
144
|
+
}
|
|
145
|
+
policy.release_mode.required_commands = stringArrayOrDefault(releaseMode.required_commands, policy.release_mode.required_commands, "release_mode.required_commands", issues);
|
|
146
|
+
}
|
|
147
|
+
return policy;
|
|
148
|
+
}
|
|
149
|
+
function validateProjectPolicy(repoPath, policy, issues, config) {
|
|
150
|
+
for (const [field, commands] of [
|
|
151
|
+
["allowed_commands", policy.allowed_commands],
|
|
152
|
+
["high_risk_commands", policy.high_risk_commands],
|
|
153
|
+
["release_mode.required_commands", policy.release_mode.required_commands],
|
|
154
|
+
]) {
|
|
155
|
+
for (const command of commands) {
|
|
156
|
+
if (field !== "high_risk_commands" && DANGEROUS_COMMAND_RE.test(command)) {
|
|
157
|
+
issues.push({ code: "high_risk_command", severity: "error", field, message: `Command is release/destructive risk and cannot be auto-executed: ${command}` });
|
|
158
|
+
}
|
|
159
|
+
if (field !== "high_risk_commands") {
|
|
160
|
+
try {
|
|
161
|
+
guardTestCommand(command, config, repoPath);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
issues.push({ code: "command_not_allowlisted", severity: "warn", field, message: `Command is not allowed by existing PatchWarden config: ${command}` });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const field of ["auto_cleanup.patterns", "auto_cleanup.exclude", "protected_paths"]) {
|
|
170
|
+
const values = field === "auto_cleanup.patterns"
|
|
171
|
+
? policy.auto_cleanup.patterns
|
|
172
|
+
: field === "auto_cleanup.exclude"
|
|
173
|
+
? policy.auto_cleanup.exclude
|
|
174
|
+
: policy.protected_paths;
|
|
175
|
+
for (const value of values) {
|
|
176
|
+
if (!value.trim()) {
|
|
177
|
+
issues.push({ code: "empty_path_pattern", severity: "error", field, message: "Path patterns must be non-empty." });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const blocksCriticalDirectory = field === "auto_cleanup.patterns" && DANGEROUS_PATTERN_RE.test(value);
|
|
181
|
+
if (value.includes("\0") || value.includes("..") || isAbsolute(value) || blocksCriticalDirectory) {
|
|
182
|
+
issues.push({ code: "unsafe_path_pattern", severity: "error", field, message: `Unsafe path pattern rejected: ${value}` });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const versionSource = policy.release_mode.version_source;
|
|
187
|
+
if (versionSource && versionSource !== "package.json" && versionSource !== "src/version.ts") {
|
|
188
|
+
const resolved = safeRepoFile(repoPath, versionSource);
|
|
189
|
+
if (!resolved) {
|
|
190
|
+
issues.push({ code: "version_source_invalid", severity: "error", field: "release_mode.version_source", message: `Version source is outside repo, sensitive, or missing: ${versionSource}` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function buildReleaseReadiness(repoPath, policy, config) {
|
|
195
|
+
const packageJson = readPackageJson(repoPath);
|
|
196
|
+
let version = null;
|
|
197
|
+
try {
|
|
198
|
+
version = resolveVersionFromPolicy(repoPath, policy);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
version = null;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
version_source: policy.release_mode.version_source,
|
|
205
|
+
version,
|
|
206
|
+
package_json_version: packageJson.version,
|
|
207
|
+
package_name: packageJson.name,
|
|
208
|
+
version_consistent: version && packageJson.version ? version === packageJson.version : null,
|
|
209
|
+
required_commands: policy.release_mode.required_commands.map((command) => {
|
|
210
|
+
try {
|
|
211
|
+
guardTestCommand(command, config, repoPath);
|
|
212
|
+
return { command, allowed: true, reason: null };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return { command, allowed: false, reason: err instanceof Error ? err.message : String(err) };
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function safeRepoFile(repoPath, relPath) {
|
|
221
|
+
const normalized = normalizeRelPath(relPath);
|
|
222
|
+
if (!normalized || isSensitivePath(normalized))
|
|
223
|
+
return null;
|
|
224
|
+
const candidate = resolve(repoPath, normalized);
|
|
225
|
+
const rel = relative(repoPath, candidate);
|
|
226
|
+
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`))
|
|
227
|
+
return null;
|
|
228
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile())
|
|
229
|
+
return null;
|
|
230
|
+
const parent = dirname(candidate);
|
|
231
|
+
const parentRel = relative(repoPath, parent);
|
|
232
|
+
if (parentRel === ".." || parentRel.startsWith(`..${sep}`))
|
|
233
|
+
return null;
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
function stringArrayOrDefault(value, fallback, field, issues) {
|
|
237
|
+
if (value === undefined)
|
|
238
|
+
return [...fallback];
|
|
239
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
240
|
+
issues.push({ code: "invalid_type", severity: "error", field, message: `${field} must be an array of strings.` });
|
|
241
|
+
return [...fallback];
|
|
242
|
+
}
|
|
243
|
+
return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
|
|
244
|
+
}
|
|
245
|
+
function clonePolicy(policy) {
|
|
246
|
+
return {
|
|
247
|
+
allowed_commands: [...policy.allowed_commands],
|
|
248
|
+
auto_cleanup: {
|
|
249
|
+
enabled: policy.auto_cleanup.enabled,
|
|
250
|
+
patterns: [...policy.auto_cleanup.patterns],
|
|
251
|
+
exclude: [...policy.auto_cleanup.exclude],
|
|
252
|
+
},
|
|
253
|
+
high_risk_commands: [...policy.high_risk_commands],
|
|
254
|
+
protected_paths: [...policy.protected_paths],
|
|
255
|
+
release_mode: {
|
|
256
|
+
version_source: policy.release_mode.version_source,
|
|
257
|
+
required_commands: [...policy.release_mode.required_commands],
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function normalizeRelPath(value) {
|
|
262
|
+
return value.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
263
|
+
}
|
|
264
|
+
function pathMatchesPattern(relPath, pattern) {
|
|
265
|
+
const normalizedPattern = normalizeRelPath(pattern);
|
|
266
|
+
if (!normalizedPattern)
|
|
267
|
+
return false;
|
|
268
|
+
if (normalizedPattern.includes("*")) {
|
|
269
|
+
const escaped = normalizedPattern
|
|
270
|
+
.split("*")
|
|
271
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
272
|
+
.join("[^/]*");
|
|
273
|
+
return new RegExp(`^${escaped}$`, "i").test(relPath);
|
|
274
|
+
}
|
|
275
|
+
return relPath.toLowerCase() === normalizedPattern.toLowerCase() ||
|
|
276
|
+
relPath.toLowerCase().startsWith(`${normalizedPattern.toLowerCase()}/`);
|
|
277
|
+
}
|
|
278
|
+
function parseGithubRepo(repository) {
|
|
279
|
+
const raw = typeof repository === "string"
|
|
280
|
+
? repository
|
|
281
|
+
: repository && typeof repository === "object" && typeof repository.url === "string"
|
|
282
|
+
? repository.url
|
|
283
|
+
: "";
|
|
284
|
+
const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
|
|
285
|
+
return match ? match[1] : null;
|
|
286
|
+
}
|
|
@@ -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.");
|