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.
Files changed (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. 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
- console.error(`[patchwarden] Workspace: ${config.workspaceRoot}`);
16
- console.error(`[patchwarden] Transport: stdio`);
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
- console.error("[patchwarden] Fatal:", err);
22
+ logger.fatal("[patchwarden] Fatal", {
23
+ error: err instanceof Error ? err.message : String(err),
24
+ });
22
25
  process.exit(1);
23
26
  });
24
- console.error("[patchwarden] MCP server ready on stdio");
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 { spawnSync } from "node:child_process";
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 isGit = runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]).stdout.trim() === "true";
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
- const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
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 = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
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
- const tracked = runGit(repoPath, ["ls-files", "-z"]);
49
- if (tracked.status === 0) {
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
- const ignored = runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]);
54
- if (ignored.status === 0) {
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
- const listed = runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]);
62
- if (listed.status === 0) {
67
+ if (listedResult.status === 0) {
63
68
  paths = [...new Set([
64
- ...listed.stdout.split("\0").filter(Boolean),
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
- return changedFiles.map((file) => {
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
- for (const range of ranges) {
187
- const result = runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths]);
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 = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
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
- const result = spawnSync("git", args, {
507
- cwd: repoPath,
508
- encoding: "utf-8",
509
- timeout: 30_000,
510
- maxBuffer: MAX_DIFF_BYTES,
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
  }
@@ -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
- console.error("Usage: node dist/runner/cli.js <task_id>");
16
- console.error(" or: npm run runner -- <task_id>");
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
- console.error(`[runner] Starting task: ${taskId}`);
20
+ logger.info(`[runner] Starting task: ${taskId}`);
20
21
  const result = await runTask(taskId);
21
- console.error(`[runner] Task ${result.task_id}: ${result.status}`);
22
+ logger.info(`[runner] Task ${result.task_id}: ${result.status}`);
22
23
  if (result.error) {
23
- console.error(`[runner] Error: ${result.error}`);
24
+ logger.fatal(`[runner] Error: ${result.error}`);
24
25
  process.exit(1);
25
26
  }
26
- console.error("[runner] Done.");
27
+ logger.info("[runner] Done.");