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
@@ -103,17 +103,29 @@ try {
103
103
  const tools = await client.listTools();
104
104
  const names = tools.tools.map((tool) => tool.name).sort();
105
105
  const expected = [
106
+ "accept_subgoal",
106
107
  "apply_patch",
107
108
  "audit_session",
108
109
  "audit_task",
109
110
  "cancel_task",
111
+ "check_release_gate",
110
112
  "create_direct_session",
113
+ "create_goal",
114
+ "create_subgoal_task",
111
115
  "create_task",
116
+ "diagnose_task",
117
+ "discard_worktree",
118
+ "discover_tools",
119
+ "explain_tool",
120
+ "export_handoff",
121
+ "export_task_evidence_pack",
112
122
  "finalize_direct_session",
113
123
  "get_diff",
114
124
  "get_plan",
125
+ "get_project_policy",
115
126
  "get_result",
116
127
  "get_result_json",
128
+ "get_task_lineage",
117
129
  "get_task_log_tail",
118
130
  "get_task_progress",
119
131
  "get_task_status",
@@ -121,16 +133,38 @@ try {
121
133
  "get_task_summary",
122
134
  "get_test_log",
123
135
  "health_check",
136
+ "invoke_discovered_tool",
124
137
  "kill_task",
125
138
  "list_agents",
139
+ "list_goals",
126
140
  "list_tasks",
127
141
  "list_workspace",
142
+ "merge_worktree",
143
+ "read_goal",
128
144
  "read_workspace_file",
145
+ "recommend_agent_for_task",
146
+ "reconcile_tasks",
147
+ "reject_subgoal",
148
+ "release_check",
149
+ "release_cleanup",
150
+ "release_prepare",
151
+ "release_verify",
129
152
  "retry_task",
153
+ "run_direct_verification_bundle",
154
+ "run_task_loop",
130
155
  "run_verification",
156
+ "safe_audit",
157
+ "safe_audit_direct_session",
158
+ "safe_diff_summary",
159
+ "safe_direct_summary",
160
+ "safe_finalize_direct_session",
161
+ "safe_result",
131
162
  "safe_status",
163
+ "safe_test_summary",
132
164
  "save_plan",
133
165
  "search_workspace",
166
+ "suggest_next_subgoal",
167
+ "summarize_goal_progress",
134
168
  "sync_file",
135
169
  "wait_for_task",
136
170
  ];
@@ -143,7 +177,7 @@ try {
143
177
  if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
144
178
  throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
145
179
  }
146
- if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 30) {
180
+ if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 64) {
147
181
  throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
148
182
  }
149
183
  if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
@@ -296,8 +330,8 @@ try {
296
330
 
297
331
  const statusPath = join(task.path, "status.json");
298
332
  const statusAfter = JSON.parse(readFileSync(statusPath, "utf-8"));
299
- if (statusAfter.status !== "done") {
300
- throw new Error(`runner status should be done, got ${statusAfter.status}`);
333
+ if (statusAfter.status !== "done" && statusAfter.status !== "done_by_agent") {
334
+ throw new Error(`runner status should be done or done_by_agent, got ${statusAfter.status}`);
301
335
  }
302
336
  for (const fileName of ["result.md", "result.json", "diff.patch", "git.diff", "file-stats.json", "test.log", "verify.json", "verify.log"]) {
303
337
  if (!existsSync(join(task.path, fileName))) {
@@ -393,7 +427,7 @@ try {
393
427
  await disabledClient.close();
394
428
  ok("chatgpt_direct disabled exposes only health_check with diagnostic");
395
429
 
396
- // 2. chatgpt_direct enabled: 10 tools + minimal create_direct_session
430
+ // 2. chatgpt_direct enabled: 14 tools + minimal create_direct_session
397
431
  const enabledConfigPath = join(tempRoot, "direct-enabled.json");
398
432
  const directRepo = join(workspaceRoot, "direct-fixture");
399
433
  mkdirSync(join(directRepo, "src"), { recursive: true });
@@ -449,15 +483,19 @@ try {
449
483
  "health_check",
450
484
  "list_workspace",
451
485
  "read_workspace_file",
486
+ "run_direct_verification_bundle",
452
487
  "run_verification",
488
+ "safe_audit_direct_session",
489
+ "safe_direct_summary",
490
+ "safe_finalize_direct_session",
453
491
  "search_workspace",
454
492
  "sync_file",
455
493
  ];
456
494
  if (JSON.stringify(enabledNames) !== JSON.stringify(expectedDirect)) {
457
495
  throw new Error(`chatgpt_direct enabled tools mismatch: ${enabledNames.join(", ")}`);
458
496
  }
459
- if (enabledTools._meta.tool_count !== 10) {
460
- throw new Error(`chatgpt_direct enabled tool_count should be 10, got ${enabledTools._meta.tool_count}`);
497
+ if (enabledTools._meta.tool_count !== 14) {
498
+ throw new Error(`chatgpt_direct enabled tool_count should be 14, got ${enabledTools._meta.tool_count}`);
461
499
  }
462
500
 
463
501
  // Minimal create_direct_session
@@ -470,6 +508,19 @@ try {
470
508
  if (!sessionResult.session_id || !sessionResult.session_id.startsWith("direct_")) {
471
509
  throw new Error(`create_direct_session failed: ${JSON.stringify(sessionResult)}`);
472
510
  }
511
+ const bundleResult = JSON.parse(
512
+ (await enabledClient.callTool({
513
+ name: "run_direct_verification_bundle",
514
+ arguments: { session_id: sessionResult.session_id, commands: ["npm test"], timeout_seconds: 30 },
515
+ })).content?.[0]?.text || "{}"
516
+ );
517
+ if (bundleResult.status !== "passed" || bundleResult.command_count !== 1 || bundleResult.passed_commands !== 1) {
518
+ throw new Error(`run_direct_verification_bundle failed: ${JSON.stringify(bundleResult)}`);
519
+ }
520
+ const bundleText = JSON.stringify(bundleResult).toLowerCase();
521
+ if (bundleText.includes("stdout_tail") || bundleText.includes("stderr_tail") || bundleText.includes("verification.log")) {
522
+ throw new Error("run_direct_verification_bundle leaked log tail fields");
523
+ }
473
524
 
474
525
  const enabledHealth = JSON.parse(
475
526
  (await enabledClient.callTool({ name: "health_check", arguments: {} })).content?.[0]?.text || "{}"
@@ -477,12 +528,12 @@ try {
477
528
  if (enabledHealth.direct_profile_enabled !== true) {
478
529
  throw new Error(`direct_profile_enabled should be true, got ${enabledHealth.direct_profile_enabled}`);
479
530
  }
480
- if (enabledHealth.direct_tool_count !== 10) {
481
- throw new Error(`direct_tool_count should be 10, got ${enabledHealth.direct_tool_count}`);
531
+ if (enabledHealth.direct_tool_count !== 14) {
532
+ throw new Error(`direct_tool_count should be 14, got ${enabledHealth.direct_tool_count}`);
482
533
  }
483
534
 
484
535
  await enabledClient.close();
485
- ok("chatgpt_direct enabled exposes 10 tools and create_direct_session works");
536
+ ok("chatgpt_direct enabled exposes 14 tools and create_direct_session works");
486
537
  } catch (error) {
487
538
  fail("MCP smoke test", error);
488
539
  } finally {
@@ -105,7 +105,7 @@ function createFixture(name) {
105
105
  server_version: "0.6.0",
106
106
  schema_epoch: "2026-06-22-v6",
107
107
  tool_profile: "chatgpt_core",
108
- tool_count: 16,
108
+ tool_count: 26,
109
109
  tool_names: [],
110
110
  tool_manifest_sha256: "a".repeat(64),
111
111
  });
@@ -167,7 +167,7 @@ function fixtureEnv(fixture, mode, lifetimeMs) {
167
167
  ...process.env,
168
168
  APPDATA: fixture.appData,
169
169
  LOCALAPPDATA: fixture.localAppData,
170
- CONTROL_PLANE_API_KEY: "watcher-supervisor-smoke-secret",
170
+ CONTROL_PLANE_API_KEY: "test-key",
171
171
  WATCHER_FIXTURE_MODE: mode,
172
172
  TUNNEL_FIXTURE_LIFETIME_MS: String(lifetimeMs),
173
173
  };
@@ -0,0 +1,149 @@
1
+ import { PatchWardenError } from "../errors.js";
2
+
3
+ // ── Types ─────────────────────────────────────────────────────────
4
+
5
+ export interface AgentRouteInput {
6
+ goal?: string;
7
+ scope?: string[];
8
+ inline_plan?: string;
9
+ template?: string;
10
+ configuredAgents: string[];
11
+ }
12
+
13
+ export interface AgentRouteResult {
14
+ recommended_agent: string;
15
+ reason: string;
16
+ fallback: boolean;
17
+ }
18
+
19
+ // ── Routing rules (roadmap 9.3) ───────────────────────────────────
20
+
21
+ export const AGENT_ROUTING_RULES = {
22
+ /** 大规模代码修改 → opencode */
23
+ largeScope: {
24
+ agent: "opencode",
25
+ threshold: 10, // scope.length > threshold
26
+ },
27
+ /** 单文件修改 → patchwarden-direct */
28
+ singleFile: {
29
+ agent: "patchwarden-direct",
30
+ },
31
+ /** 验收审计 → patchwarden-audit */
32
+ audit: {
33
+ agent: "patchwarden-audit",
34
+ keywords: ["审计", "audit", "验收", "verify"],
35
+ },
36
+ /** 复杂推理和重构 → codex */
37
+ refactor: {
38
+ agent: "codex",
39
+ keywords: ["重构", "refactor", "跨模块", "redesign"],
40
+ },
41
+ /** 文档整理 → claude(fallback 到 opencode) */
42
+ documentation: {
43
+ agent: "claude",
44
+ fallbackAgent: "opencode",
45
+ keywords: ["文档", "readme", "changelog", "doc"],
46
+ },
47
+ } as const;
48
+
49
+ // ── Helpers ───────────────────────────────────────────────────────
50
+
51
+ function containsAny(text: string, keywords: readonly string[]): boolean {
52
+ return keywords.some((keyword) => text.includes(keyword));
53
+ }
54
+
55
+ function resolveRoute(
56
+ preferred: string,
57
+ reason: string,
58
+ configuredAgents: string[]
59
+ ): AgentRouteResult {
60
+ if (configuredAgents.includes(preferred)) {
61
+ return { recommended_agent: preferred, reason, fallback: false };
62
+ }
63
+ const fallback = configuredAgents[0];
64
+ return {
65
+ recommended_agent: fallback,
66
+ reason: `fallback: ${preferred} not configured, using ${fallback}`,
67
+ fallback: true,
68
+ };
69
+ }
70
+
71
+ // ── Route agent ───────────────────────────────────────────────────
72
+
73
+ export function routeAgent(input: AgentRouteInput): AgentRouteResult {
74
+ if (!input.configuredAgents || input.configuredAgents.length === 0) {
75
+ throw new PatchWardenError(
76
+ "no_agent_configured",
77
+ "No agent is configured for routing.",
78
+ "Configure at least one agent in patchwarden.config.json under the agents field."
79
+ );
80
+ }
81
+
82
+ const scopeCount = input.scope?.length ?? 0;
83
+ const text = `${input.goal || ""} ${input.inline_plan || ""}`.toLowerCase();
84
+
85
+ // Rule 1: 大规模代码修改 → opencode (scope files > 10)
86
+ if (scopeCount > AGENT_ROUTING_RULES.largeScope.threshold) {
87
+ return resolveRoute(
88
+ AGENT_ROUTING_RULES.largeScope.agent,
89
+ `large scope (${scopeCount} files)`,
90
+ input.configuredAgents
91
+ );
92
+ }
93
+
94
+ // Rule 2: 单文件修改 → patchwarden-direct (scope files === 1)
95
+ if (scopeCount === 1) {
96
+ return resolveRoute(
97
+ AGENT_ROUTING_RULES.singleFile.agent,
98
+ "single file scope",
99
+ input.configuredAgents
100
+ );
101
+ }
102
+
103
+ // Rule 3: 验收审计 → patchwarden-audit
104
+ if (containsAny(text, AGENT_ROUTING_RULES.audit.keywords)) {
105
+ return resolveRoute(
106
+ AGENT_ROUTING_RULES.audit.agent,
107
+ "audit keywords",
108
+ input.configuredAgents
109
+ );
110
+ }
111
+
112
+ // Rule 4: 复杂推理和重构 → codex
113
+ if (containsAny(text, AGENT_ROUTING_RULES.refactor.keywords)) {
114
+ return resolveRoute(
115
+ AGENT_ROUTING_RULES.refactor.agent,
116
+ "refactor keywords",
117
+ input.configuredAgents
118
+ );
119
+ }
120
+
121
+ // Rule 5: 文档整理 → claude(fallback 到 opencode)
122
+ if (containsAny(text, AGENT_ROUTING_RULES.documentation.keywords)) {
123
+ const preferred = AGENT_ROUTING_RULES.documentation.agent;
124
+ const fallbackAgent = AGENT_ROUTING_RULES.documentation.fallbackAgent;
125
+ if (input.configuredAgents.includes(preferred)) {
126
+ return { recommended_agent: preferred, reason: "documentation keywords", fallback: false };
127
+ }
128
+ if (input.configuredAgents.includes(fallbackAgent)) {
129
+ return {
130
+ recommended_agent: fallbackAgent,
131
+ reason: `fallback: ${preferred} not configured, using ${fallbackAgent}`,
132
+ fallback: true,
133
+ };
134
+ }
135
+ const firstAgent = input.configuredAgents[0];
136
+ return {
137
+ recommended_agent: firstAgent,
138
+ reason: `fallback: ${preferred} not configured, using ${firstAgent}`,
139
+ fallback: true,
140
+ };
141
+ }
142
+
143
+ // Rule 6: 无匹配 → fallback 到 configuredAgents[0]
144
+ return {
145
+ recommended_agent: input.configuredAgents[0],
146
+ reason: `no specific routing rule matched, using default agent ${input.configuredAgents[0]}`,
147
+ fallback: true,
148
+ };
149
+ }
package/src/config.ts CHANGED
@@ -21,7 +21,7 @@ export interface PatchWardenConfig {
21
21
  defaultTaskTimeoutSeconds: number;
22
22
  maxTaskTimeoutSeconds: number;
23
23
  watcherStaleSeconds: number;
24
- toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct";
24
+ toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct" | "chatgpt_search";
25
25
  repoAliases?: Record<string, string>;
26
26
  enableAgentAssessment?: boolean;
27
27
  agentAssessmentTimeoutSeconds?: number;
@@ -224,8 +224,14 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
224
224
  if (!Number.isInteger(config.watcherStaleSeconds) || config.watcherStaleSeconds < 5 || config.watcherStaleSeconds > 3600) {
225
225
  throw new Error("watcherStaleSeconds must be an integer from 5 to 3600");
226
226
  }
227
- if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core" && config.toolProfile !== "chatgpt_direct") {
228
- throw new Error('toolProfile must be "full", "chatgpt_core", or "chatgpt_direct"');
227
+ if (
228
+ config.toolProfile !== undefined &&
229
+ config.toolProfile !== "full" &&
230
+ config.toolProfile !== "chatgpt_core" &&
231
+ config.toolProfile !== "chatgpt_direct" &&
232
+ config.toolProfile !== "chatgpt_search"
233
+ ) {
234
+ throw new Error('toolProfile must be "full", "chatgpt_core", "chatgpt_direct", or "chatgpt_search"');
229
235
  }
230
236
  if (config.enableAgentAssessment !== undefined && typeof config.enableAgentAssessment !== "boolean") {
231
237
  throw new Error("enableAgentAssessment must be a boolean");
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * PatchWarden Control Center — local HTTP dashboard service.
4
4
  *
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { createServer, get as httpGet, type IncomingMessage, type ServerResponse } from "node:http";
16
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
17
17
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { randomUUID } from "node:crypto";
@@ -27,11 +27,16 @@ import {
27
27
  type PatchWardenConfig,
28
28
  } from "./config.js";
29
29
  import { listTasks, type TaskEntry } from "./tools/listTasks.js";
30
+ import type { AcceptanceStatus } from "./tools/createTask.js";
30
31
  import { listAgents, type AgentAvailability } from "./tools/listAgents.js";
31
32
  import { readWatcherStatus, type WatcherStatusSnapshot } from "./watcherStatus.js";
32
33
  import { redactSensitiveContent } from "./security/contentRedaction.js";
33
34
  import { guardWorkspacePath } from "./security/pathGuard.js";
34
35
  import { auditTask } from "./tools/auditTask.js";
36
+ import { getProjectPolicySummary } from "./policy/projectPolicy.js";
37
+ import { safeDirectSummary } from "./tools/safeViews.js";
38
+ import { toSafeTaskLineage, type TaskLineageRecord } from "./tools/taskLineage.js";
39
+ import { listEvidencePacks, readEvidencePack } from "./tools/evidencePack.js";
35
40
  import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "./version.js";
36
41
 
37
42
  // ── Paths ─────────────────────────────────────────────────────────
@@ -249,6 +254,25 @@ function readJsonFileSafe<T = unknown>(filePath: string): T | null {
249
254
  }
250
255
  }
251
256
 
257
+ function isPathInside(root: string, target: string): boolean {
258
+ const rel = relative(root, target);
259
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
260
+ }
261
+
262
+ function readJsonFileSafeUnder<T = unknown>(root: string, relPath: string): T | null {
263
+ const base = resolve(root);
264
+ const target = resolve(base, relPath);
265
+ if (!isPathInside(base, target) || !existsSync(target)) return null;
266
+ try {
267
+ const realBase = realpathSync(base);
268
+ const realTarget = realpathSync(target);
269
+ if (!isPathInside(realBase, realTarget)) return null;
270
+ return readJsonFileSafe<T>(realTarget);
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
252
276
  function readTextFileSafe(filePath: string): string | null {
253
277
  if (!existsSync(filePath)) return null;
254
278
  try {
@@ -464,6 +488,7 @@ interface StaleClassification {
464
488
 
465
489
  const TERMINAL_TASK_STATUSES = new Set([
466
490
  "done",
491
+ "done_by_agent",
467
492
  "failed",
468
493
  "failed_verification",
469
494
  "failed_scope_violation",
@@ -1018,6 +1043,108 @@ function handleStaleTasks(res: ServerResponse): void {
1018
1043
  }
1019
1044
  }
1020
1045
 
1046
+ function handleLineages(res: ServerResponse): void {
1047
+ try {
1048
+ const root = join(config.workspaceRoot, ".patchwarden", "lineages");
1049
+ if (!existsSync(root)) {
1050
+ sendJson(res, 200, { lineages: [], total: 0, reason: null });
1051
+ return;
1052
+ }
1053
+ const lineages = readdirSync(root, { withFileTypes: true })
1054
+ .filter((entry) => entry.isDirectory())
1055
+ .map((entry) => readJsonFileSafeUnder<TaskLineageRecord>(root, join(entry.name, "lineage.json")))
1056
+ .filter((entry): entry is TaskLineageRecord => entry !== null)
1057
+ .map((entry) => toSafeTaskLineage(entry, 6))
1058
+ .sort((a, b) => b.updated_at.localeCompare(a.updated_at))
1059
+ .slice(0, 50);
1060
+ sendJson(res, 200, { lineages, total: lineages.length, reason: null });
1061
+ } catch (err) {
1062
+ sendJson(res, 200, { lineages: [], total: 0, reason: errorMessage(err) });
1063
+ }
1064
+ }
1065
+
1066
+ function handleLineageDetail(res: ServerResponse, lineageId: string): void {
1067
+ try {
1068
+ if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
1069
+ sendJson(res, 400, { error: "Invalid lineage id" });
1070
+ return;
1071
+ }
1072
+ const data = readJsonFileSafeUnder<TaskLineageRecord>(
1073
+ join(config.workspaceRoot, ".patchwarden", "lineages"),
1074
+ join(lineageId, "lineage.json")
1075
+ );
1076
+ if (!data) {
1077
+ sendJson(res, 404, { error: "Lineage not found" });
1078
+ return;
1079
+ }
1080
+ sendJson(res, 200, toSafeTaskLineage(data, 20));
1081
+ } catch (err) {
1082
+ sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err) });
1083
+ }
1084
+ }
1085
+
1086
+ function handleProjectPolicy(res: ServerResponse, repoPath: string): void {
1087
+ try {
1088
+ sendJson(res, 200, getProjectPolicySummary(repoPath || "."));
1089
+ } catch (err) {
1090
+ sendJson(res, 200, {
1091
+ repo_path: repoPath || ".",
1092
+ valid: false,
1093
+ issues: [{ code: "policy_unavailable", severity: "error", field: "repo_path", message: errorMessage(err) }],
1094
+ });
1095
+ }
1096
+ }
1097
+
1098
+ function handleReleaseStatus(res: ServerResponse, repoPath: string): void {
1099
+ try {
1100
+ const policy = getProjectPolicySummary(repoPath || ".");
1101
+ sendJson(res, 200, {
1102
+ repo_path: repoPath || ".",
1103
+ resolved_repo_path: policy.resolved_repo_path,
1104
+ policy_valid: policy.valid,
1105
+ policy_issue_count: policy.issues.length,
1106
+ policy_issues: policy.issues.slice(0, 10),
1107
+ release_readiness: policy.release_readiness,
1108
+ next_action: policy.valid && policy.release_readiness.version_consistent !== false
1109
+ ? "Run release_check or release_prepare from the full MCP profile when ready."
1110
+ : "Fix project-policy or version consistency issues before release preparation.",
1111
+ remote_write_performed: false,
1112
+ });
1113
+ } catch (err) {
1114
+ sendJson(res, 200, {
1115
+ repo_path: repoPath || ".",
1116
+ policy_valid: false,
1117
+ error: errorMessage(err),
1118
+ remote_write_performed: false,
1119
+ });
1120
+ }
1121
+ }
1122
+
1123
+ function handleEvidencePacks(res: ServerResponse): void {
1124
+ try {
1125
+ sendJson(res, 200, listEvidencePacks({ max_items: 50 }));
1126
+ } catch (err) {
1127
+ sendJson(res, 200, { evidence_packs: [], total: 0, reason: errorMessage(err) });
1128
+ }
1129
+ }
1130
+
1131
+ function handleEvidencePackDetail(res: ServerResponse, lineageId: string): void {
1132
+ try {
1133
+ if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
1134
+ sendJson(res, 400, { error: "Invalid lineage id" });
1135
+ return;
1136
+ }
1137
+ const pack = readEvidencePack(lineageId);
1138
+ if (!pack) {
1139
+ sendJson(res, 404, { error: "Evidence pack not found" });
1140
+ return;
1141
+ }
1142
+ sendJson(res, 200, pack);
1143
+ } catch (err) {
1144
+ sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err), bounded: true });
1145
+ }
1146
+ }
1147
+
1021
1148
  /**
1022
1149
  * Reconcile a stale task. Does NOT delete the task. Reads the task files,
1023
1150
  * decides whether it is safe to mark the task as stale/archived, writes a
@@ -1049,13 +1176,19 @@ function handleReconcile(res: ServerResponse, taskId: string): void {
1049
1176
  const runtimeData = readJsonFileSafe<Record<string, unknown>>(runtimePath) ?? {};
1050
1177
 
1051
1178
  const watcher = readWatcherStatusSafe();
1179
+ const VALID_ACCEPTANCE = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
1180
+ const taskStatus = String(statusData.status || "pending");
1181
+ const taskAcceptanceStatus = taskStatus === "done_by_agent"
1182
+ ? (typeof statusData.acceptance_status === "string" && VALID_ACCEPTANCE.includes(statusData.acceptance_status) ? (statusData.acceptance_status as AcceptanceStatus) : "pending" as AcceptanceStatus)
1183
+ : null;
1052
1184
  const taskEntry: TaskEntry = {
1053
1185
  task_id: taskId,
1054
1186
  plan_id: String(statusData.plan_id || ""),
1055
1187
  title: "",
1056
1188
  agent: String(statusData.agent || ""),
1057
- status: String(statusData.status || "pending") as TaskEntry["status"],
1189
+ status: taskStatus as TaskEntry["status"],
1058
1190
  phase: String(runtimeData.phase || statusData.phase || "queued") as TaskEntry["phase"],
1191
+ acceptance_status: taskAcceptanceStatus,
1059
1192
  created_at: String(statusData.created_at || ""),
1060
1193
  updated_at: String(statusData.updated_at || ""),
1061
1194
  workspace_root: String(statusData.workspace_root || config.workspaceRoot),
@@ -1199,13 +1332,19 @@ function handleTaskDetail(res: ServerResponse, taskId: string): void {
1199
1332
  let stale: StaleClassification | null = null;
1200
1333
  if (statusData) {
1201
1334
  const watcher = readWatcherStatusSafe();
1335
+ const VALID_ACCEPTANCE2 = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
1336
+ const taskStatus2 = String(statusData.status || "pending");
1337
+ const taskAcceptanceStatus2 = taskStatus2 === "done_by_agent"
1338
+ ? (typeof statusData.acceptance_status === "string" && VALID_ACCEPTANCE2.includes(statusData.acceptance_status) ? (statusData.acceptance_status as AcceptanceStatus) : "pending" as AcceptanceStatus)
1339
+ : null;
1202
1340
  const entry: TaskEntry = {
1203
1341
  task_id: taskId,
1204
1342
  plan_id: String(statusData.plan_id || ""),
1205
1343
  title: "",
1206
1344
  agent: String(statusData.agent || ""),
1207
- status: String(statusData.status || "pending") as TaskEntry["status"],
1345
+ status: taskStatus2 as TaskEntry["status"],
1208
1346
  phase: String(runtimeData?.phase || statusData.phase || "queued") as TaskEntry["phase"],
1347
+ acceptance_status: taskAcceptanceStatus2,
1209
1348
  created_at: String(statusData.created_at || ""),
1210
1349
  updated_at: String(statusData.updated_at || ""),
1211
1350
  workspace_root: String(statusData.workspace_root || config.workspaceRoot),
@@ -1641,6 +1780,29 @@ function handleDirectSessionDetail(res: ServerResponse, sessionId: string): void
1641
1780
  }
1642
1781
  }
1643
1782
 
1783
+ function handleDirectSessionSafeSummary(res: ServerResponse, sessionId: string): void {
1784
+ try {
1785
+ if (
1786
+ sessionId === "." ||
1787
+ sessionId === ".." ||
1788
+ sessionId.includes("/") ||
1789
+ sessionId.includes("\\") ||
1790
+ sessionId.includes("\0")
1791
+ ) {
1792
+ sendJson(res, 400, { error: "Invalid session id" });
1793
+ return;
1794
+ }
1795
+ sendJson(res, 200, safeDirectSummary(sessionId, { max_items: 12 }));
1796
+ } catch (err) {
1797
+ sendJson(res, 200, {
1798
+ session_id: sessionId,
1799
+ error: errorMessage(err),
1800
+ large_logs_omitted: true,
1801
+ diff_omitted: true,
1802
+ });
1803
+ }
1804
+ }
1805
+
1644
1806
  function parseReviewVerdict(content: string): string | null {
1645
1807
  // independent-review.md format: "**Verdict**: PASS" (case-insensitive)
1646
1808
  const m = content.match(/\*\*Verdict\*\*\s*:\s*([A-Za-z]+)/);
@@ -1938,6 +2100,44 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
1938
2100
  handleStaleTasks(res);
1939
2101
  return;
1940
2102
  }
2103
+ if (method === "GET" && pathname === "/api/lineages") {
2104
+ handleLineages(res);
2105
+ return;
2106
+ }
2107
+ const lineageMatch = pathname.match(/^\/api\/lineages\/([^/]+)$/);
2108
+ if (method === "GET" && lineageMatch) {
2109
+ let lineageId: string;
2110
+ try {
2111
+ lineageId = decodeURIComponent(lineageMatch[1]);
2112
+ } catch {
2113
+ lineageId = lineageMatch[1];
2114
+ }
2115
+ handleLineageDetail(res, lineageId);
2116
+ return;
2117
+ }
2118
+ if (method === "GET" && pathname === "/api/project-policy") {
2119
+ handleProjectPolicy(res, parsedUrl.searchParams.get("repo_path") || ".");
2120
+ return;
2121
+ }
2122
+ if (method === "GET" && pathname === "/api/release/status") {
2123
+ handleReleaseStatus(res, parsedUrl.searchParams.get("repo_path") || ".");
2124
+ return;
2125
+ }
2126
+ if (method === "GET" && pathname === "/api/evidence-packs") {
2127
+ handleEvidencePacks(res);
2128
+ return;
2129
+ }
2130
+ const evidencePackMatch = pathname.match(/^\/api\/evidence-packs\/([^/]+)$/);
2131
+ if (method === "GET" && evidencePackMatch) {
2132
+ let lineageId: string;
2133
+ try {
2134
+ lineageId = decodeURIComponent(evidencePackMatch[1]);
2135
+ } catch {
2136
+ lineageId = evidencePackMatch[1];
2137
+ }
2138
+ handleEvidencePackDetail(res, lineageId);
2139
+ return;
2140
+ }
1941
2141
  const taskMatch = pathname.match(/^\/api\/tasks\/([^/]+)$/);
1942
2142
  if (method === "GET" && taskMatch) {
1943
2143
  let taskId: string;
@@ -1996,6 +2196,17 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
1996
2196
  handleDirectSessions(res);
1997
2197
  return;
1998
2198
  }
2199
+ const directSessionSummaryMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/summary$/);
2200
+ if (method === "GET" && directSessionSummaryMatch) {
2201
+ let sessionId: string;
2202
+ try {
2203
+ sessionId = decodeURIComponent(directSessionSummaryMatch[1]);
2204
+ } catch {
2205
+ sessionId = directSessionSummaryMatch[1];
2206
+ }
2207
+ handleDirectSessionSafeSummary(res, sessionId);
2208
+ return;
2209
+ }
1999
2210
  const directSessionMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)$/);
2000
2211
  if (method === "GET" && directSessionMatch) {
2001
2212
  let sessionId: string;
@@ -40,6 +40,8 @@ export interface DirectSessionVerificationRun {
40
40
  exit_code: number | null;
41
41
  passed: boolean;
42
42
  timed_out: boolean;
43
+ redacted?: boolean;
44
+ redaction_categories?: string[];
43
45
  stdout_tail: string;
44
46
  stderr_tail: string;
45
47
  started_at: string;
@@ -69,6 +69,13 @@ export async function runDirectVerification(
69
69
  exit_code: result.exitCode,
70
70
  passed,
71
71
  timed_out: result.timedOut,
72
+ redacted: stdoutRedacted.redacted || stderrRedacted.redacted,
73
+ redaction_categories: [
74
+ ...new Set([
75
+ ...stdoutRedacted.redaction_categories,
76
+ ...stderrRedacted.redaction_categories,
77
+ ]),
78
+ ],
72
79
  stdout_tail: stdoutTail,
73
80
  stderr_tail: stderrTail,
74
81
  started_at: startedAt,