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