patchwarden 0.6.4 → 1.5.0

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 (254) hide show
  1. package/README.en.md +54 -20
  2. package/README.md +48 -21
  3. package/dist/agents/agentRouter.d.ts +40 -0
  4. package/dist/agents/agentRouter.js +95 -0
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -2
  7. package/dist/controlCenter.js +212 -3
  8. package/dist/direct/directSessionStore.d.ts +2 -0
  9. package/dist/direct/directVerification.js +7 -0
  10. package/dist/doctor.js +32 -1
  11. package/dist/goal/acceptanceEngine.d.ts +62 -0
  12. package/dist/goal/acceptanceEngine.js +103 -0
  13. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  14. package/dist/goal/acceptanceTemplate.js +104 -0
  15. package/dist/goal/goalGraph.d.ts +58 -0
  16. package/dist/goal/goalGraph.js +189 -0
  17. package/dist/goal/goalProgress.d.ts +81 -0
  18. package/dist/goal/goalProgress.js +216 -0
  19. package/dist/goal/goalStatus.d.ts +60 -0
  20. package/dist/goal/goalStatus.js +137 -0
  21. package/dist/goal/goalStore.d.ts +79 -0
  22. package/dist/goal/goalStore.js +211 -0
  23. package/dist/goal/handoffExport.d.ts +34 -0
  24. package/dist/goal/handoffExport.js +183 -0
  25. package/dist/goal/subgoalSync.d.ts +40 -0
  26. package/dist/goal/subgoalSync.js +72 -0
  27. package/dist/goal/worktreeManager.d.ts +96 -0
  28. package/dist/goal/worktreeManager.js +292 -0
  29. package/dist/logging.d.ts +44 -0
  30. package/dist/logging.js +68 -0
  31. package/dist/policy/projectPolicy.d.ts +55 -0
  32. package/dist/policy/projectPolicy.js +286 -0
  33. package/dist/release/releaseGate.d.ts +99 -0
  34. package/dist/release/releaseGate.js +475 -0
  35. package/dist/runner/postTaskCleanup.d.ts +13 -0
  36. package/dist/runner/postTaskCleanup.js +147 -0
  37. package/dist/runner/runTask.js +50 -10
  38. package/dist/security/discoveryTokenStore.d.ts +62 -0
  39. package/dist/security/discoveryTokenStore.js +100 -0
  40. package/dist/security/toolInvocationGuard.d.ts +35 -0
  41. package/dist/security/toolInvocationGuard.js +153 -0
  42. package/dist/smoke-test.js +18 -10
  43. package/dist/taskRuntime.d.ts +17 -0
  44. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  45. package/dist/test/unit/acceptance-engine.test.js +228 -0
  46. package/dist/test/unit/agent-router.test.d.ts +1 -0
  47. package/dist/test/unit/agent-router.test.js +287 -0
  48. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  49. package/dist/test/unit/audit-checks.test.js +350 -0
  50. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  51. package/dist/test/unit/diagnose-task.test.js +457 -0
  52. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  53. package/dist/test/unit/discovery-token-store.test.js +139 -0
  54. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  55. package/dist/test/unit/evidence-pack.test.js +130 -0
  56. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  57. package/dist/test/unit/goal-graph.test.js +298 -0
  58. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  59. package/dist/test/unit/goal-progress.test.js +381 -0
  60. package/dist/test/unit/goal-status.test.d.ts +1 -0
  61. package/dist/test/unit/goal-status.test.js +215 -0
  62. package/dist/test/unit/goal-store.test.d.ts +1 -0
  63. package/dist/test/unit/goal-store.test.js +253 -0
  64. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  65. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  66. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  67. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  68. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  69. package/dist/test/unit/handoff-export.test.js +263 -0
  70. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  72. package/dist/test/unit/logging.test.js +127 -5
  73. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  74. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  75. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  76. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  77. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  78. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  79. package/dist/test/unit/release-gate.test.d.ts +1 -0
  80. package/dist/test/unit/release-gate.test.js +242 -0
  81. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  82. package/dist/test/unit/run-task-loop.test.js +380 -0
  83. package/dist/test/unit/safe-views.test.d.ts +1 -0
  84. package/dist/test/unit/safe-views.test.js +171 -0
  85. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  86. package/dist/test/unit/schema-drift-check.test.js +176 -0
  87. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  88. package/dist/test/unit/subgoal-sync.test.js +183 -0
  89. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  90. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  91. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  92. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  93. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  94. package/dist/test/unit/toolSearch.test.js +571 -0
  95. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  96. package/dist/test/unit/worktree-manager.test.js +176 -0
  97. package/dist/tools/auditTask.d.ts +103 -1
  98. package/dist/tools/auditTask.js +461 -8
  99. package/dist/tools/cancelTask.js +1 -1
  100. package/dist/tools/checkReleaseGate.d.ts +21 -0
  101. package/dist/tools/checkReleaseGate.js +22 -0
  102. package/dist/tools/createTask.d.ts +18 -2
  103. package/dist/tools/createTask.js +29 -1
  104. package/dist/tools/diagnoseTask.d.ts +45 -0
  105. package/dist/tools/diagnoseTask.js +347 -0
  106. package/dist/tools/discardWorktree.d.ts +24 -0
  107. package/dist/tools/discardWorktree.js +27 -0
  108. package/dist/tools/discoverTools.d.ts +11 -0
  109. package/dist/tools/discoverTools.js +39 -0
  110. package/dist/tools/evidencePack.d.ts +39 -0
  111. package/dist/tools/evidencePack.js +168 -0
  112. package/dist/tools/explainTool.d.ts +11 -0
  113. package/dist/tools/explainTool.js +21 -0
  114. package/dist/tools/getTaskSummary.js +2 -1
  115. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  116. package/dist/tools/goalSubgoalTask.js +110 -0
  117. package/dist/tools/healthCheck.d.ts +1 -0
  118. package/dist/tools/healthCheck.js +5 -1
  119. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  120. package/dist/tools/invokeDiscoveredTool.js +112 -0
  121. package/dist/tools/listTasks.d.ts +3 -1
  122. package/dist/tools/listTasks.js +14 -1
  123. package/dist/tools/mergeWorktree.d.ts +24 -0
  124. package/dist/tools/mergeWorktree.js +27 -0
  125. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  126. package/dist/tools/recommendAgentForTask.js +56 -0
  127. package/dist/tools/reconcileTasks.d.ts +41 -0
  128. package/dist/tools/reconcileTasks.js +352 -0
  129. package/dist/tools/registry.js +926 -2
  130. package/dist/tools/releaseMode.d.ts +50 -0
  131. package/dist/tools/releaseMode.js +370 -0
  132. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  133. package/dist/tools/runDirectVerificationBundle.js +64 -0
  134. package/dist/tools/runTaskLoop.d.ts +57 -0
  135. package/dist/tools/runTaskLoop.js +417 -0
  136. package/dist/tools/runVerification.d.ts +4 -0
  137. package/dist/tools/runVerification.js +4 -0
  138. package/dist/tools/safeStatus.d.ts +31 -1
  139. package/dist/tools/safeStatus.js +43 -1
  140. package/dist/tools/safeViews.d.ts +262 -0
  141. package/dist/tools/safeViews.js +252 -0
  142. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  143. package/dist/tools/schemaDriftCheck.js +80 -0
  144. package/dist/tools/taskLineage.d.ts +91 -0
  145. package/dist/tools/taskLineage.js +175 -0
  146. package/dist/tools/toolCatalog.d.ts +4 -3
  147. package/dist/tools/toolCatalog.js +39 -11
  148. package/dist/tools/toolRegistry.d.ts +61 -0
  149. package/dist/tools/toolRegistry.js +834 -0
  150. package/dist/tools/toolSearch.d.ts +110 -0
  151. package/dist/tools/toolSearch.js +331 -0
  152. package/dist/tools/toolUsageStats.d.ts +41 -0
  153. package/dist/tools/toolUsageStats.js +116 -0
  154. package/dist/tools/waitForTask.js +1 -0
  155. package/dist/version.d.ts +2 -2
  156. package/dist/version.js +2 -2
  157. package/dist/watcherStatus.d.ts +29 -0
  158. package/dist/watcherStatus.js +92 -1
  159. package/docs/chatgpt-usage.md +31 -0
  160. package/docs/control-center/README.md +9 -0
  161. package/package.json +3 -2
  162. package/scripts/checks/control-center-smoke.js +87 -0
  163. package/scripts/checks/control-smoke.js +2 -2
  164. package/scripts/checks/doctor-smoke.js +1 -0
  165. package/scripts/checks/lifecycle-smoke.js +13 -9
  166. package/scripts/checks/mcp-manifest-check.js +65 -6
  167. package/scripts/checks/mcp-smoke.js +60 -9
  168. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  169. package/src/agents/agentRouter.ts +149 -0
  170. package/src/config.ts +9 -3
  171. package/src/controlCenter.ts +215 -4
  172. package/src/direct/directSessionStore.ts +2 -0
  173. package/src/direct/directVerification.ts +7 -0
  174. package/src/doctor.ts +36 -1
  175. package/src/goal/acceptanceEngine.ts +160 -0
  176. package/src/goal/acceptanceTemplate.ts +121 -0
  177. package/src/goal/goalGraph.ts +225 -0
  178. package/src/goal/goalProgress.ts +301 -0
  179. package/src/goal/goalStatus.ts +234 -0
  180. package/src/goal/goalStore.ts +306 -0
  181. package/src/goal/handoffExport.ts +211 -0
  182. package/src/goal/subgoalSync.ts +82 -0
  183. package/src/goal/worktreeManager.ts +404 -0
  184. package/src/logging.ts +91 -0
  185. package/src/policy/projectPolicy.ts +344 -0
  186. package/src/release/releaseGate.ts +567 -0
  187. package/src/runner/postTaskCleanup.ts +154 -0
  188. package/src/runner/runTask.ts +49 -10
  189. package/src/security/discoveryTokenStore.ts +157 -0
  190. package/src/security/toolInvocationGuard.ts +251 -0
  191. package/src/smoke-test.ts +15 -7
  192. package/src/taskRuntime.ts +17 -0
  193. package/src/test/unit/acceptance-engine.test.ts +261 -0
  194. package/src/test/unit/agent-router.test.ts +342 -0
  195. package/src/test/unit/audit-checks.test.ts +567 -0
  196. package/src/test/unit/diagnose-task.test.ts +544 -0
  197. package/src/test/unit/discovery-token-store.test.ts +181 -0
  198. package/src/test/unit/evidence-pack.test.ts +142 -0
  199. package/src/test/unit/goal-graph.test.ts +347 -0
  200. package/src/test/unit/goal-progress.test.ts +538 -0
  201. package/src/test/unit/goal-status.test.ts +270 -0
  202. package/src/test/unit/goal-store.test.ts +318 -0
  203. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  204. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  205. package/src/test/unit/handoff-export.test.ts +295 -0
  206. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  207. package/src/test/unit/logging.test.ts +177 -5
  208. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  209. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  210. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  211. package/src/test/unit/release-gate.test.ts +314 -0
  212. package/src/test/unit/run-task-loop.test.ts +425 -0
  213. package/src/test/unit/safe-views.test.ts +184 -0
  214. package/src/test/unit/schema-drift-check.test.ts +260 -0
  215. package/src/test/unit/subgoal-sync.test.ts +236 -0
  216. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  217. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  218. package/src/test/unit/toolSearch.test.ts +756 -0
  219. package/src/test/unit/worktree-manager.test.ts +247 -0
  220. package/src/tools/auditTask.ts +831 -402
  221. package/src/tools/cancelTask.ts +1 -1
  222. package/src/tools/checkReleaseGate.ts +45 -0
  223. package/src/tools/createTask.ts +64 -6
  224. package/src/tools/diagnoseTask.ts +460 -0
  225. package/src/tools/discardWorktree.ts +42 -0
  226. package/src/tools/discoverTools.ts +51 -0
  227. package/src/tools/evidencePack.ts +205 -0
  228. package/src/tools/explainTool.ts +26 -0
  229. package/src/tools/getTaskSummary.ts +2 -1
  230. package/src/tools/goalSubgoalTask.ts +170 -0
  231. package/src/tools/healthCheck.ts +4 -1
  232. package/src/tools/invokeDiscoveredTool.ts +163 -0
  233. package/src/tools/listTasks.ts +16 -2
  234. package/src/tools/listWorkspace.ts +71 -71
  235. package/src/tools/mergeWorktree.ts +42 -0
  236. package/src/tools/recommendAgentForTask.ts +79 -0
  237. package/src/tools/reconcileTasks.ts +464 -0
  238. package/src/tools/registry.ts +1093 -73
  239. package/src/tools/releaseMode.ts +450 -0
  240. package/src/tools/runDirectVerificationBundle.ts +98 -0
  241. package/src/tools/runTaskLoop.ts +526 -0
  242. package/src/tools/runVerification.ts +8 -0
  243. package/src/tools/safeStatus.ts +73 -2
  244. package/src/tools/safeViews.ts +273 -0
  245. package/src/tools/schemaDriftCheck.ts +120 -0
  246. package/src/tools/taskLineage.ts +300 -0
  247. package/src/tools/toolCatalog.ts +40 -11
  248. package/src/tools/toolRegistry.ts +896 -0
  249. package/src/tools/toolSearch.ts +464 -0
  250. package/src/tools/toolUsageStats.ts +151 -0
  251. package/src/tools/waitForTask.ts +1 -0
  252. package/src/version.ts +2 -2
  253. package/src/watcherStatus.ts +97 -1
  254. package/ui/pages/dashboard.html +143 -2
@@ -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
+ });
@@ -0,0 +1,184 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { reloadConfig } from "../../config.js";
7
+ import { safeDirectSummary, safeAuditDirectSession, safeDiffSummary, safeResult, safeTestSummary } from "../../tools/safeViews.js";
8
+
9
+ let tempDir: string;
10
+ let prevConfigEnv: string | undefined;
11
+
12
+ function writeConfig() {
13
+ const configPath = join(tempDir, "patchwarden.config.json");
14
+ writeFileSync(
15
+ configPath,
16
+ JSON.stringify({
17
+ workspaceRoot: tempDir,
18
+ tasksDir: ".patchwarden/tasks",
19
+ plansDir: ".patchwarden/plans",
20
+ assessmentsDir: ".patchwarden/assessments",
21
+ directSessionsDir: ".patchwarden/direct-sessions",
22
+ agents: {},
23
+ allowedTestCommands: ["npm test"],
24
+ directAllowedCommands: ["npm test"],
25
+ enableDirectProfile: true,
26
+ }),
27
+ "utf-8"
28
+ );
29
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
30
+ process.env.PATCHWARDEN_CONFIG = configPath;
31
+ reloadConfig();
32
+ }
33
+
34
+ describe("safeViews", () => {
35
+ beforeEach(() => {
36
+ tempDir = mkdtempSync(join(tmpdir(), "pw-safeviews-"));
37
+ writeConfig();
38
+ });
39
+
40
+ afterEach(() => {
41
+ if (prevConfigEnv === undefined) delete process.env.PATCHWARDEN_CONFIG;
42
+ else process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
43
+ reloadConfig();
44
+ rmSync(tempDir, { recursive: true, force: true });
45
+ });
46
+
47
+ it("returns bounded task summaries without log or diff content", () => {
48
+ const taskId = "task-safe-001";
49
+ const taskDir = join(tempDir, ".patchwarden", "tasks", taskId);
50
+ mkdirSync(taskDir, { recursive: true });
51
+ writeFileSync(join(taskDir, "status.json"), JSON.stringify({
52
+ task_id: taskId,
53
+ status: "done_by_agent",
54
+ phase: "done_by_agent",
55
+ repo_path: "repo",
56
+ resolved_repo_path: join(tempDir, "repo"),
57
+ verify_status: "passed",
58
+ }), "utf-8");
59
+ writeFileSync(join(taskDir, "result.json"), JSON.stringify({
60
+ task_id: taskId,
61
+ status: "done_by_agent",
62
+ summary: "Finished safely",
63
+ changed_files: [{ path: "src/index.ts", change: "modified" }],
64
+ verify_status: "passed",
65
+ warnings: [],
66
+ }), "utf-8");
67
+ writeFileSync(join(taskDir, "verify.json"), JSON.stringify({
68
+ status: "passed",
69
+ commands: [{ command: "npm test", status: "passed", exit_code: 0, stdout_tail: "SECRET_STDOUT", stderr_tail: "SECRET_STDERR" }],
70
+ }), "utf-8");
71
+ writeFileSync(join(taskDir, "changed-files.json"), JSON.stringify({
72
+ changed_files: [{ path: "src/index.ts", change: "modified", tracked: true, ignored: false, kind: "source" }],
73
+ additions: 1,
74
+ deletions: 0,
75
+ diff_available: true,
76
+ diff_truncated: false,
77
+ patch_mode: "textual",
78
+ artifact_hygiene: { counts: { source_changes: 1 }, source_changes: [], tracked_build_artifacts: [], ignored_untracked_artifacts: [], runtime_generated_files: [], suspicious_changes: [] },
79
+ }), "utf-8");
80
+ writeFileSync(join(taskDir, "git.diff"), "SECRET_DIFF", "utf-8");
81
+ writeFileSync(join(taskDir, "test.log"), "SECRET_TEST_LOG", "utf-8");
82
+
83
+ const payload = JSON.stringify({
84
+ result: safeResult(taskId),
85
+ tests: safeTestSummary(taskId),
86
+ diff: safeDiffSummary(taskId),
87
+ });
88
+ assert.ok(!payload.includes("SECRET_STDOUT"));
89
+ assert.ok(!payload.includes("SECRET_STDERR"));
90
+ assert.ok(!payload.includes("SECRET_DIFF"));
91
+ assert.ok(!payload.includes("SECRET_TEST_LOG"));
92
+ assert.ok(payload.includes("src/index.ts"));
93
+ });
94
+
95
+ it("treats corrupted safe-view JSON artifacts as empty summaries", () => {
96
+ const taskId = "task-safe-corrupt";
97
+ const taskDir = join(tempDir, ".patchwarden", "tasks", taskId);
98
+ mkdirSync(taskDir, { recursive: true });
99
+ writeFileSync(join(taskDir, "status.json"), JSON.stringify({
100
+ task_id: taskId,
101
+ status: "done_by_agent",
102
+ phase: "done_by_agent",
103
+ repo_path: "repo",
104
+ resolved_repo_path: join(tempDir, "repo"),
105
+ verify_status: "passed",
106
+ }), "utf-8");
107
+ writeFileSync(join(taskDir, "result.json"), JSON.stringify({
108
+ task_id: taskId,
109
+ status: "done_by_agent",
110
+ summary: "Finished safely",
111
+ changed_files: [],
112
+ verify_status: "passed",
113
+ warnings: [],
114
+ }), "utf-8");
115
+ writeFileSync(join(taskDir, "verify.json"), "\uFEFF{not json", "utf-8");
116
+ writeFileSync(join(taskDir, "changed-files.json"), "{not json", "utf-8");
117
+
118
+ const tests = safeTestSummary(taskId);
119
+ const diff = safeDiffSummary(taskId);
120
+
121
+ assert.equal(tests.command_count, 0);
122
+ assert.equal(diff.changed_files_total, 0);
123
+ });
124
+
125
+ it("returns Direct safe summaries without verification tails", () => {
126
+ const sessionId = "direct-safe-001";
127
+ const sessionDir = join(tempDir, ".patchwarden", "direct-sessions", sessionId);
128
+ mkdirSync(sessionDir, { recursive: true });
129
+ const session = {
130
+ session_id: sessionId,
131
+ title: "safe direct",
132
+ repo_path: "repo",
133
+ resolved_repo_path: join(tempDir, "repo"),
134
+ created_at: new Date().toISOString(),
135
+ expires_at: new Date(Date.now() + 60_000).toISOString(),
136
+ server_version: "1.0.0",
137
+ schema_epoch: "test",
138
+ tool_manifest_sha256: "x".repeat(64),
139
+ workspace_snapshot_before: { head: null, status: "", files: {}, workspace_dirty: false, warnings: [], is_git: false },
140
+ workspace_fingerprint_before: "fingerprint",
141
+ allowed_commands: ["npm test"],
142
+ operations: [],
143
+ verification_runs: [{
144
+ command: "npm test",
145
+ exit_code: 0,
146
+ passed: true,
147
+ timed_out: false,
148
+ stdout_tail: "SECRET_DIRECT_STDOUT",
149
+ stderr_tail: "SECRET_DIRECT_STDERR",
150
+ started_at: new Date().toISOString(),
151
+ finished_at: new Date().toISOString(),
152
+ log_path: join(sessionDir, "verification.log"),
153
+ }],
154
+ finalized: true,
155
+ finalized_at: new Date().toISOString(),
156
+ audited: false,
157
+ change_artifacts: {
158
+ changed_files: [],
159
+ diff: "SECRET_DIRECT_DIFF",
160
+ diff_available: true,
161
+ diff_truncated: false,
162
+ diff_size_bytes: 18,
163
+ additions: 0,
164
+ deletions: 0,
165
+ file_stats: [],
166
+ workspace_dirty_before: false,
167
+ workspace_dirty_after: false,
168
+ patch_mode: "no_changes",
169
+ unavailable_reason: null,
170
+ artifact_hygiene: { counts: { source_changes: 0, tracked_build_artifacts: 0, ignored_untracked_artifacts: 0, runtime_generated_files: 0, suspicious_changes: 0 }, source_changes: [], tracked_build_artifacts: [], ignored_untracked_artifacts: [], runtime_generated_files: [], suspicious_changes: [] },
171
+ },
172
+ };
173
+ writeFileSync(join(sessionDir, "session.json"), JSON.stringify(session, null, 2), "utf-8");
174
+
175
+ const payload = JSON.stringify({
176
+ summary: safeDirectSummary(sessionId),
177
+ audit: safeAuditDirectSession(sessionId),
178
+ });
179
+ assert.ok(!payload.includes("SECRET_DIRECT_STDOUT"));
180
+ assert.ok(!payload.includes("SECRET_DIRECT_STDERR"));
181
+ assert.ok(!payload.includes("SECRET_DIRECT_DIFF"));
182
+ assert.ok(payload.includes("npm test"));
183
+ });
184
+ });