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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * v0.7.1: discover_tools MCP 工具封装。
3
+ *
4
+ * 搜索候选工具,返回压缩摘要和风险等级。
5
+ * 不加入 chatgpt_core,仅 full profile。
6
+ * v0.8.1: 为每个搜索结果生成 discoveryToken(server-side 保存)。
7
+ * v0.9.0: 传入 usageStatsProvider,从 invocation.log 聚合历史成功率用于混合排序。
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import type { ToolDef } from "./registry.js";
12
+ import { buildToolRegistry, type PatchWardenToolMeta } from "./toolRegistry.js";
13
+ import { discoverTools as searchDiscover, type DiscoverToolsInput } from "./toolSearch.js";
14
+ import { issueToken } from "../security/discoveryTokenStore.js";
15
+ import { aggregateUsageStats, type ToolUsageStats } from "./toolUsageStats.js";
16
+ import { getConfig } from "../config.js";
17
+
18
+ export function discoverTools(
19
+ input: DiscoverToolsInput,
20
+ tools: ToolDef[]
21
+ ): ReturnType<typeof searchDiscover> {
22
+ const registry: PatchWardenToolMeta[] = buildToolRegistry(tools);
23
+
24
+ // v0.9.0: 从 invocation.log 聚合历史成功率,用于混合排序公式
25
+ let usageMap: Map<string, ToolUsageStats> | null = null;
26
+ try {
27
+ const workspaceRoot = getConfig().workspaceRoot;
28
+ const logsDir = join(workspaceRoot, ".patchwarden", "logs");
29
+ usageMap = aggregateUsageStats(logsDir);
30
+ } catch {
31
+ // 配置读取失败或日志目录不存在时,无历史数据(不阻断搜索)
32
+ usageMap = null;
33
+ }
34
+
35
+ return searchDiscover(
36
+ input,
37
+ registry,
38
+ (tool) => {
39
+ return issueToken({
40
+ toolName: tool.name,
41
+ risk: tool.risk,
42
+ query: input.query,
43
+ schemaDigest: tool.inputSchemaDigest,
44
+ profile: input.profile ?? "full",
45
+ });
46
+ },
47
+ usageMap
48
+ ? (toolName: string) => usageMap!.get(toolName) ?? null
49
+ : undefined,
50
+ );
51
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * v0.7.1: explain_tool MCP 工具封装。
3
+ *
4
+ * 展开单个工具详情,可选包含完整 inputSchema。
5
+ * 不加入 chatgpt_core,仅 full profile。
6
+ * v0.8.1: includeSchema=true 时通过 tokenPeeker 二次确认 schema digest,
7
+ * 检测 registry 与实际 toolDef 之间的 schema drift。
8
+ */
9
+
10
+ import type { ToolDef } from "./registry.js";
11
+ import { buildToolRegistry, computeSchemaDigest, type PatchWardenToolMeta } from "./toolRegistry.js";
12
+ import { explainTool as searchExplain, type ExplainToolInput } from "./toolSearch.js";
13
+
14
+ export function explainTool(
15
+ input: ExplainToolInput,
16
+ tools: ToolDef[]
17
+ ): ReturnType<typeof searchExplain> {
18
+ const registry: PatchWardenToolMeta[] = buildToolRegistry(tools);
19
+ const toolDefsMap = new Map(tools.map((t) => [t.name, t]));
20
+ return searchExplain(input, registry, toolDefsMap, (toolName) => {
21
+ const toolDef = toolDefsMap.get(toolName);
22
+ if (!toolDef) return null;
23
+ const currentDigest = computeSchemaDigest(toolDef.inputSchema);
24
+ return { schemaDigest: currentDigest };
25
+ });
26
+ }
@@ -7,6 +7,7 @@ import { redactSensitiveValue } from "../security/contentRedaction.js";
7
7
 
8
8
  const TERMINAL_STATUSES = new Set([
9
9
  "done",
10
+ "done_by_agent",
10
11
  "failed",
11
12
  "failed_verification",
12
13
  "failed_scope_violation",
@@ -195,7 +196,7 @@ export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions =
195
196
  warnings.push("acceptance.json exists but could not be parsed.");
196
197
  }
197
198
  } else if (terminal) {
198
- if (status.status !== "done" || outOfScope.length > 0 || verifyStatus === "failed") {
199
+ if ((status.status !== "done" && status.status !== "done_by_agent") || outOfScope.length > 0 || verifyStatus === "failed") {
199
200
  acceptanceStatus = "failed";
200
201
  } else if (verifyStatus === "passed") {
201
202
  acceptanceStatus = "ready_for_review";
@@ -0,0 +1,170 @@
1
+ /**
2
+ * v0.8.0: createSubgoalTask — 原子地将一个新 subgoal 关联到一个新任务。
3
+ *
4
+ * 流程(原子语义):
5
+ * 1. 读取 goal_status.json(不存在则抛 goal_not_found)
6
+ * 2. addSubgoal(校验 depends_on 引用已有 subgoal)
7
+ * 3. createTask 创建关联任务(写入 goal_id / subgoal_id 到 task status.json)
8
+ * 4. linkTaskToSubgoal(将 task_id 关联到 subgoal)
9
+ * 5. updateSubgoalStatus(running)(ready → running)
10
+ * 6. 写回 goal_status.json
11
+ *
12
+ * 注意:
13
+ * - createSubgoalTask 强制 execution_mode 为 "execute",因为 assess_only 不产生可关联的 task。
14
+ * - createTask 内部使用 getConfig(),因此 workspaceRoot 由全局配置决定。
15
+ */
16
+
17
+ import { readGoalStatus, writeGoalStatus } from "../goal/goalStore.js";
18
+ import { addSubgoal, linkTaskToSubgoal, updateSubgoalStatus, type Subgoal } from "../goal/goalStatus.js";
19
+ import { createTask, type CreateTaskInput } from "./createTask.js";
20
+ import { createWorktree, discardWorktree } from "../goal/worktreeManager.js";
21
+ import { getConfig } from "../config.js";
22
+ import { PatchWardenError } from "../errors.js";
23
+
24
+ // ── 类型定义 ──────────────────────────────────────────────────────
25
+
26
+ export interface CreateSubgoalTaskInput {
27
+ goal_id: string;
28
+ subgoal_title: string;
29
+ depends_on?: string[];
30
+ // create_task 的标准参数(除 goal_id/subgoal_id 外)
31
+ plan_id?: string;
32
+ inline_plan?: string;
33
+ plan_title?: string;
34
+ template?: CreateTaskInput["template"];
35
+ goal?: string;
36
+ agent?: string;
37
+ repo_path: string;
38
+ test_command?: string;
39
+ verify_commands?: string[];
40
+ timeout_seconds?: number;
41
+ execution_mode?: "assess_only" | "execute";
42
+ assessment_id?: string;
43
+ scope?: string[];
44
+ forbidden?: string[];
45
+ verification?: string[];
46
+ done_evidence?: string[];
47
+ // v1.0.0 Part B: 是否为该 subgoal task 创建隔离 git worktree(默认 true)。
48
+ // 设为 false 时退化为原行为,使用 input.repo_path,不调用 worktreeManager。
49
+ isolate_worktree?: boolean;
50
+ }
51
+
52
+ export interface CreateSubgoalTaskOutput {
53
+ subgoal_id: string;
54
+ task_id: string;
55
+ subgoal_status: "running";
56
+ }
57
+
58
+ // ── 函数实现 ──────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * 原子地创建一个 subgoal 并关联到一个新任务。
62
+ *
63
+ * @throws PatchWardenError("goal_not_found") 当 goal_id 不存在
64
+ * @throws PatchWardenError("invalid_dependency") 当 depends_on 引用不存在的 subgoal
65
+ * @throws PatchWardenError("invalid_execution_mode") 当 execution_mode 为 "assess_only"
66
+ */
67
+ export function createSubgoalTask(input: CreateSubgoalTaskInput): CreateSubgoalTaskOutput {
68
+ // assess_only 不产生 task,无法关联到 subgoal
69
+ if (input.execution_mode === "assess_only") {
70
+ throw new PatchWardenError(
71
+ "invalid_execution_mode",
72
+ "createSubgoalTask does not support execution_mode \"assess_only\"",
73
+ "Use execution_mode \"execute\" (default) so a task is created and linked to the subgoal.",
74
+ true,
75
+ { goal_id: input.goal_id, execution_mode: input.execution_mode }
76
+ );
77
+ }
78
+
79
+ // 1. 读取 goal_status.json,如果不存在抛 goal_not_found
80
+ let goalStatus;
81
+ try {
82
+ goalStatus = readGoalStatus(input.goal_id);
83
+ } catch {
84
+ throw new PatchWardenError(
85
+ "goal_not_found",
86
+ `Goal "${input.goal_id}" not found`,
87
+ "Call list_goals to see available goals.",
88
+ true,
89
+ { goal_id: input.goal_id }
90
+ );
91
+ }
92
+
93
+ // 2. addSubgoal(校验 depends_on)
94
+ const { goalStatus: withSubgoal, subgoalId } = addSubgoal(
95
+ goalStatus,
96
+ input.subgoal_title,
97
+ input.depends_on ?? []
98
+ );
99
+
100
+ // 2.5 Worktree 隔离(默认开启):为该 subgoal task 创建独立 git worktree,
101
+ // 用 worktree 路径作为 createTask 的 repo_path,避免并发 task 互相污染。
102
+ // isolate_worktree === false 时退化为原行为(使用 input.repo_path)。
103
+ const isolate = input.isolate_worktree !== false;
104
+ let repoPathForTask = input.repo_path;
105
+ let worktreeId: string | null = null;
106
+ if (isolate) {
107
+ const workspaceRoot = getConfig().workspaceRoot;
108
+ const wt = createWorktree(input.goal_id, subgoalId, workspaceRoot);
109
+ repoPathForTask = wt.worktreePath;
110
+ worktreeId = wt.worktreeId;
111
+ }
112
+
113
+ // 3. 调用 createTask 创建关联任务(强制 execute 模式,确保返回 CreateTaskOutput)
114
+ let taskId: string;
115
+ try {
116
+ const taskResult = createTask({
117
+ plan_id: input.plan_id,
118
+ inline_plan: input.inline_plan,
119
+ plan_title: input.plan_title,
120
+ template: input.template,
121
+ goal: input.goal,
122
+ agent: input.agent,
123
+ repo_path: repoPathForTask,
124
+ test_command: input.test_command,
125
+ verify_commands: input.verify_commands,
126
+ timeout_seconds: input.timeout_seconds,
127
+ execution_mode: "execute",
128
+ assessment_id: input.assessment_id,
129
+ scope: input.scope,
130
+ forbidden: input.forbidden,
131
+ verification: input.verification,
132
+ done_evidence: input.done_evidence,
133
+ goal_id: input.goal_id,
134
+ subgoal_id: subgoalId,
135
+ });
136
+ // execution_mode 为 "execute",createTask 返回 CreateTaskOutput(含 task_id)
137
+ taskId = (taskResult as { task_id: string }).task_id;
138
+ } catch (createTaskErr) {
139
+ // createTask 失败:清理已创建的隔离 worktree,避免遗留半成品(best effort)
140
+ if (isolate && worktreeId) {
141
+ try {
142
+ discardWorktree(worktreeId, getConfig().workspaceRoot);
143
+ } catch { /* ignore cleanup failure,向上抛出原始 createTask 错误 */ }
144
+ }
145
+ throw createTaskErr;
146
+ }
147
+
148
+ // 4. linkTaskToSubgoal + updateSubgoalStatus(running)
149
+ const withTask = linkTaskToSubgoal(withSubgoal, subgoalId, taskId);
150
+ let withRunning = updateSubgoalStatus(withTask, subgoalId, "running");
151
+
152
+ // 4.5 隔离模式下,把 worktree_id 记录到 subgoal 对象(写入 goal_status.json)
153
+ if (isolate && worktreeId) {
154
+ const newSubgoals: Subgoal[] = withRunning.subgoals.map((s) =>
155
+ s.id === subgoalId
156
+ ? ({ ...s, worktree_id: worktreeId } as Subgoal)
157
+ : s
158
+ );
159
+ withRunning = { ...withRunning, subgoals: newSubgoals };
160
+ }
161
+
162
+ // 5. 写回 goal_status.json
163
+ writeGoalStatus(input.goal_id, withRunning);
164
+
165
+ return {
166
+ subgoal_id: subgoalId,
167
+ task_id: taskId,
168
+ subgoal_status: "running",
169
+ };
170
+ }
@@ -6,7 +6,7 @@ import { listAgents } from "./listAgents.js";
6
6
  import { redactSensitiveContent, redactSensitiveValue } from "../security/contentRedaction.js";
7
7
  import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
8
8
  import type { ToolCatalogSnapshot } from "./toolCatalog.js";
9
- import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
9
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, CHATGPT_SEARCH_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
10
10
  import { readWatcherStatus } from "../watcherStatus.js";
11
11
  import { listTasks } from "./listTasks.js";
12
12
 
@@ -45,6 +45,8 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
45
45
  expectedNames = ["health_check"];
46
46
  profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
47
47
  profileConsistent = false;
48
+ } else if (activeProfile === "chatgpt_search") {
49
+ expectedNames = [...CHATGPT_SEARCH_TOOL_NAMES];
48
50
  }
49
51
  if (expectedNames) {
50
52
  const catalogNames = new Set(catalog.tool_names);
@@ -106,6 +108,7 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
106
108
  direct_sessions_dir: directSessions,
107
109
  direct_session_ttl_seconds: config.directSessionTtlSeconds,
108
110
  ...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
111
+ ...(activeProfile === "chatgpt_search" ? { search_tool_count: CHATGPT_SEARCH_TOOL_NAMES.length } : {}),
109
112
  connector_visibility: {
110
113
  status: "not_observable_server_side",
111
114
  verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
@@ -0,0 +1,163 @@
1
+ /**
2
+ * v0.8.1: invoke_discovered_tool MCP 工具封装层。
3
+ *
4
+ * 受控动态调用链:agent 必须先 discover_tools 获取 discoveryToken,
5
+ * 再凭 token 调用工具。本模块串联 consumeToken → toolMeta 查找 →
6
+ * 防递归 → checkInvocation → dispatch → logToolInvocation,
7
+ * 任一环节失败都记审计日志并返回错误结果,不向上层抛错。
8
+ */
9
+
10
+ import { consumeToken, type DiscoveryTokenRecord } from "../security/discoveryTokenStore.js";
11
+ import { checkInvocation } from "../security/toolInvocationGuard.js";
12
+ import { logToolInvocation, computeArgumentsDigest } from "../logging.js";
13
+ import { buildToolRegistry, type PatchWardenToolMeta } from "./toolRegistry.js";
14
+ import type { ToolProfile } from "./toolCatalog.js";
15
+ import type { ToolDef } from "./registry.js";
16
+ import { PatchWardenError } from "../errors.js";
17
+
18
+ // ── 类型定义 ──────────────────────────────────────────────────────
19
+
20
+ export interface InvokeDiscoveredToolInput {
21
+ toolName: string;
22
+ arguments: Record<string, unknown>;
23
+ discoveryToken: string;
24
+ assessmentId?: string;
25
+ }
26
+
27
+ export type ToolDispatch = (name: string, args: Record<string, unknown>) => Promise<unknown>;
28
+
29
+ export interface InvokeDiscoveredToolContext {
30
+ tools: ToolDef[];
31
+ profile: ToolProfile;
32
+ dispatch: ToolDispatch;
33
+ }
34
+
35
+ export interface InvokeDiscoveredToolResult {
36
+ ok: boolean;
37
+ result?: unknown;
38
+ error?: { reason: string; message: string };
39
+ invocation_log_written: boolean;
40
+ }
41
+
42
+ // ── 内部错误码常量 ────────────────────────────────────────────────
43
+
44
+ const RECURSIVE_INVOCATION_BLOCKED = "recursive_invocation_blocked";
45
+ const TOOL_NOT_REGISTERED = "tool_not_registered";
46
+ const DISPATCH_ERROR = "dispatch_error";
47
+
48
+ // ── 公共 API ──────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * 受控动态调用入口。串联 token 消费、toolMeta 查找、防递归、
52
+ * 8 项守卫校验、工具分发和审计日志。任何环节失败都记日志并返回
53
+ * 错误结果,不向上层抛错。
54
+ */
55
+ export async function invokeDiscoveredTool(
56
+ input: InvokeDiscoveredToolInput,
57
+ context: InvokeDiscoveredToolContext,
58
+ ): Promise<InvokeDiscoveredToolResult> {
59
+ const startTime = Date.now();
60
+ const argumentsDigest = computeArgumentsDigest(input.arguments);
61
+
62
+ let tokenRecord: DiscoveryTokenRecord | null = null;
63
+ let toolMeta: PatchWardenToolMeta | null = null;
64
+
65
+ const writeLog = (result: "ok" | "error", errorCode?: string): void => {
66
+ const durationMs = Date.now() - startTime;
67
+ logToolInvocation({
68
+ timestamp: new Date().toISOString(),
69
+ toolName: input.toolName,
70
+ discoveryToken: input.discoveryToken,
71
+ risk: toolMeta?.risk ?? "unknown",
72
+ profile: context.profile,
73
+ arguments_digest: argumentsDigest,
74
+ ...(tokenRecord?.allowedScope ? { allowedScope: tokenRecord.allowedScope } : {}),
75
+ result,
76
+ ...(errorCode ? { error_code: errorCode } : {}),
77
+ duration_ms: durationMs,
78
+ });
79
+ };
80
+
81
+ const fail = (reason: string, message: string): InvokeDiscoveredToolResult => {
82
+ writeLog("error", reason);
83
+ return {
84
+ ok: false,
85
+ error: { reason, message },
86
+ invocation_log_written: true,
87
+ };
88
+ };
89
+
90
+ try {
91
+ // 1. consumeToken(单次使用语义,token_not_found / token_expired 在此抛出)
92
+ try {
93
+ tokenRecord = consumeToken(input.discoveryToken);
94
+ } catch (err) {
95
+ if (err instanceof PatchWardenError) {
96
+ return fail(err.reason, err.message);
97
+ }
98
+ return fail("token_error", err instanceof Error ? err.message : String(err));
99
+ }
100
+
101
+ // 2. 查 toolMeta(防止调用未注册工具)
102
+ const registry = buildToolRegistry(context.tools);
103
+ toolMeta = registry.find((meta) => meta.name === input.toolName) ?? null;
104
+ if (!toolMeta) {
105
+ return fail(
106
+ TOOL_NOT_REGISTERED,
107
+ `Tool "${input.toolName}" is not registered in the tool registry.`,
108
+ );
109
+ }
110
+
111
+ // 3. 防递归:禁止调用 invoke_discovered_tool 自身(防止绕过守卫)
112
+ if (input.toolName === "invoke_discovered_tool") {
113
+ return fail(
114
+ RECURSIVE_INVOCATION_BLOCKED,
115
+ "Recursive invocation of invoke_discovered_tool is blocked. invoke_discovered_tool cannot call itself.",
116
+ );
117
+ }
118
+
119
+ // 4. checkInvocation(8 项调用前强制校验)
120
+ try {
121
+ checkInvocation({
122
+ toolName: input.toolName,
123
+ toolMeta,
124
+ args: input.arguments,
125
+ discoveryTokenRecord: tokenRecord,
126
+ profile: context.profile,
127
+ assessmentId: input.assessmentId,
128
+ });
129
+ } catch (err) {
130
+ if (err instanceof PatchWardenError) {
131
+ return fail(err.reason, err.message);
132
+ }
133
+ return fail("guard_error", err instanceof Error ? err.message : String(err));
134
+ }
135
+
136
+ // 5. dispatch(调用实际工具 handler)
137
+ let dispatchResult: unknown;
138
+ try {
139
+ dispatchResult = await context.dispatch(input.toolName, input.arguments);
140
+ } catch (err) {
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ return fail(
143
+ DISPATCH_ERROR,
144
+ `Dispatch failed for tool "${input.toolName}": ${message}`,
145
+ );
146
+ }
147
+
148
+ // 6. 成功 → 记审计日志 → 返回成功结果
149
+ writeLog("ok");
150
+ return {
151
+ ok: true,
152
+ result: dispatchResult,
153
+ invocation_log_written: true,
154
+ };
155
+ } catch (err) {
156
+ // 兜底:任何意外错误都记日志并返回错误结果(不抛给上层)
157
+ const message = err instanceof Error ? err.message : String(err);
158
+ return fail(
159
+ "unexpected_error",
160
+ `Unexpected error during invoke_discovered_tool: ${message}`,
161
+ );
162
+ }
163
+ }
@@ -3,7 +3,7 @@ import { resolve, join } from "node:path";
3
3
  import { getTasksDir, getPlansDir, getConfig } from "../config.js";
4
4
  import { guardPath } from "../security/pathGuard.js";
5
5
  import { readTaskRuntime } from "../taskRuntime.js";
6
- import type { TaskPhase, TaskStatus } from "./createTask.js";
6
+ import type { TaskPhase, TaskStatus, AcceptanceStatus } from "./createTask.js";
7
7
  import {
8
8
  derivePendingReason,
9
9
  readWatcherStatus,
@@ -19,6 +19,7 @@ export interface TaskEntry {
19
19
  agent: string;
20
20
  status: TaskStatus;
21
21
  phase: TaskPhase;
22
+ acceptance_status: AcceptanceStatus;
22
23
  created_at: string;
23
24
  updated_at: string;
24
25
  workspace_root: string;
@@ -38,6 +39,7 @@ export interface ListTasksInput {
38
39
  status?: string;
39
40
  repo_path?: string;
40
41
  active_only?: boolean;
42
+ acceptance_status?: string;
41
43
  limit?: number;
42
44
  }
43
45
 
@@ -54,6 +56,7 @@ export function listTasks(input?: ListTasksInput): ListTasksOutput {
54
56
  const plansDir = getPlansDir(config);
55
57
  const limit = input?.limit && input.limit > 0 ? Math.min(input.limit, 100) : 20;
56
58
  const filterStatus = input?.status || null;
59
+ const filterAcceptance = input?.acceptance_status || null;
57
60
  const filterRepo = input?.repo_path?.trim().replace(/\\/g, "/") || null;
58
61
  const watcher = readWatcherStatus(config);
59
62
 
@@ -88,7 +91,13 @@ export function listTasks(input?: ListTasksInput): ListTasksOutput {
88
91
  const data = JSON.parse(readFileSync(statusFile, "utf-8"));
89
92
  const runtime = readTaskRuntime(taskDir);
90
93
  if (filterStatus && data.status !== filterStatus) continue;
91
- if (input?.active_only && data.status !== "pending" && data.status !== "running") continue;
94
+ if (filterAcceptance) {
95
+ const taskAcceptance = data.status === "done_by_agent"
96
+ ? (typeof data.acceptance_status === "string" ? data.acceptance_status : "pending")
97
+ : null;
98
+ if (taskAcceptance !== filterAcceptance) continue;
99
+ }
100
+ if (input?.active_only && !["pending", "running", "collecting_artifacts"].includes(String(data.status))) continue;
92
101
  const normalizedRepo = String(data.repo_path || ".").replace(/\\/g, "/");
93
102
  const normalizedResolvedRepo = String(data.resolved_repo_path || "").replace(/\\/g, "/");
94
103
  if (filterRepo && normalizedRepo !== filterRepo && normalizedResolvedRepo !== filterRepo) continue;
@@ -109,6 +118,10 @@ export function listTasks(input?: ListTasksInput): ListTasksOutput {
109
118
  }
110
119
 
111
120
  const phase = runtime.phase || data.phase || "queued";
121
+ const VALID_ACCEPTANCE = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
122
+ const acceptanceStatus: AcceptanceStatus = data.status === "done_by_agent"
123
+ ? (typeof data.acceptance_status === "string" && VALID_ACCEPTANCE.includes(data.acceptance_status) ? data.acceptance_status : "pending")
124
+ : null;
112
125
  tasks.push({
113
126
  task_id: taskId,
114
127
  plan_id: data.plan_id || "",
@@ -116,6 +129,7 @@ export function listTasks(input?: ListTasksInput): ListTasksOutput {
116
129
  agent: data.agent || "",
117
130
  status: data.status || "pending",
118
131
  phase,
132
+ acceptance_status: acceptanceStatus,
119
133
  created_at: data.created_at || "",
120
134
  updated_at: data.updated_at || "",
121
135
  workspace_root: data.workspace_root || config.workspaceRoot,
@@ -0,0 +1,42 @@
1
+ /**
2
+ * v1.0.0 Part B: merge_worktree MCP 工具 handler — 把隔离 worktree 的 branch
3
+ * 合并回主工作区,并将 worktree 状态置为 "merged"。
4
+ *
5
+ * 参考 src/tools/auditTask.ts 的导出/返回风格。
6
+ */
7
+
8
+ import { getConfig } from "../config.js";
9
+ import { guardWorkspacePath } from "../security/pathGuard.js";
10
+ import { mergeWorktree } from "../goal/worktreeManager.js";
11
+
12
+ export interface MergeWorktreeInput {
13
+ worktree_id: string;
14
+ repo_path: string;
15
+ }
16
+
17
+ export interface MergeWorktreeOutput {
18
+ worktree_id: string;
19
+ status: "merged";
20
+ merged_at: string;
21
+ }
22
+
23
+ /**
24
+ * 合并指定 worktree 的 branch 回 repo_path(主工作区)。
25
+ *
26
+ * @throws PatchWardenError("workspace_path_escape") 当 repo_path 逃逸出 workspaceRoot
27
+ * @throws PatchWardenError("worktree_not_found") 当 worktree 不存在
28
+ * @throws PatchWardenError("invalid_worktree_state") 当 worktree 非 active
29
+ * @throws PatchWardenError("worktree_merge_failed") 当 git merge 失败
30
+ */
31
+ export function mergeWorktreeTool(input: MergeWorktreeInput): MergeWorktreeOutput {
32
+ const config = getConfig();
33
+ const repoPathSafe = guardWorkspacePath(input.repo_path, config.workspaceRoot);
34
+
35
+ mergeWorktree(input.worktree_id, repoPathSafe);
36
+
37
+ return {
38
+ worktree_id: input.worktree_id,
39
+ status: "merged",
40
+ merged_at: new Date().toISOString(),
41
+ };
42
+ }