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
@@ -0,0 +1,380 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { join, normalize } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { reloadConfig } from "../../config.js";
7
+ import { runTaskLoopWithDeps } from "../../tools/runTaskLoop.js";
8
+ import { getTaskLineage, writeTaskLineage } from "../../tools/taskLineage.js";
9
+ let tempDir;
10
+ let prevConfigEnv;
11
+ function writeConfig() {
12
+ const configPath = join(tempDir, "patchwarden.config.json");
13
+ writeFileSync(configPath, JSON.stringify({
14
+ workspaceRoot: tempDir,
15
+ tasksDir: ".patchwarden/tasks",
16
+ plansDir: ".patchwarden/plans",
17
+ assessmentsDir: ".patchwarden/assessments",
18
+ agents: {
19
+ fake: { command: "fake-agent", args: [] },
20
+ },
21
+ allowedTestCommands: ["npm test", "npm run build"],
22
+ directAllowedCommands: ["npm test", "npm run build"],
23
+ defaultTaskTimeoutSeconds: 30,
24
+ maxTaskTimeoutSeconds: 120,
25
+ enableDirectProfile: true,
26
+ }), "utf-8");
27
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
28
+ process.env.PATCHWARDEN_CONFIG = configPath;
29
+ reloadConfig();
30
+ }
31
+ function depsFor(options) {
32
+ const decisions = [...(options.decisions || ["allow"])];
33
+ const tasks = [...(options.tasks || ["task-main"])];
34
+ const calls = [];
35
+ const deps = {
36
+ createTask: ((input) => {
37
+ calls.push(input.execution_mode === "assess_only" ? "assess" : "execute");
38
+ if (input.execution_mode === "assess_only") {
39
+ const decision = decisions.shift() || "allow";
40
+ return {
41
+ assessment_id: `assessment-${calls.length}`,
42
+ decision,
43
+ reason_codes: decision === "allow" ? ["repo_scoped"] : ["release_template_needs_confirm"],
44
+ };
45
+ }
46
+ return { task_id: tasks.shift() || `task-${calls.length}`, status: "pending" };
47
+ }),
48
+ waitForTask: (async (taskId) => ({
49
+ task_id: taskId,
50
+ status: options.statuses?.[taskId] || "done_by_agent",
51
+ phase: options.statuses?.[taskId] || "done_by_agent",
52
+ terminal: true,
53
+ continuation_required: false,
54
+ next_action: "safe_audit",
55
+ })),
56
+ safeResult: ((taskId) => ({
57
+ task_id: taskId,
58
+ status: options.statuses?.[taskId] || "done_by_agent",
59
+ terminal: true,
60
+ verification: { status: options.verifications?.[taskId] || "passed" },
61
+ next_action: "audit_or_accept",
62
+ })),
63
+ safeTestSummary: ((taskId) => ({
64
+ task_id: taskId,
65
+ status: options.verifications?.[taskId] || "passed",
66
+ commands: [{ command: "npm test", status: options.verifications?.[taskId] || "passed", exit_code: 0 }],
67
+ })),
68
+ safeAudit: ((taskId) => {
69
+ const audit = options.audits?.[taskId] || { verdict: "pass" };
70
+ return {
71
+ task_id: taskId,
72
+ verdict: audit.verdict,
73
+ fail_checks: (audit.fail || []).map((name) => ({ name, result: "fail" })),
74
+ warn_checks: (audit.warn || []).map((name) => ({ name, result: "warn" })),
75
+ recommended_next_actions: ["accept"],
76
+ };
77
+ }),
78
+ createDirectSession: ((input) => ({
79
+ session_id: "direct-test",
80
+ repo_path: input.repo_path,
81
+ resolved_repo_path: tempDir,
82
+ workspace_clean: true,
83
+ allowed_commands: ["npm test", "npm run build"],
84
+ expires_at: "2026-07-04T13:00:00.000Z",
85
+ next_action: "run_verification",
86
+ })),
87
+ runDirectVerificationBundle: (async () => ({
88
+ session_id: "direct-test",
89
+ status: options.directBundleStatus || "passed",
90
+ command_count: 1,
91
+ passed_commands: (options.directBundleStatus || "passed") === "passed" ? 1 : 0,
92
+ failed_commands: (options.directBundleStatus || "passed") === "passed" ? 0 : 1,
93
+ timed_out_commands: 0,
94
+ commands: [{
95
+ command: "npm test",
96
+ passed: (options.directBundleStatus || "passed") === "passed",
97
+ exit_code: (options.directBundleStatus || "passed") === "passed" ? 0 : 1,
98
+ timed_out: false,
99
+ redacted: false,
100
+ redaction_categories: [],
101
+ started_at: "2026-07-04T12:00:00.000Z",
102
+ finished_at: "2026-07-04T12:00:01.000Z",
103
+ }],
104
+ large_logs_omitted: true,
105
+ next_action: "safe_finalize_direct_session",
106
+ })),
107
+ safeFinalizeDirectSession: (() => ({
108
+ session_id: "direct-test",
109
+ finalized: true,
110
+ changed_files_total: 0,
111
+ next_action: "safe_audit_direct_session",
112
+ })),
113
+ safeAuditDirectSession: (() => ({
114
+ session_id: "direct-test",
115
+ decision: options.directAuditDecision || "pass",
116
+ reason_codes: [],
117
+ blocking_findings: [],
118
+ warnings: [],
119
+ evidence: { changed_files_total: 0, verification_runs: [] },
120
+ next_action: "accept",
121
+ })),
122
+ writeTaskLineage,
123
+ createLineageId: (() => "lineage_20260704_test"),
124
+ recommendAgentForTask: ((input) => ({
125
+ repo_path: input.repo_path,
126
+ resolved_repo_path: tempDir,
127
+ recommended_agent: "fake",
128
+ fallback_agent: null,
129
+ fallback: false,
130
+ reason: "test route",
131
+ risk_notes: [],
132
+ suggested_verify_commands: ["npm test"],
133
+ bounded: true,
134
+ })),
135
+ createWorktree: ((goalId, subgoalId, workspaceRoot) => {
136
+ calls.push(`worktree:${workspaceRoot}`);
137
+ return {
138
+ worktreeId: "wt-test",
139
+ worktreePath: tempDir,
140
+ branch: "pw-test",
141
+ };
142
+ }),
143
+ now: () => new Date("2026-07-04T12:00:00.000Z"),
144
+ sleep: async () => { },
145
+ };
146
+ return { deps, calls };
147
+ }
148
+ describe("runTaskLoop", () => {
149
+ beforeEach(() => {
150
+ tempDir = mkdtempSync(join(tmpdir(), "pw-loop-"));
151
+ writeConfig();
152
+ });
153
+ afterEach(() => {
154
+ if (prevConfigEnv === undefined)
155
+ delete process.env.PATCHWARDEN_CONFIG;
156
+ else
157
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
158
+ reloadConfig();
159
+ rmSync(tempDir, { recursive: true, force: true });
160
+ });
161
+ it("returns success with a bounded lineage summary", async () => {
162
+ const { deps } = depsFor({});
163
+ const result = await runTaskLoopWithDeps({
164
+ repo_path: ".",
165
+ goal: "Make a safe change",
166
+ agent: "fake",
167
+ verify_commands: ["npm test"],
168
+ }, deps);
169
+ assert.equal(result.stop_reason, "success");
170
+ assert.equal(result.final_status, "accepted");
171
+ assert.equal(result.tasks.main, "task-main");
172
+ const payload = JSON.stringify(result);
173
+ assert.ok(!payload.includes("stdout"));
174
+ assert.ok(!payload.includes("stderr"));
175
+ assert.ok(!payload.includes("diff.patch"));
176
+ });
177
+ it("keeps v1.3 behavior when direct_verify is false", async () => {
178
+ const { deps } = depsFor({});
179
+ const result = await runTaskLoopWithDeps({
180
+ repo_path: ".",
181
+ goal: "Run without Direct",
182
+ agent: "fake",
183
+ verify_commands: ["npm test"],
184
+ direct_verify: false,
185
+ }, deps);
186
+ assert.equal(result.stop_reason, "success");
187
+ assert.equal(result.direct_verify, false);
188
+ assert.deepEqual(result.tasks.direct_sessions, []);
189
+ assert.equal(result.isolation_mode, "current_repo");
190
+ assert.equal(result.worktree.status, "not_used");
191
+ });
192
+ it("records bounded agent routing when agent is auto", async () => {
193
+ const { deps } = depsFor({});
194
+ const result = await runTaskLoopWithDeps({
195
+ repo_path: ".",
196
+ goal: "Pick a safe agent",
197
+ agent: "auto",
198
+ verify_commands: ["npm test"],
199
+ scope_files: ["src/index.ts"],
200
+ }, deps);
201
+ assert.equal(result.stop_reason, "success");
202
+ assert.equal(result.agent_routing?.requested_agent, "auto");
203
+ assert.equal(result.agent_routing?.selected_agent, "fake");
204
+ assert.equal(result.agent_routing?.reason, "test route");
205
+ });
206
+ it("uses worktree isolation for task execution and records worktree evidence", async () => {
207
+ const { deps, calls } = depsFor({});
208
+ const result = await runTaskLoopWithDeps({
209
+ repo_path: "child-repo",
210
+ goal: "Run in a worktree",
211
+ agent: "fake",
212
+ verify_commands: ["npm test"],
213
+ isolation_mode: "worktree",
214
+ worktree_cleanup: "keep",
215
+ }, deps);
216
+ assert.equal(result.stop_reason, "success");
217
+ assert.equal(result.isolation_mode, "worktree");
218
+ assert.equal(result.worktree.worktree_id, "wt-test");
219
+ assert.equal(result.worktree.branch, "pw-test");
220
+ assert.equal(result.worktree.status, "active");
221
+ assert.ok(calls.includes(`worktree:${normalize(join(tempDir, "child-repo"))}`));
222
+ });
223
+ it("records Direct verification evidence when direct_verify succeeds", async () => {
224
+ const { deps } = depsFor({});
225
+ const result = await runTaskLoopWithDeps({
226
+ repo_path: ".",
227
+ goal: "Run with Direct verification",
228
+ agent: "fake",
229
+ verify_commands: ["npm test"],
230
+ direct_verify: true,
231
+ }, deps);
232
+ assert.equal(result.stop_reason, "success");
233
+ assert.equal(result.direct_verify, true);
234
+ assert.equal(result.tasks.direct_sessions.length, 1);
235
+ assert.equal(result.tasks.direct_sessions[0].session_id, "direct-test");
236
+ assert.equal(result.tasks.direct_sessions[0].status, "passed");
237
+ assert.equal(result.tasks.direct_sessions[0].audit_decision, "pass");
238
+ const payload = JSON.stringify(result);
239
+ assert.ok(!payload.includes("stdout_tail"));
240
+ assert.ok(!payload.includes("stderr_tail"));
241
+ assert.ok(!payload.includes("diff.patch"));
242
+ });
243
+ it("stops clearly when direct_verify is requested but Direct profile is disabled", async () => {
244
+ const configPath = join(tempDir, "patchwarden.config.json");
245
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
246
+ raw.enableDirectProfile = false;
247
+ writeFileSync(configPath, JSON.stringify(raw), "utf-8");
248
+ reloadConfig();
249
+ const { deps } = depsFor({});
250
+ const result = await runTaskLoopWithDeps({
251
+ repo_path: ".",
252
+ goal: "Run with unavailable Direct",
253
+ agent: "fake",
254
+ verify_commands: ["npm test"],
255
+ direct_verify: true,
256
+ }, deps);
257
+ assert.equal(result.stop_reason, "direct_profile_disabled");
258
+ assert.equal(result.final_status, "blocked");
259
+ assert.equal(result.tasks.main, "task-main");
260
+ assert.equal(result.tasks.direct_sessions[0].session_id, "not_created");
261
+ });
262
+ it("returns direct_verification_failed without leaking Direct logs", async () => {
263
+ const { deps } = depsFor({ directBundleStatus: "failed" });
264
+ const result = await runTaskLoopWithDeps({
265
+ repo_path: ".",
266
+ goal: "Run with failing Direct verification",
267
+ agent: "fake",
268
+ verify_commands: ["npm test"],
269
+ direct_verify: true,
270
+ }, deps);
271
+ assert.equal(result.stop_reason, "direct_verification_failed");
272
+ assert.equal(result.final_status, "needs_fix");
273
+ assert.equal(result.tasks.direct_sessions[0].failed_commands, 1);
274
+ const payload = JSON.stringify(result);
275
+ assert.ok(!payload.includes("stdout_tail"));
276
+ assert.ok(!payload.includes("stderr_tail"));
277
+ });
278
+ it("returns direct_audit_failed when Direct audit fails", async () => {
279
+ const { deps } = depsFor({ directAuditDecision: "fail" });
280
+ const result = await runTaskLoopWithDeps({
281
+ repo_path: ".",
282
+ goal: "Run with failing Direct audit",
283
+ agent: "fake",
284
+ verify_commands: ["npm test"],
285
+ direct_verify: true,
286
+ }, deps);
287
+ assert.equal(result.stop_reason, "direct_audit_failed");
288
+ assert.equal(result.final_status, "blocked");
289
+ assert.equal(result.tasks.direct_sessions[0].audit_decision, "fail");
290
+ });
291
+ it("creates a fix_tests follow-up after failed verification", async () => {
292
+ const { deps } = depsFor({
293
+ tasks: ["task-main", "task-fix"],
294
+ statuses: { "task-main": "failed_verification", "task-fix": "done_by_agent" },
295
+ verifications: { "task-main": "failed", "task-fix": "passed" },
296
+ audits: {
297
+ "task-main": { verdict: "warn", warn: ["test_exit_code"] },
298
+ "task-fix": { verdict: "pass" },
299
+ },
300
+ });
301
+ const result = await runTaskLoopWithDeps({
302
+ repo_path: ".",
303
+ goal: "Repair tests",
304
+ agent: "fake",
305
+ verify_commands: ["npm test"],
306
+ max_iterations: 2,
307
+ }, deps);
308
+ assert.equal(result.stop_reason, "success");
309
+ assert.equal(result.tasks.main, "task-main");
310
+ assert.deepEqual(result.tasks.fix, ["task-fix"]);
311
+ assert.equal(result.rounds.length, 2);
312
+ });
313
+ it("stops before execution when assessment needs confirmation", async () => {
314
+ const { deps, calls } = depsFor({ decisions: ["needs_confirm"] });
315
+ const result = await runTaskLoopWithDeps({
316
+ repo_path: ".",
317
+ goal: "Release-like work",
318
+ agent: "fake",
319
+ verify_commands: ["npm test"],
320
+ }, deps);
321
+ assert.equal(result.stop_reason, "user_confirmation_required");
322
+ assert.equal(result.stopped_before_execution, true);
323
+ assert.deepEqual(calls, ["assess"]);
324
+ });
325
+ it("returns max_iterations_reached when verification keeps failing", async () => {
326
+ const { deps } = depsFor({
327
+ statuses: { "task-main": "failed_verification" },
328
+ verifications: { "task-main": "failed" },
329
+ audits: { "task-main": { verdict: "warn", warn: ["test_exit_code"] } },
330
+ });
331
+ const result = await runTaskLoopWithDeps({
332
+ repo_path: ".",
333
+ goal: "Fix tests",
334
+ agent: "fake",
335
+ verify_commands: ["npm test"],
336
+ max_iterations: 1,
337
+ }, deps);
338
+ assert.equal(result.stop_reason, "max_iterations_reached");
339
+ assert.equal(result.final_status, "needs_fix");
340
+ });
341
+ it("writes BOM-free lineage JSON readable through get_task_lineage", async () => {
342
+ const { deps } = depsFor({});
343
+ const result = await runTaskLoopWithDeps({
344
+ repo_path: ".",
345
+ goal: "Persist lineage",
346
+ agent: "fake",
347
+ verify_commands: ["npm test"],
348
+ }, deps);
349
+ const lineagePath = join(tempDir, ".patchwarden", "lineages", result.lineage_id, "lineage.json");
350
+ const raw = readFileSync(lineagePath);
351
+ assert.notEqual(raw[0], 0xef);
352
+ const parsed = JSON.parse(raw.toString("utf-8"));
353
+ assert.equal(parsed.lineage_id, result.lineage_id);
354
+ const safe = getTaskLineage(result.lineage_id);
355
+ assert.equal(safe.lineage_id, result.lineage_id);
356
+ assert.equal(safe.rounds.length, 1);
357
+ });
358
+ it("reads legacy string direct_sessions as bounded evidence", () => {
359
+ const safe = writeTaskLineage({
360
+ lineage_id: "lineage_legacy_direct",
361
+ goal: "Legacy lineage",
362
+ repo_path: ".",
363
+ created_at: "2026-07-04T12:00:00.000Z",
364
+ updated_at: "2026-07-04T12:00:00.000Z",
365
+ final_status: "accepted",
366
+ stop_reason: "success",
367
+ next_action: "accept",
368
+ main_task: "task-main",
369
+ fix_tasks: [],
370
+ cleanup_tasks: [],
371
+ direct_sessions: ["direct-old"],
372
+ rounds: [],
373
+ warnings: [],
374
+ errors: [],
375
+ });
376
+ assert.equal(safe.tasks.direct_sessions[0].session_id, "direct-old");
377
+ const readBack = getTaskLineage("lineage_legacy_direct");
378
+ assert.equal(readBack.tasks.direct_sessions[0].session_id, "direct-old");
379
+ });
380
+ });
@@ -18,6 +18,7 @@ const OTHER_SCHEMA = {
18
18
  },
19
19
  required: ["plan_id"],
20
20
  };
21
+ const CORE_COUNT = CHATGPT_CORE_TOOL_NAMES.length;
21
22
  /** 构造一个满足 PatchWardenToolMeta 接口的元数据对象。 */
22
23
  function makeMeta(opts) {
23
24
  const schema = opts.inputSchema ?? SAMPLE_SCHEMA;
@@ -100,39 +101,39 @@ describe("checkChatgptCoreManifestStable", () => {
100
101
  });
101
102
  it("缺少工具时 ok=false", () => {
102
103
  // 移除最后一个工具,使 chatgpt_core 工具数为 20
103
- const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, 20);
104
+ const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, CORE_COUNT - 1);
104
105
  const toolDefs = makeToolDefs(names.map((name) => ({ name })));
105
106
  const result = checkChatgptCoreManifestStable(toolDefs);
106
107
  assert.equal(result.ok, false);
107
108
  assert.equal(result.warnings.length, 1);
108
109
  assert.ok(result.warnings[0].includes("missing tools"), "warning 应包含 missing tools");
109
- assert.ok(result.warnings[0].includes(CHATGPT_CORE_TOOL_NAMES[20]), "warning 应包含缺失的工具名");
110
+ assert.ok(result.warnings[0].includes(CHATGPT_CORE_TOOL_NAMES[CORE_COUNT - 1]), "warning 应包含缺失的工具名");
110
111
  });
111
112
  });
112
113
  // ── 3. checkNewToolsProfileAppend ────────────────────────────────
113
114
  describe("checkNewToolsProfileAppend", () => {
114
115
  it("chatgpt_core 21 工具时 ok=true", () => {
115
- const registry = makeCoreRegistry(21);
116
+ const registry = makeCoreRegistry(CORE_COUNT);
116
117
  const result = checkNewToolsProfileAppend(registry);
117
118
  assert.equal(result.ok, true);
118
119
  assert.deepEqual(result.warnings, []);
119
120
  });
120
121
  it("chatgpt_core 工具数不为 21 时 ok=false", () => {
121
122
  // 22 个 chatgpt_core 工具:21 个标准工具 + 1 个重复名(模拟新工具追加)
122
- const registry = makeCoreRegistry(21);
123
+ const registry = makeCoreRegistry(CORE_COUNT);
123
124
  registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
124
125
  const result = checkNewToolsProfileAppend(registry);
125
126
  assert.equal(result.ok, false);
126
127
  assert.equal(result.warnings.length, 1);
127
- assert.ok(result.warnings[0].includes("expected 21"), "warning 应包含预期数量 21");
128
- assert.ok(result.warnings[0].includes("got 22"), "warning 应包含实际数量 22");
128
+ assert.ok(result.warnings[0].includes(`expected ${CORE_COUNT}`), "warning 应包含预期数量 21");
129
+ assert.ok(result.warnings[0].includes(`got ${CORE_COUNT + 1}`), "warning 应包含实际数量 22");
129
130
  });
130
131
  });
131
132
  // ── 4. runAllSchemaDriftChecks ───────────────────────────────────
132
133
  describe("runAllSchemaDriftChecks", () => {
133
134
  it("无 drift 时 ok=true", () => {
134
135
  // registry 与 toolDefs 完全一致,且 chatgpt_core 工具数为 21
135
- const registry = makeCoreRegistry(21);
136
+ const registry = makeCoreRegistry(CORE_COUNT);
136
137
  const toolDefs = makeCoreToolDefs();
137
138
  const result = runAllSchemaDriftChecks(registry, toolDefs);
138
139
  assert.equal(result.ok, true);
@@ -144,7 +145,7 @@ describe("runAllSchemaDriftChecks", () => {
144
145
  // - check 2: toolDefs 缺少一个 chatgpt_core 工具(数量 16)
145
146
  // - check 3: registry 有 22 个 chatgpt_core 工具
146
147
  const driftedSchema = OTHER_SCHEMA;
147
- const registry = makeCoreRegistry(21);
148
+ const registry = makeCoreRegistry(CORE_COUNT);
148
149
  // 第一个工具的 digest 指向 SAMPLE_SCHEMA,但 toolDefs 中该工具用 driftedSchema
149
150
  registry[0] = makeMeta({
150
151
  name: CHATGPT_CORE_TOOL_NAMES[0],
@@ -155,7 +156,7 @@ describe("runAllSchemaDriftChecks", () => {
155
156
  registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
156
157
  // toolDefs:第一个工具用 driftedSchema(触发 check 1),且移除最后一个核心工具(触发 check 2)
157
158
  const coreNames = [...CHATGPT_CORE_TOOL_NAMES];
158
- const toolDefEntries = coreNames.slice(0, 20).map((name) => ({
159
+ const toolDefEntries = coreNames.slice(0, CORE_COUNT - 1).map((name) => ({
159
160
  name,
160
161
  inputSchema: name === CHATGPT_CORE_TOOL_NAMES[0] ? driftedSchema : SAMPLE_SCHEMA,
161
162
  }));
@@ -0,0 +1 @@
1
+ export {};