patchwarden 0.6.1 → 1.1.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 (261) hide show
  1. package/PatchWarden-Control-Tray.cmd +11 -0
  2. package/PatchWarden-Control.cmd +6 -0
  3. package/PatchWarden-Desktop.cmd +5 -0
  4. package/PatchWarden.cmd +1 -1
  5. package/README.en.md +112 -24
  6. package/README.md +36 -25
  7. package/Restart-PatchWarden-Control.cmd +6 -0
  8. package/Stop-PatchWarden.cmd +11 -0
  9. package/dist/agents/agentRouter.d.ts +40 -0
  10. package/dist/agents/agentRouter.js +95 -0
  11. package/dist/config.d.ts +1 -1
  12. package/dist/config.js +6 -2
  13. package/dist/controlCenter.d.ts +14 -0
  14. package/dist/controlCenter.js +2015 -0
  15. package/dist/doctor.js +35 -4
  16. package/dist/goal/acceptanceEngine.d.ts +62 -0
  17. package/dist/goal/acceptanceEngine.js +103 -0
  18. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  19. package/dist/goal/acceptanceTemplate.js +104 -0
  20. package/dist/goal/goalGraph.d.ts +58 -0
  21. package/dist/goal/goalGraph.js +189 -0
  22. package/dist/goal/goalProgress.d.ts +81 -0
  23. package/dist/goal/goalProgress.js +216 -0
  24. package/dist/goal/goalStatus.d.ts +60 -0
  25. package/dist/goal/goalStatus.js +137 -0
  26. package/dist/goal/goalStore.d.ts +79 -0
  27. package/dist/goal/goalStore.js +211 -0
  28. package/dist/goal/handoffExport.d.ts +34 -0
  29. package/dist/goal/handoffExport.js +183 -0
  30. package/dist/goal/subgoalSync.d.ts +40 -0
  31. package/dist/goal/subgoalSync.js +72 -0
  32. package/dist/goal/worktreeManager.d.ts +96 -0
  33. package/dist/goal/worktreeManager.js +292 -0
  34. package/dist/logging.d.ts +44 -0
  35. package/dist/logging.js +68 -0
  36. package/dist/release/releaseGate.d.ts +99 -0
  37. package/dist/release/releaseGate.js +475 -0
  38. package/dist/runner/postTaskCleanup.d.ts +13 -0
  39. package/dist/runner/postTaskCleanup.js +147 -0
  40. package/dist/runner/runTask.js +50 -10
  41. package/dist/security/discoveryTokenStore.d.ts +62 -0
  42. package/dist/security/discoveryTokenStore.js +100 -0
  43. package/dist/security/toolInvocationGuard.d.ts +35 -0
  44. package/dist/security/toolInvocationGuard.js +153 -0
  45. package/dist/smoke-test.js +18 -10
  46. package/dist/taskRuntime.d.ts +17 -0
  47. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  48. package/dist/test/unit/acceptance-engine.test.js +228 -0
  49. package/dist/test/unit/agent-router.test.d.ts +1 -0
  50. package/dist/test/unit/agent-router.test.js +287 -0
  51. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  52. package/dist/test/unit/audit-checks.test.js +350 -0
  53. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  54. package/dist/test/unit/diagnose-task.test.js +457 -0
  55. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  56. package/dist/test/unit/discovery-token-store.test.js +139 -0
  57. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  58. package/dist/test/unit/goal-graph.test.js +298 -0
  59. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  60. package/dist/test/unit/goal-progress.test.js +381 -0
  61. package/dist/test/unit/goal-status.test.d.ts +1 -0
  62. package/dist/test/unit/goal-status.test.js +215 -0
  63. package/dist/test/unit/goal-store.test.d.ts +1 -0
  64. package/dist/test/unit/goal-store.test.js +253 -0
  65. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  66. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  67. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  68. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  69. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  70. package/dist/test/unit/handoff-export.test.js +263 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  72. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  73. package/dist/test/unit/logging.test.js +127 -5
  74. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  75. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  76. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  77. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  78. package/dist/test/unit/release-gate.test.d.ts +1 -0
  79. package/dist/test/unit/release-gate.test.js +242 -0
  80. package/dist/test/unit/safe-views.test.d.ts +1 -0
  81. package/dist/test/unit/safe-views.test.js +171 -0
  82. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  83. package/dist/test/unit/schema-drift-check.test.js +175 -0
  84. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  85. package/dist/test/unit/subgoal-sync.test.js +183 -0
  86. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  87. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  88. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  89. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  90. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  91. package/dist/test/unit/toolSearch.test.js +571 -0
  92. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  93. package/dist/test/unit/worktree-manager.test.js +176 -0
  94. package/dist/tools/auditTask.d.ts +103 -1
  95. package/dist/tools/auditTask.js +461 -8
  96. package/dist/tools/cancelTask.js +1 -1
  97. package/dist/tools/checkReleaseGate.d.ts +21 -0
  98. package/dist/tools/checkReleaseGate.js +22 -0
  99. package/dist/tools/createTask.d.ts +18 -2
  100. package/dist/tools/createTask.js +29 -1
  101. package/dist/tools/diagnoseTask.d.ts +45 -0
  102. package/dist/tools/diagnoseTask.js +347 -0
  103. package/dist/tools/discardWorktree.d.ts +24 -0
  104. package/dist/tools/discardWorktree.js +27 -0
  105. package/dist/tools/discoverTools.d.ts +11 -0
  106. package/dist/tools/discoverTools.js +39 -0
  107. package/dist/tools/explainTool.d.ts +11 -0
  108. package/dist/tools/explainTool.js +21 -0
  109. package/dist/tools/getTaskSummary.js +2 -1
  110. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  111. package/dist/tools/goalSubgoalTask.js +110 -0
  112. package/dist/tools/healthCheck.d.ts +1 -0
  113. package/dist/tools/healthCheck.js +5 -1
  114. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  115. package/dist/tools/invokeDiscoveredTool.js +112 -0
  116. package/dist/tools/listTasks.d.ts +3 -1
  117. package/dist/tools/listTasks.js +14 -1
  118. package/dist/tools/mergeWorktree.d.ts +24 -0
  119. package/dist/tools/mergeWorktree.js +27 -0
  120. package/dist/tools/reconcileTasks.d.ts +41 -0
  121. package/dist/tools/reconcileTasks.js +352 -0
  122. package/dist/tools/registry.js +550 -0
  123. package/dist/tools/safeStatus.d.ts +31 -1
  124. package/dist/tools/safeStatus.js +43 -1
  125. package/dist/tools/safeViews.d.ts +256 -0
  126. package/dist/tools/safeViews.js +250 -0
  127. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  128. package/dist/tools/schemaDriftCheck.js +80 -0
  129. package/dist/tools/toolCatalog.d.ts +4 -3
  130. package/dist/tools/toolCatalog.js +33 -11
  131. package/dist/tools/toolRegistry.d.ts +61 -0
  132. package/dist/tools/toolRegistry.js +724 -0
  133. package/dist/tools/toolSearch.d.ts +110 -0
  134. package/dist/tools/toolSearch.js +331 -0
  135. package/dist/tools/toolUsageStats.d.ts +41 -0
  136. package/dist/tools/toolUsageStats.js +116 -0
  137. package/dist/tools/waitForTask.js +1 -0
  138. package/dist/version.d.ts +2 -2
  139. package/dist/version.js +2 -2
  140. package/dist/watcherStatus.d.ts +29 -0
  141. package/dist/watcherStatus.js +92 -1
  142. package/docs/control-center/README.md +33 -0
  143. package/docs/control-center/control-center-daily-driver.md +211 -0
  144. package/docs/control-center/control-center-mvp.md +205 -0
  145. package/docs/control-center/control-center-phase2.md +159 -0
  146. package/docs/demo.md +3 -0
  147. package/docs/release-v0.6.4.md +45 -0
  148. package/examples/openai-tunnel/README.md +5 -5
  149. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  150. package/package.json +25 -16
  151. package/scripts/README.md +47 -0
  152. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  153. package/scripts/checks/control-center-smoke.js +1098 -0
  154. package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
  155. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
  156. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
  157. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
  158. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
  159. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
  160. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  161. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  162. package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
  163. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
  164. package/scripts/control/control-center-tray.ps1 +281 -0
  165. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  166. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  167. package/scripts/control/restart-control-center.ps1 +173 -0
  168. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  169. package/scripts/control/start-control-center.ps1 +263 -0
  170. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  171. package/scripts/control/stop-patchwarden.ps1 +114 -0
  172. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  173. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  174. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  175. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  176. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  177. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  178. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  179. package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
  180. package/src/agents/agentRouter.ts +149 -0
  181. package/src/config.ts +9 -3
  182. package/src/controlCenter.ts +2166 -0
  183. package/src/doctor.ts +40 -5
  184. package/src/goal/acceptanceEngine.ts +160 -0
  185. package/src/goal/acceptanceTemplate.ts +121 -0
  186. package/src/goal/goalGraph.ts +225 -0
  187. package/src/goal/goalProgress.ts +301 -0
  188. package/src/goal/goalStatus.ts +234 -0
  189. package/src/goal/goalStore.ts +306 -0
  190. package/src/goal/handoffExport.ts +211 -0
  191. package/src/goal/subgoalSync.ts +82 -0
  192. package/src/goal/worktreeManager.ts +404 -0
  193. package/src/logging.ts +91 -0
  194. package/src/release/releaseGate.ts +567 -0
  195. package/src/runner/postTaskCleanup.ts +154 -0
  196. package/src/runner/runTask.ts +49 -10
  197. package/src/security/discoveryTokenStore.ts +157 -0
  198. package/src/security/toolInvocationGuard.ts +251 -0
  199. package/src/smoke-test.ts +15 -7
  200. package/src/taskRuntime.ts +17 -0
  201. package/src/test/unit/acceptance-engine.test.ts +261 -0
  202. package/src/test/unit/agent-router.test.ts +342 -0
  203. package/src/test/unit/audit-checks.test.ts +567 -0
  204. package/src/test/unit/diagnose-task.test.ts +544 -0
  205. package/src/test/unit/discovery-token-store.test.ts +181 -0
  206. package/src/test/unit/goal-graph.test.ts +347 -0
  207. package/src/test/unit/goal-progress.test.ts +538 -0
  208. package/src/test/unit/goal-status.test.ts +270 -0
  209. package/src/test/unit/goal-store.test.ts +318 -0
  210. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  211. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  212. package/src/test/unit/handoff-export.test.ts +295 -0
  213. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  214. package/src/test/unit/logging.test.ts +177 -5
  215. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  216. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  217. package/src/test/unit/release-gate.test.ts +314 -0
  218. package/src/test/unit/safe-views.test.ts +184 -0
  219. package/src/test/unit/schema-drift-check.test.ts +258 -0
  220. package/src/test/unit/subgoal-sync.test.ts +236 -0
  221. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  222. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  223. package/src/test/unit/toolSearch.test.ts +756 -0
  224. package/src/test/unit/worktree-manager.test.ts +247 -0
  225. package/src/tools/auditTask.ts +831 -402
  226. package/src/tools/cancelTask.ts +1 -1
  227. package/src/tools/checkReleaseGate.ts +45 -0
  228. package/src/tools/createTask.ts +64 -6
  229. package/src/tools/diagnoseTask.ts +460 -0
  230. package/src/tools/discardWorktree.ts +42 -0
  231. package/src/tools/discoverTools.ts +51 -0
  232. package/src/tools/explainTool.ts +26 -0
  233. package/src/tools/getTaskSummary.ts +2 -1
  234. package/src/tools/goalSubgoalTask.ts +170 -0
  235. package/src/tools/healthCheck.ts +4 -1
  236. package/src/tools/invokeDiscoveredTool.ts +163 -0
  237. package/src/tools/listTasks.ts +16 -2
  238. package/src/tools/mergeWorktree.ts +42 -0
  239. package/src/tools/reconcileTasks.ts +464 -0
  240. package/src/tools/registry.ts +618 -1
  241. package/src/tools/safeStatus.ts +73 -2
  242. package/src/tools/safeViews.ts +271 -0
  243. package/src/tools/schemaDriftCheck.ts +120 -0
  244. package/src/tools/toolCatalog.ts +34 -11
  245. package/src/tools/toolRegistry.ts +786 -0
  246. package/src/tools/toolSearch.ts +464 -0
  247. package/src/tools/toolUsageStats.ts +151 -0
  248. package/src/tools/waitForTask.ts +1 -0
  249. package/src/version.ts +2 -2
  250. package/src/watcherStatus.ts +97 -1
  251. package/ui/colors_and_type.css +141 -0
  252. package/ui/pages/audit.html +743 -0
  253. package/ui/pages/dashboard.html +1154 -0
  254. package/ui/pages/direct-sessions.html +652 -0
  255. package/ui/pages/logs.html +502 -0
  256. package/ui/pages/task-detail.html +1229 -0
  257. package/ui/pages/tasks.html +702 -0
  258. package/ui/pages/workspace.html +947 -0
  259. package/ui/partials/project-shell.html +362 -0
  260. package/ui/vendor/lucide.js +12 -0
  261. package/ui/vendor/tailwindcss-browser.js +947 -0
package/src/doctor.ts CHANGED
@@ -19,6 +19,9 @@ import { guardPlanContent } from "./security/planGuard.js";
19
19
  import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
20
20
  import { getToolDefs } from "./tools/registry.js";
21
21
  import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
22
+ import { buildToolRegistry } from "./tools/toolRegistry.js";
23
+ import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
24
+ import { runReleaseGateCheck } from "./release/releaseGate.js";
22
25
  import { PATCHWARDEN_VERSION } from "./version.js";
23
26
 
24
27
  // ── State ──────────────────────────────────────────────────────────
@@ -235,8 +238,8 @@ check(
235
238
  const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
236
239
  check("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION,
237
240
  `${PATCHWARDEN_VERSION} vs ${packageJson.version}`);
238
- check("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/mcp-manifest-check.js")),
239
- "scripts/mcp-manifest-check.js");
241
+ check("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")),
242
+ "scripts/checks/mcp-manifest-check.js");
240
243
 
241
244
  const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
242
245
  try {
@@ -245,7 +248,7 @@ try {
245
248
  const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
246
249
  const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema as any;
247
250
  const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema as any;
248
- check("Full tool profile exposes 30 tools", fullTools.length === 30, `${fullTools.length} tools`);
251
+ check("Full tool profile exposes 54 tools", fullTools.length === 54, `${fullTools.length} tools`);
249
252
  check(
250
253
  `chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`,
251
254
  JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES),
@@ -280,11 +283,43 @@ try {
280
283
  results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
281
284
  ok++;
282
285
  }
286
+
287
+ // 16. Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
288
+ // 用 fullTools 同时构建 registry 和 toolDefs Map,确保 schema digest 比对基准一致。
289
+ const driftRegistry = buildToolRegistry(fullTools);
290
+ const driftToolDefs = new Map<string, { inputSchema: unknown }>();
291
+ for (const tool of fullTools) {
292
+ driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
293
+ }
294
+ const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
295
+ if (driftResult.ok) {
296
+ results.push(`[OK] Schema drift check — no drift detected`);
297
+ ok++;
298
+ } else {
299
+ // 每个 warning 单独输出为 WARN,与其他自检项的多警告模式一致
300
+ for (const w of driftResult.warnings) {
301
+ results.push(`[WARN] Schema drift: ${w}`);
302
+ warn++;
303
+ }
304
+ }
283
305
  } finally {
284
306
  if (previousProfile === undefined) delete process.env.PATCHWARDEN_TOOL_PROFILE;
285
307
  else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
286
308
  }
287
309
 
310
+ // 17. Release gate module loadable (v1.0.0) — module integrity only.
311
+ // Does NOT execute local_ready (would recurse into doctor:ci).
312
+ try {
313
+ const releaseGateReady = typeof runReleaseGateCheck === "function";
314
+ check(
315
+ "Release gate module loadable",
316
+ releaseGateReady,
317
+ releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid",
318
+ );
319
+ } catch (error) {
320
+ check("Release gate module loadable", false, error instanceof Error ? error.message : String(error));
321
+ }
322
+
288
323
  // 9. HTTP port check
289
324
  const httpPort = (config as any)?.http?.port || 7331;
290
325
  try {
@@ -477,9 +512,9 @@ const tunnelFiles = [
477
512
  "examples/openai-tunnel/README.md",
478
513
  "examples/openai-tunnel/tunnel-client.example.yaml",
479
514
  "examples/openai-tunnel/chatgpt-test-prompt.md",
480
- "scripts/patchwarden-mcp-direct.cmd",
515
+ "scripts/mcp/patchwarden-mcp-direct.cmd",
481
516
  "PatchWarden.cmd",
482
- "scripts/manage-patchwarden.ps1",
517
+ "scripts/control/manage-patchwarden.ps1",
483
518
  "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
484
519
  ];
485
520
  for (const tf of tunnelFiles) {
@@ -0,0 +1,160 @@
1
+ /**
2
+ * v0.7.2: 验收引擎 — 基于证据推导 4 级验收结论。
3
+ *
4
+ * 输入:status.json、task_meta(goal/scope/forbidden/verification/done_evidence)、
5
+ * audit checks(pass/warn/fail)、release claims、artifact 信息
6
+ * 输出:ACCEPTED / NEEDS_FIX / REJECTED / BLOCKED_BY_APPROVAL
7
+ * + reasons + required_evidence + next_suggested_task
8
+ *
9
+ * 决策规则:
10
+ * 1. 存在 fail 级检查项 → REJECTED
11
+ * 2. 存在 release claims 但无法验证远端 → BLOCKED_BY_APPROVAL
12
+ * 3. 存在 warn 级检查项或文档不同步 → NEEDS_FIX
13
+ * 4. 所有检查通过 → ACCEPTED
14
+ */
15
+
16
+ import type { AcceptanceStatus } from "../tools/createTask.js";
17
+
18
+ // ── 类型定义 ──────────────────────────────────────────────────────
19
+
20
+ export type AcceptanceVerdict = "ACCEPTED" | "NEEDS_FIX" | "REJECTED" | "BLOCKED_BY_APPROVAL";
21
+
22
+ export interface AcceptanceCheck {
23
+ name: string;
24
+ result: "pass" | "warn" | "fail";
25
+ detail: string;
26
+ }
27
+
28
+ export interface AcceptanceEvidence {
29
+ task_id: string;
30
+ task_status: string;
31
+ // 证据文件是否存在
32
+ result_md_exists: boolean;
33
+ result_json_exists: boolean;
34
+ verify_json_exists: boolean;
35
+ test_log_exists: boolean;
36
+ git_diff_exists: boolean;
37
+ // 验证状态
38
+ verify_status: "passed" | "failed" | "skipped" | null;
39
+ // 范围违规
40
+ new_out_of_scope_changes: Array<{ path: string; change: string }>;
41
+ // 任务元数据(来自 create_task 的结构化验收标准)
42
+ goal: string | null;
43
+ scope: string[] | null;
44
+ forbidden: string[] | null;
45
+ verification: string[] | null;
46
+ done_evidence: string[] | null;
47
+ // artifact 信息
48
+ artifact_status: string | null;
49
+ // release claims(auditTask 扫描到的发布声明)
50
+ release_claims_unverified: boolean;
51
+ // audit checks(来自 auditTask 的完整检查列表)
52
+ checks: AcceptanceCheck[];
53
+ }
54
+
55
+ export interface AcceptanceResult {
56
+ verdict: AcceptanceVerdict;
57
+ acceptance_status: AcceptanceStatus;
58
+ reason: string;
59
+ reasons: string[];
60
+ required_evidence: string[];
61
+ next_suggested_task: string;
62
+ fail_checks: AcceptanceCheck[];
63
+ warn_checks: AcceptanceCheck[];
64
+ }
65
+
66
+ // ── 验收决策 ──────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * 核心验收决策函数。
70
+ * 输入所有证据,输出 4 级验收结论。
71
+ */
72
+ export function evaluateAcceptance(evidence: AcceptanceEvidence): AcceptanceResult {
73
+ const reasons: string[] = [];
74
+ const requiredEvidence: string[] = [];
75
+ const failChecks: AcceptanceCheck[] = [];
76
+ const warnChecks: AcceptanceCheck[] = [];
77
+
78
+ // 收集 fail 和 warn 检查项
79
+ for (const check of evidence.checks) {
80
+ if (check.result === "fail") {
81
+ failChecks.push(check);
82
+ } else if (check.result === "warn") {
83
+ warnChecks.push(check);
84
+ }
85
+ }
86
+
87
+ // ── Rule 1: fail 级检查项 → REJECTED ──────────────────────
88
+ if (failChecks.length > 0) {
89
+ for (const check of failChecks) {
90
+ reasons.push(`[FAIL] ${check.name}: ${check.detail}`);
91
+ }
92
+ return {
93
+ verdict: "REJECTED",
94
+ acceptance_status: "rejected",
95
+ reason: `${failChecks.length} fail-level check(s): ${failChecks.map((c) => c.name).join(", ")}`,
96
+ reasons,
97
+ required_evidence: requiredEvidence.length > 0 ? requiredEvidence : ["Fix all fail-level issues and re-run audit_task"],
98
+ next_suggested_task: `Fix fail-level issues: ${failChecks.map((c) => c.name).join(", ")}`,
99
+ fail_checks: failChecks,
100
+ warn_checks: warnChecks,
101
+ };
102
+ }
103
+
104
+ // ── Rule 2: release claims 无法验证 → BLOCKED_BY_APPROVAL ──
105
+ if (evidence.release_claims_unverified) {
106
+ reasons.push("[BLOCKED] Release claims detected but remote state cannot be verified");
107
+ requiredEvidence.push("Verify remote release state (npm publish, GitHub release, git tag)");
108
+ return {
109
+ verdict: "BLOCKED_BY_APPROVAL",
110
+ acceptance_status: "blocked",
111
+ reason: "Release claims require manual verification of remote state",
112
+ reasons,
113
+ required_evidence: requiredEvidence,
114
+ next_suggested_task: "Manually verify remote release state, then re-run audit_task",
115
+ fail_checks: failChecks,
116
+ warn_checks: warnChecks,
117
+ };
118
+ }
119
+
120
+ // ── Rule 3: warn 级检查项 → NEEDS_FIX ──────────────────────
121
+ if (warnChecks.length > 0) {
122
+ for (const check of warnChecks) {
123
+ reasons.push(`[WARN] ${check.name}: ${check.detail}`);
124
+ }
125
+ return {
126
+ verdict: "NEEDS_FIX",
127
+ acceptance_status: "needs_fix",
128
+ reason: `${warnChecks.length} warn-level check(s) require fixes: ${warnChecks.map((c) => c.name).join(", ")}`,
129
+ reasons,
130
+ required_evidence: requiredEvidence.length > 0 ? requiredEvidence : ["Resolve all warn-level issues"],
131
+ next_suggested_task: `Fix warn-level issues: ${warnChecks.map((c) => c.name).join(", ")}`,
132
+ fail_checks: failChecks,
133
+ warn_checks: warnChecks,
134
+ };
135
+ }
136
+
137
+ // ── Rule 4: 所有检查通过 → ACCEPTED ───────────────────────
138
+ return {
139
+ verdict: "ACCEPTED",
140
+ acceptance_status: "accepted",
141
+ reason: "All acceptance checks passed",
142
+ reasons: ["All checks passed, no warnings or failures"],
143
+ required_evidence: [],
144
+ next_suggested_task: "Task accepted. Proceed to next goal or task.",
145
+ fail_checks: failChecks,
146
+ warn_checks: warnChecks,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * 将 AcceptanceVerdict 映射到 AcceptanceStatus(用于回写 status.json)。
152
+ */
153
+ export function verdictToStatus(verdict: AcceptanceVerdict): AcceptanceStatus {
154
+ switch (verdict) {
155
+ case "ACCEPTED": return "accepted";
156
+ case "REJECTED": return "rejected";
157
+ case "NEEDS_FIX": return "needs_fix";
158
+ case "BLOCKED_BY_APPROVAL": return "blocked";
159
+ }
160
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * v0.7.2: ACCEPTANCE.md 模板生成 — 人类可读的验收报告导出。
3
+ *
4
+ * 将 AcceptanceResult 渲染为 Markdown,落盘到 .patchwarden/tasks/{task_id}/ACCEPTANCE.md。
5
+ */
6
+
7
+ import type { AcceptanceResult, AcceptanceEvidence } from "./acceptanceEngine.js";
8
+ import { PATCHWARDEN_VERSION } from "../version.js";
9
+
10
+ /**
11
+ * 生成 ACCEPTANCE.md 内容。
12
+ */
13
+ export function renderAcceptanceMarkdown(
14
+ taskId: string,
15
+ result: AcceptanceResult,
16
+ evidence: AcceptanceEvidence
17
+ ): string {
18
+ const lines: string[] = [];
19
+ const now = new Date().toISOString();
20
+
21
+ lines.push("# Acceptance Report — " + taskId);
22
+ lines.push("");
23
+ lines.push("Generated: " + now);
24
+ lines.push("");
25
+
26
+ // ── Verdict ──────────────────────────────────────────────
27
+ lines.push("## Verdict");
28
+ lines.push("");
29
+ lines.push("| Field | Value |");
30
+ lines.push("| --- | --- |");
31
+ lines.push("| Verdict | **" + result.verdict + "** |");
32
+ lines.push("| Acceptance Status | " + (result.acceptance_status ?? "null") + " |");
33
+ lines.push("| Reason | " + result.reason + " |");
34
+ lines.push("| Next Suggested Task | " + result.next_suggested_task + " |");
35
+ lines.push("");
36
+
37
+ // ── Evidence Summary ────────────────────────────────────
38
+ lines.push("## Evidence Summary");
39
+ lines.push("");
40
+ lines.push("| Evidence | Present |");
41
+ lines.push("| --- | --- |");
42
+ lines.push("| result.md | " + (evidence.result_md_exists ? "YES" : "NO") + " |");
43
+ lines.push("| result.json | " + (evidence.result_json_exists ? "YES" : "NO") + " |");
44
+ lines.push("| verify.json | " + (evidence.verify_json_exists ? "YES" : "NO") + " |");
45
+ lines.push("| test.log | " + (evidence.test_log_exists ? "YES" : "NO") + " |");
46
+ lines.push("| git.diff | " + (evidence.git_diff_exists ? "YES" : "NO") + " |");
47
+ lines.push("| verify_status | " + (evidence.verify_status ?? "N/A") + " |");
48
+ lines.push("| artifact_status | " + (evidence.artifact_status ?? "N/A") + " |");
49
+ lines.push("| out_of_scope_changes | " + evidence.new_out_of_scope_changes.length + " |");
50
+ lines.push("| release_claims_unverified | " + (evidence.release_claims_unverified ? "YES" : "NO") + " |");
51
+ lines.push("");
52
+
53
+ // ── Task Metadata ────────────────────────────────────────
54
+ if (evidence.goal || evidence.scope || evidence.forbidden || evidence.verification || evidence.done_evidence) {
55
+ lines.push("## Task Acceptance Criteria");
56
+ lines.push("");
57
+ if (evidence.goal) {
58
+ lines.push("- **Goal**: " + evidence.goal);
59
+ }
60
+ if (evidence.scope && evidence.scope.length > 0) {
61
+ lines.push("- **Scope**: " + evidence.scope.join(", "));
62
+ }
63
+ if (evidence.forbidden && evidence.forbidden.length > 0) {
64
+ lines.push("- **Forbidden**: " + evidence.forbidden.join(", "));
65
+ }
66
+ if (evidence.verification && evidence.verification.length > 0) {
67
+ lines.push("- **Verification**: " + evidence.verification.join(", "));
68
+ }
69
+ if (evidence.done_evidence && evidence.done_evidence.length > 0) {
70
+ lines.push("- **Done Evidence**: " + evidence.done_evidence.join(", "));
71
+ }
72
+ lines.push("");
73
+ }
74
+
75
+ // ── Fail Checks ─────────────────────────────────────────
76
+ if (result.fail_checks.length > 0) {
77
+ lines.push("## Fail-Level Checks");
78
+ lines.push("");
79
+ for (const check of result.fail_checks) {
80
+ lines.push("- **" + check.name + "**: " + check.detail);
81
+ }
82
+ lines.push("");
83
+ }
84
+
85
+ // ── Warn Checks ─────────────────────────────────────────
86
+ if (result.warn_checks.length > 0) {
87
+ lines.push("## Warn-Level Checks");
88
+ lines.push("");
89
+ for (const check of result.warn_checks) {
90
+ lines.push("- **" + check.name + "**: " + check.detail);
91
+ }
92
+ lines.push("");
93
+ }
94
+
95
+ // ── Reasons ─────────────────────────────────────────────
96
+ if (result.reasons.length > 0) {
97
+ lines.push("## Reasons");
98
+ lines.push("");
99
+ for (const reason of result.reasons) {
100
+ lines.push("- " + reason);
101
+ }
102
+ lines.push("");
103
+ }
104
+
105
+ // ── Required Evidence ────────────────────────────────────
106
+ if (result.required_evidence.length > 0) {
107
+ lines.push("## Required Evidence");
108
+ lines.push("");
109
+ for (const reqEvidence of result.required_evidence) {
110
+ lines.push("- " + reqEvidence);
111
+ }
112
+ lines.push("");
113
+ }
114
+
115
+ lines.push("---");
116
+ lines.push("");
117
+ lines.push("This report was generated by PatchWarden v" + PATCHWARDEN_VERSION + " acceptanceEngine.");
118
+ lines.push("");
119
+
120
+ return lines.join("\n");
121
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * v0.8.0: Goal Session 依赖图 — 就绪/阻塞判定、下一子目标建议、环检测、拓扑排序。
3
+ *
4
+ * 所有函数均为纯函数,不修改传入的 goalStatus 对象。
5
+ * 依赖语义:subgoal.depends_on 引用其他 subgoal 的 id,被引用者 status 必须为
6
+ * "accepted" 才算依赖满足。引用不存在的 id 视为数据不一致,按阻塞处理。
7
+ */
8
+
9
+ import type { GoalStatus, Subgoal } from "./goalStatus.js";
10
+ import { PatchWardenError } from "../errors.js";
11
+
12
+ // ── 内部工具 ──────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * 构建 subgoal_id → Subgoal 的查找表。
16
+ */
17
+ function buildSubgoalMap(goalStatus: GoalStatus): Map<string, Subgoal> {
18
+ const map = new Map<string, Subgoal>();
19
+ for (const s of goalStatus.subgoals) {
20
+ map.set(s.id, s);
21
+ }
22
+ return map;
23
+ }
24
+
25
+ /**
26
+ * 判断单个 subgoal 的依赖是否全部满足(所有 depends_on 引用存在且 status 为 accepted)。
27
+ * depends_on 为空视为依赖满足。引用不存在的 id 视为未满足。
28
+ */
29
+ function areDependenciesMet(subgoal: Subgoal, subgoalMap: Map<string, Subgoal>): boolean {
30
+ for (const depId of subgoal.depends_on) {
31
+ const dep = subgoalMap.get(depId);
32
+ if (!dep) return false;
33
+ if (dep.status !== "accepted") return false;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ // ── 公共 API ──────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * 返回所有 status 为 "ready" 且依赖全部满足的子目标。
42
+ * 依赖满足 = depends_on 中所有 id 都存在且对应 subgoal status 为 "accepted"。
43
+ * depends_on 为空数组视为依赖满足。
44
+ * depends_on 引用不存在的 subgoal 时,该子目标不算 ready(跳过)。
45
+ */
46
+ export function getReadySubgoals(goalStatus: GoalStatus): Subgoal[] {
47
+ const subgoalMap = buildSubgoalMap(goalStatus);
48
+ return goalStatus.subgoals.filter(
49
+ (s) => s.status === "ready" && areDependenciesMet(s, subgoalMap)
50
+ );
51
+ }
52
+
53
+ /**
54
+ * 返回所有 status 为 "ready" 但依赖未全部满足的子目标,附带阻塞它的依赖 id 列表。
55
+ * blocked_by 包含 depends_on 中 status 不为 "accepted" 的 subgoal_id,
56
+ * 以及引用不存在的 id。
57
+ */
58
+ export function getBlockedSubgoals(
59
+ goalStatus: GoalStatus
60
+ ): Array<{ subgoal: Subgoal; blocked_by: string[] }> {
61
+ const subgoalMap = buildSubgoalMap(goalStatus);
62
+ const result: Array<{ subgoal: Subgoal; blocked_by: string[] }> = [];
63
+
64
+ for (const s of goalStatus.subgoals) {
65
+ if (s.status !== "ready") continue;
66
+ const blockedBy: string[] = [];
67
+ for (const depId of s.depends_on) {
68
+ const dep = subgoalMap.get(depId);
69
+ if (!dep) {
70
+ blockedBy.push(depId);
71
+ } else if (dep.status !== "accepted") {
72
+ blockedBy.push(depId);
73
+ }
74
+ }
75
+ if (blockedBy.length > 0) {
76
+ result.push({ subgoal: s, blocked_by: blockedBy });
77
+ }
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * 建议下一个可执行的子目标。
85
+ *
86
+ * 优先级:
87
+ * 1. 有 ready 子目标 → 返回第一个(按 subgoal 添加顺序)的 { subgoal_id, title, depends_on }
88
+ * 2. 无 ready 但有 blocked → 返回 { subgoal_id: null, reason: "dependencies_not_met", blocked_by: [...] }
89
+ * 3. 无 ready 无 blocked → 返回 { subgoal_id: null, reason: "no_ready_subgoal" }
90
+ */
91
+ export function suggestNextSubgoal(goalStatus: GoalStatus): {
92
+ subgoal_id: string | null;
93
+ title?: string;
94
+ depends_on?: string[];
95
+ reason?: string;
96
+ blocked_by?: string[];
97
+ } {
98
+ const ready = getReadySubgoals(goalStatus);
99
+ if (ready.length > 0) {
100
+ const first = ready[0];
101
+ return {
102
+ subgoal_id: first.id,
103
+ title: first.title,
104
+ depends_on: [...first.depends_on],
105
+ };
106
+ }
107
+
108
+ const blocked = getBlockedSubgoals(goalStatus);
109
+ if (blocked.length > 0) {
110
+ return {
111
+ subgoal_id: null,
112
+ reason: "dependencies_not_met",
113
+ blocked_by: blocked.map((b) => b.subgoal.id),
114
+ };
115
+ }
116
+
117
+ return {
118
+ subgoal_id: null,
119
+ reason: "no_ready_subgoal",
120
+ };
121
+ }
122
+
123
+ /**
124
+ * 检测 subgoal 依赖图是否有环(DFS 三色标记法)。
125
+ *
126
+ * 沿 depends_on 深度优先遍历,遇到当前路径上正在访问的节点即为环。
127
+ * depends_on 引用不存在的 subgoal 不算环(忽略)。
128
+ *
129
+ * @returns 有环时返回环上的 subgoal_id 数组(任一环即可);无环返回 null。
130
+ */
131
+ export function detectCycle(goalStatus: GoalStatus): string[] | null {
132
+ const subgoalMap = buildSubgoalMap(goalStatus);
133
+
134
+ const WHITE = 0;
135
+ const GRAY = 1;
136
+ const BLACK = 2;
137
+ const color = new Map<string, number>();
138
+ for (const s of goalStatus.subgoals) {
139
+ color.set(s.id, WHITE);
140
+ }
141
+
142
+ let cycle: string[] | null = null;
143
+
144
+ const dfs = (id: string, path: string[]): boolean => {
145
+ color.set(id, GRAY);
146
+ path.push(id);
147
+
148
+ const node = subgoalMap.get(id);
149
+ if (node) {
150
+ for (const depId of node.depends_on) {
151
+ if (!subgoalMap.has(depId)) continue; // 引用不存在,忽略
152
+ const depColor = color.get(depId);
153
+ if (depColor === GRAY) {
154
+ // 发现环:截取从依赖节点到当前节点的路径
155
+ const cycleStart = path.indexOf(depId);
156
+ cycle = path.slice(cycleStart);
157
+ return true;
158
+ }
159
+ if (depColor === WHITE) {
160
+ if (dfs(depId, path)) return true;
161
+ }
162
+ }
163
+ }
164
+
165
+ path.pop();
166
+ color.set(id, BLACK);
167
+ return false;
168
+ };
169
+
170
+ for (const s of goalStatus.subgoals) {
171
+ if (color.get(s.id) === WHITE) {
172
+ if (dfs(s.id, [])) return cycle;
173
+ }
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * 对 subgoal 做拓扑排序(依赖在前)。
181
+ *
182
+ * 如果 B depends_on A,则 A 排在 B 之前。
183
+ * depends_on 引用不存在的 id 会被忽略(不影响排序)。
184
+ *
185
+ * @throws {PatchWardenError} reason="dependency_cycle" 当依赖图存在环时。
186
+ * @returns subgoal_id 数组,依赖项在前。
187
+ */
188
+ export function topologicalSort(goalStatus: GoalStatus): string[] {
189
+ const cycle = detectCycle(goalStatus);
190
+ if (cycle) {
191
+ throw new PatchWardenError(
192
+ "dependency_cycle",
193
+ `Dependency cycle detected among subgoals: ${cycle.join(" -> ")}`,
194
+ "Remove the circular dependency among subgoals before performing topological sort.",
195
+ true,
196
+ { cycle }
197
+ );
198
+ }
199
+
200
+ const subgoalMap = buildSubgoalMap(goalStatus);
201
+ const visited = new Set<string>();
202
+ const result: string[] = [];
203
+
204
+ const visit = (id: string): void => {
205
+ if (visited.has(id)) return;
206
+ visited.add(id);
207
+
208
+ const node = subgoalMap.get(id);
209
+ if (node) {
210
+ for (const depId of node.depends_on) {
211
+ if (subgoalMap.has(depId)) {
212
+ visit(depId);
213
+ }
214
+ }
215
+ }
216
+
217
+ result.push(id);
218
+ };
219
+
220
+ for (const s of goalStatus.subgoals) {
221
+ visit(s.id);
222
+ }
223
+
224
+ return result;
225
+ }