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,224 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { reloadConfig } from "../../config.js";
7
+ import { recommendAgentForTask } from "../../tools/recommendAgentForTask.js";
8
+ import { exportTaskEvidencePack, listEvidencePacks, readEvidencePack } from "../../tools/evidencePack.js";
9
+ import { writeTaskLineage } from "../../tools/taskLineage.js";
10
+ let tempDir;
11
+ let prevConfigEnv;
12
+ function writeConfig() {
13
+ const configPath = join(tempDir, "patchwarden.config.json");
14
+ writeFileSync(configPath, JSON.stringify({
15
+ workspaceRoot: tempDir,
16
+ tasksDir: ".patchwarden/tasks",
17
+ plansDir: ".patchwarden/plans",
18
+ assessmentsDir: ".patchwarden/assessments",
19
+ agents: {
20
+ codex: { command: "codex", args: [] },
21
+ opencode: { command: "opencode", args: [] },
22
+ },
23
+ allowedTestCommands: ["npm test", "npm run build"],
24
+ defaultTaskTimeoutSeconds: 30,
25
+ maxTaskTimeoutSeconds: 120,
26
+ }), "utf-8");
27
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
28
+ process.env.PATCHWARDEN_CONFIG = configPath;
29
+ reloadConfig();
30
+ }
31
+ describe("v1.5 evidence packs and agent recommendations", () => {
32
+ beforeEach(() => {
33
+ tempDir = mkdtempSync(join(tmpdir(), "pw-evidence-"));
34
+ writeConfig();
35
+ });
36
+ afterEach(() => {
37
+ if (prevConfigEnv === undefined)
38
+ delete process.env.PATCHWARDEN_CONFIG;
39
+ else
40
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
41
+ reloadConfig();
42
+ rmSync(tempDir, { recursive: true, force: true });
43
+ });
44
+ it("recommends an agent without creating task evidence", () => {
45
+ const recommendation = recommendAgentForTask({
46
+ repo_path: ".",
47
+ goal: "Refactor two modules safely",
48
+ scope_files: ["src/a.ts", "src/b.ts"],
49
+ });
50
+ assert.equal(recommendation.bounded, true);
51
+ assert.ok(["codex", "opencode"].includes(recommendation.recommended_agent));
52
+ assert.ok(Array.isArray(recommendation.suggested_verify_commands));
53
+ const payload = JSON.stringify(recommendation);
54
+ assert.ok(!payload.includes("stdout"));
55
+ assert.ok(!payload.includes("stderr"));
56
+ assert.ok(!payload.includes("diff"));
57
+ });
58
+ it("exports bounded BOM-free evidence pack from lineage", () => {
59
+ writeTaskLineage({
60
+ lineage_id: "lineage_v15_pack",
61
+ goal: "Export evidence",
62
+ repo_path: ".",
63
+ created_at: "2026-07-05T12:00:00.000Z",
64
+ updated_at: "2026-07-05T12:01:00.000Z",
65
+ final_status: "accepted",
66
+ stop_reason: "success",
67
+ next_action: "accept",
68
+ main_task: "task-main",
69
+ fix_tasks: [],
70
+ cleanup_tasks: [],
71
+ direct_sessions: [{
72
+ session_id: "direct-one",
73
+ status: "passed",
74
+ command_count: 1,
75
+ passed_commands: 1,
76
+ failed_commands: 0,
77
+ timed_out_commands: 0,
78
+ audit_decision: "pass",
79
+ changed_files_total: 0,
80
+ next_action: "accept",
81
+ }],
82
+ rounds: [{
83
+ iteration: 1,
84
+ task_id: "task-main",
85
+ role: "main",
86
+ status: "done_by_agent",
87
+ terminal: true,
88
+ verification_status: "passed",
89
+ audit_verdict: "pass",
90
+ fail_checks: [],
91
+ warn_checks: [],
92
+ next_action: "accept",
93
+ }],
94
+ warnings: [],
95
+ errors: [],
96
+ worktree: {
97
+ isolation_mode: "worktree",
98
+ worktree_id: "wt-one",
99
+ worktree_path: join(tempDir, "_workspacetrees", "wt-one"),
100
+ branch: "pw-test",
101
+ cleanup: "keep",
102
+ status: "active",
103
+ next_action: "merge or discard explicitly",
104
+ },
105
+ agent_routing: {
106
+ requested_agent: "auto",
107
+ selected_agent: "codex",
108
+ reason: "test route",
109
+ fallback: false,
110
+ },
111
+ });
112
+ const pack = exportTaskEvidencePack({ lineage_id: "lineage_v15_pack" });
113
+ assert.equal(pack.bounded, true);
114
+ assert.equal(pack.lineage.worktree.worktree_id, "wt-one");
115
+ assert.equal(pack.lineage.agent_routing?.selected_agent, "codex");
116
+ const raw = readFileSync(pack.files.json);
117
+ assert.notEqual(raw[0], 0xef);
118
+ JSON.parse(raw.toString("utf-8"));
119
+ const payload = JSON.stringify(pack);
120
+ assert.ok(!payload.includes("stdout_tail"));
121
+ assert.ok(!payload.includes("stderr_tail"));
122
+ assert.ok(!payload.includes("diff.patch"));
123
+ assert.ok(!payload.includes("verification.log"));
124
+ const readBack = readEvidencePack("lineage_v15_pack");
125
+ assert.equal(readBack?.lineage_id, "lineage_v15_pack");
126
+ const listed = listEvidencePacks();
127
+ assert.equal(listed.total, 1);
128
+ assert.equal(listed.evidence_packs[0].lineage_id, "lineage_v15_pack");
129
+ // v2: all six structured artifact files should exist.
130
+ for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
131
+ assert.ok(existsSync(pack.files[key]), `${key} file should exist`);
132
+ }
133
+ // v2: attestation.json must carry the required provenance fields.
134
+ const attestation = JSON.parse(readFileSync(pack.files.attestation, "utf-8"));
135
+ assert.ok(attestation.patchwarden_version, "attestation should have patchwarden_version");
136
+ assert.ok(attestation.package_version, "attestation should have package_version");
137
+ assert.ok(attestation.commit, "attestation should have commit");
138
+ assert.ok(attestation.node_version, "attestation should have node_version");
139
+ assert.ok(attestation.os, "attestation should have os");
140
+ assert.ok(attestation.schema_epoch, "attestation should have schema_epoch");
141
+ // v2: redactions.json must have the audit structure even when empty.
142
+ const redactions = JSON.parse(readFileSync(pack.files.redactions, "utf-8"));
143
+ assert.ok(Array.isArray(redactions.redactions), "redactions should be an array");
144
+ assert.equal(typeof redactions.total_redacted, "number", "total_redacted should be a number");
145
+ });
146
+ it("exports v2 structured artifacts and redacts secrets in diffstat", () => {
147
+ const secretValue = "ghp_1234567890abcdefghijklmnop";
148
+ // Create a task directory with file-stats.json containing a secret in a file path.
149
+ const taskDir = join(tempDir, ".patchwarden", "tasks", "task-v2-secret");
150
+ mkdirSync(taskDir, { recursive: true });
151
+ writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
152
+ task_id: "task-v2-secret",
153
+ additions: 5,
154
+ deletions: 2,
155
+ files: [{
156
+ path: `secrets/${secretValue}.json`,
157
+ status: "modified",
158
+ additions: 5,
159
+ deletions: 2,
160
+ }],
161
+ }), "utf-8");
162
+ writeTaskLineage({
163
+ lineage_id: "lineage_v2_secret",
164
+ goal: "Export v2 evidence with diffstat",
165
+ repo_path: ".",
166
+ created_at: "2026-07-09T12:00:00.000Z",
167
+ updated_at: "2026-07-09T12:01:00.000Z",
168
+ final_status: "accepted",
169
+ stop_reason: "success",
170
+ next_action: "accept",
171
+ main_task: "task-v2-secret",
172
+ fix_tasks: [],
173
+ cleanup_tasks: [],
174
+ direct_sessions: [],
175
+ rounds: [{
176
+ iteration: 1,
177
+ task_id: "task-v2-secret",
178
+ role: "main",
179
+ status: "done_by_agent",
180
+ terminal: true,
181
+ verification_status: "passed",
182
+ audit_verdict: "pass",
183
+ fail_checks: [],
184
+ warn_checks: ["minor scope drift"],
185
+ next_action: "accept",
186
+ }],
187
+ warnings: [],
188
+ errors: [],
189
+ worktree: {
190
+ isolation_mode: "current_repo",
191
+ cleanup: "keep",
192
+ status: "not_used",
193
+ next_action: "none",
194
+ },
195
+ });
196
+ const pack = exportTaskEvidencePack({ lineage_id: "lineage_v2_secret" });
197
+ // All six v2 files should exist.
198
+ for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
199
+ assert.ok(existsSync(pack.files[key]), `${key} file should exist`);
200
+ }
201
+ // The original secret must not appear in any v2 file.
202
+ for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
203
+ const content = readFileSync(pack.files[key], "utf-8");
204
+ assert.ok(!content.includes(secretValue), `${key} should not contain the original secret value`);
205
+ }
206
+ // redactions.json should have detected the secret via diffstat aggregation.
207
+ const redactions = JSON.parse(readFileSync(pack.files.redactions, "utf-8"));
208
+ assert.ok(redactions.total_redacted > 0, "redactions.json should have detected the secret");
209
+ assert.ok(!JSON.stringify(redactions).includes(secretValue), "redactions.json should not contain the original secret value");
210
+ // risk.json should carry the warn_check as a medium-severity risk.
211
+ const risk = JSON.parse(readFileSync(pack.files.risk, "utf-8"));
212
+ assert.ok(risk.count > 0, "risk.json should have at least one risk item");
213
+ assert.equal(risk.by_severity.medium, 1, "should have one medium-severity risk");
214
+ // verify.json should carry the round verification record.
215
+ const verify = JSON.parse(readFileSync(pack.files.verify, "utf-8"));
216
+ assert.ok(verify.count > 0, "verify.json should have at least one record");
217
+ assert.equal(verify.records[0].verification_status, "passed");
218
+ // lineage.json should carry the bounded summary.
219
+ const lineageSummary = JSON.parse(readFileSync(pack.files.lineage, "utf-8"));
220
+ assert.equal(lineageSummary.lineage_id, "lineage_v2_secret");
221
+ assert.equal(lineageSummary.iterations_count, 1);
222
+ assert.equal(lineageSummary.final_status, "accepted");
223
+ });
224
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,174 @@
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 { getTaskStatus } from "../../tools/getTaskStatus.js";
8
+ let tempDir;
9
+ let tasksDir;
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
+ directSessionsDir: ".patchwarden/direct-sessions",
19
+ agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
20
+ allowedTestCommands: ["npm test"],
21
+ }), "utf-8");
22
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
23
+ process.env.PATCHWARDEN_CONFIG = configPath;
24
+ reloadConfig();
25
+ }
26
+ function writeTaskStatus(taskId, status) {
27
+ const taskDir = join(tasksDir, taskId);
28
+ mkdirSync(taskDir, { recursive: true });
29
+ writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
30
+ return taskDir;
31
+ }
32
+ function writeTaskRuntime(taskDir, runtime) {
33
+ writeFileSync(join(taskDir, "runtime.json"), JSON.stringify(runtime, null, 2), "utf-8");
34
+ }
35
+ describe("getTaskStatus", () => {
36
+ beforeEach(() => {
37
+ tempDir = mkdtempSync(join(tmpdir(), "pw-getstatus-"));
38
+ tasksDir = join(tempDir, ".patchwarden", "tasks");
39
+ mkdirSync(tasksDir, { recursive: true });
40
+ writeConfig();
41
+ });
42
+ afterEach(() => {
43
+ if (prevConfigEnv === undefined)
44
+ delete process.env.PATCHWARDEN_CONFIG;
45
+ else
46
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
47
+ reloadConfig();
48
+ rmSync(tempDir, { recursive: true, force: true });
49
+ });
50
+ // ── pending ──
51
+ it("reads pending status task", () => {
52
+ writeTaskStatus("task-pending-001", {
53
+ task_id: "task-pending-001",
54
+ plan_id: "plan-001",
55
+ agent: "codex",
56
+ workspace_root: tempDir,
57
+ repo_path: "repo",
58
+ resolved_repo_path: join(tempDir, "repo"),
59
+ status: "pending",
60
+ phase: "queued",
61
+ created_at: "2026-07-12T10:00:00Z",
62
+ updated_at: "2026-07-12T10:00:01Z",
63
+ timeout_seconds: 900,
64
+ error: null,
65
+ });
66
+ const result = getTaskStatus("task-pending-001");
67
+ assert.equal(result.task_id, "task-pending-001");
68
+ assert.equal(result.status, "pending");
69
+ assert.equal(result.phase, "queued");
70
+ assert.equal(result.agent, "codex");
71
+ assert.equal(result.plan_id, "plan-001");
72
+ assert.equal(result.error, null);
73
+ // No watcher heartbeat → watcher missing → pending task is execution_blocked
74
+ assert.equal(result.execution_blocked, true);
75
+ assert.equal(result.watcher_status, "missing");
76
+ assert.equal(result.pending_reason, "queued_but_watcher_missing");
77
+ });
78
+ // ── running ──
79
+ it("reads running status task and merges runtime.json", () => {
80
+ const taskDir = writeTaskStatus("task-running-001", {
81
+ task_id: "task-running-001",
82
+ plan_id: "plan-001",
83
+ agent: "codex",
84
+ workspace_root: tempDir,
85
+ repo_path: "repo",
86
+ resolved_repo_path: join(tempDir, "repo"),
87
+ status: "running",
88
+ phase: "executing_agent",
89
+ created_at: "2026-07-12T10:00:00Z",
90
+ started_at: "2026-07-12T10:00:05Z",
91
+ updated_at: "2026-07-12T10:00:10Z",
92
+ timeout_seconds: 900,
93
+ error: null,
94
+ });
95
+ writeTaskRuntime(taskDir, {
96
+ phase: "executing_agent",
97
+ last_heartbeat_at: "2026-07-12T10:00:12Z",
98
+ current_command: "codex exec --prompt",
99
+ });
100
+ const result = getTaskStatus("task-running-001");
101
+ assert.equal(result.status, "running");
102
+ assert.equal(result.phase, "executing_agent");
103
+ assert.equal(result.current_command, "codex exec --prompt");
104
+ assert.equal(result.last_heartbeat_at, "2026-07-12T10:00:12Z");
105
+ assert.equal(result.started_at, "2026-07-12T10:00:05Z");
106
+ // running (not pending) → execution_blocked is false
107
+ assert.equal(result.execution_blocked, false);
108
+ assert.equal(result.pending_reason, "agent_running");
109
+ });
110
+ // ── done ──
111
+ it("reads done status task", () => {
112
+ writeTaskStatus("task-done-001", {
113
+ task_id: "task-done-001",
114
+ plan_id: "plan-001",
115
+ agent: "codex",
116
+ workspace_root: tempDir,
117
+ repo_path: "repo",
118
+ resolved_repo_path: join(tempDir, "repo"),
119
+ status: "done",
120
+ phase: "completed",
121
+ created_at: "2026-07-12T10:00:00Z",
122
+ started_at: "2026-07-12T10:00:05Z",
123
+ finished_at: "2026-07-12T10:05:00Z",
124
+ updated_at: "2026-07-12T10:05:00Z",
125
+ timeout_seconds: 900,
126
+ verify_status: "passed",
127
+ error: null,
128
+ });
129
+ const result = getTaskStatus("task-done-001");
130
+ assert.equal(result.status, "done");
131
+ assert.equal(result.phase, "completed");
132
+ assert.equal(result.finished_at, "2026-07-12T10:05:00Z");
133
+ assert.equal(result.verify_status, "passed");
134
+ assert.equal(result.execution_blocked, false);
135
+ assert.equal(result.pending_reason, null);
136
+ });
137
+ // ── failed ──
138
+ it("reads failed status task", () => {
139
+ writeTaskStatus("task-failed-001", {
140
+ task_id: "task-failed-001",
141
+ plan_id: "plan-001",
142
+ agent: "codex",
143
+ workspace_root: tempDir,
144
+ repo_path: "repo",
145
+ resolved_repo_path: join(tempDir, "repo"),
146
+ status: "failed",
147
+ phase: "failed",
148
+ created_at: "2026-07-12T10:00:00Z",
149
+ started_at: "2026-07-12T10:00:05Z",
150
+ finished_at: "2026-07-12T10:05:00Z",
151
+ updated_at: "2026-07-12T10:05:00Z",
152
+ timeout_seconds: 900,
153
+ error: "Agent exited with code 1",
154
+ });
155
+ const result = getTaskStatus("task-failed-001");
156
+ assert.equal(result.status, "failed");
157
+ assert.equal(result.phase, "failed");
158
+ assert.equal(result.error, "Agent exited with code 1");
159
+ assert.equal(result.finished_at, "2026-07-12T10:05:00Z");
160
+ assert.equal(result.execution_blocked, false);
161
+ });
162
+ // ── status.json missing ──
163
+ it("throws when status.json is missing (task not found)", () => {
164
+ // guardReadPath throws "File not found" before the existsSync "Task not found" check
165
+ assert.throws(() => getTaskStatus("non-existent-task"), /(File not found|Task not found)/);
166
+ });
167
+ // ── status.json corrupt ──
168
+ it("throws when status.json is corrupt", () => {
169
+ const taskDir = join(tasksDir, "task-corrupt-001");
170
+ mkdirSync(taskDir, { recursive: true });
171
+ writeFileSync(join(taskDir, "status.json"), "{not valid json", "utf-8");
172
+ assert.throws(() => getTaskStatus("task-corrupt-001"), SyntaxError);
173
+ });
174
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
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 { getTaskSummary } from "../../tools/getTaskSummary.js";
8
+ let tempDir;
9
+ let tasksDir;
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
+ directSessionsDir: ".patchwarden/direct-sessions",
19
+ agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
20
+ allowedTestCommands: ["npm test"],
21
+ }), "utf-8");
22
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
23
+ process.env.PATCHWARDEN_CONFIG = configPath;
24
+ reloadConfig();
25
+ }
26
+ function writeTaskStatus(taskId, status) {
27
+ const taskDir = join(tasksDir, taskId);
28
+ mkdirSync(taskDir, { recursive: true });
29
+ writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
30
+ return taskDir;
31
+ }
32
+ function baseStatus(taskId, overrides = {}) {
33
+ return {
34
+ task_id: taskId,
35
+ plan_id: "plan-001",
36
+ agent: "codex",
37
+ workspace_root: tempDir,
38
+ repo_path: "repo",
39
+ resolved_repo_path: join(tempDir, "repo"),
40
+ created_at: "2026-07-12T10:00:00Z",
41
+ started_at: "2026-07-12T10:00:05Z",
42
+ finished_at: "2026-07-12T10:05:00Z",
43
+ updated_at: "2026-07-12T10:05:00Z",
44
+ timeout_seconds: 900,
45
+ error: null,
46
+ ...overrides,
47
+ };
48
+ }
49
+ describe("getTaskSummary", () => {
50
+ beforeEach(() => {
51
+ tempDir = mkdtempSync(join(tmpdir(), "pw-getsummary-"));
52
+ tasksDir = join(tempDir, ".patchwarden", "tasks");
53
+ mkdirSync(tasksDir, { recursive: true });
54
+ writeConfig();
55
+ });
56
+ afterEach(() => {
57
+ if (prevConfigEnv === undefined)
58
+ delete process.env.PATCHWARDEN_CONFIG;
59
+ else
60
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
61
+ reloadConfig();
62
+ rmSync(tempDir, { recursive: true, force: true });
63
+ });
64
+ // ── compact mode ──
65
+ it("returns compact summary in compact mode", () => {
66
+ writeTaskStatus("task-compact-001", baseStatus("task-compact-001", {
67
+ status: "done",
68
+ phase: "completed",
69
+ verify_status: "passed",
70
+ }));
71
+ const result = getTaskSummary("task-compact-001", { view: "compact" });
72
+ assert.equal(result.view, "compact");
73
+ assert.equal(result.task_id, "task-compact-001");
74
+ assert.equal(result.status, "done");
75
+ assert.equal(result.terminal, true);
76
+ // compact-only fields
77
+ assert.equal(typeof result.changed_files_total, "number");
78
+ assert.equal(typeof result.release_artifacts_count, "number");
79
+ assert.ok(result.artifact_hygiene);
80
+ assert.ok("max_items" in result.artifact_hygiene);
81
+ });
82
+ // ── full / standard mode ──
83
+ it("returns full summary in standard mode", () => {
84
+ writeTaskStatus("task-full-001", baseStatus("task-full-001", {
85
+ status: "done",
86
+ phase: "completed",
87
+ verify_status: "passed",
88
+ }));
89
+ const result = getTaskSummary("task-full-001");
90
+ // standard mode has no `view` field
91
+ assert.equal(result.view, undefined);
92
+ assert.equal(result.task_id, "task-full-001");
93
+ assert.equal(result.status, "done");
94
+ assert.equal(result.terminal, true);
95
+ // full-mode-only fields
96
+ assert.ok("log_tails" in result);
97
+ assert.ok("artifacts" in result);
98
+ assert.ok("failed_command_detail" in result);
99
+ assert.ok("acceptance_reviewed_at" in result);
100
+ });
101
+ // ── task not found ──
102
+ it("throws when task does not exist", () => {
103
+ assert.throws(() => getTaskSummary("non-existent-task"), /(File not found|Task not found)/);
104
+ });
105
+ // ── terminal: done ──
106
+ it("returns terminal summary for done task with passed verification", () => {
107
+ writeTaskStatus("task-done-001", baseStatus("task-done-001", {
108
+ status: "done",
109
+ phase: "completed",
110
+ verify_status: "passed",
111
+ }));
112
+ const result = getTaskSummary("task-done-001");
113
+ assert.equal(result.terminal, true);
114
+ assert.equal(result.status, "done");
115
+ assert.equal(result.verify_status, "passed");
116
+ // done + verify passed → ready_for_review
117
+ assert.equal(result.acceptance_status, "ready_for_review");
118
+ });
119
+ // ── terminal: failed ──
120
+ it("returns terminal summary for failed task", () => {
121
+ writeTaskStatus("task-failed-001", baseStatus("task-failed-001", {
122
+ status: "failed",
123
+ phase: "failed",
124
+ error: "Agent exited with code 1",
125
+ }));
126
+ const result = getTaskSummary("task-failed-001");
127
+ assert.equal(result.terminal, true);
128
+ assert.equal(result.status, "failed");
129
+ assert.equal(result.acceptance_status, "failed");
130
+ assert.ok(result.errors.includes("Agent exited with code 1"));
131
+ });
132
+ // ── running (non-terminal) ──
133
+ it("returns non-terminal summary for running task", () => {
134
+ writeTaskStatus("task-running-001", baseStatus("task-running-001", {
135
+ status: "running",
136
+ phase: "executing_agent",
137
+ started_at: "2026-07-12T10:00:00Z",
138
+ finished_at: undefined,
139
+ }));
140
+ const result = getTaskSummary("task-running-001");
141
+ assert.equal(result.terminal, false);
142
+ assert.equal(result.status, "running");
143
+ assert.equal(result.phase, "executing_agent");
144
+ assert.equal(result.acceptance_status, "pending");
145
+ });
146
+ });
@@ -0,0 +1 @@
1
+ export {};