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
@@ -157,7 +157,7 @@ export function derivePendingReason(task, watcher) {
157
157
  return "queued_but_watcher_unreadable";
158
158
  return "queued_waiting_for_watcher";
159
159
  }
160
- if (task.status !== "running")
160
+ if (task.status !== "running" && task.status !== "collecting_artifacts")
161
161
  return null;
162
162
  if (task.phase === "executing_agent")
163
163
  return "agent_running";
@@ -167,3 +167,94 @@ export function derivePendingReason(task, watcher) {
167
167
  return "collecting_artifacts";
168
168
  return "preparing";
169
169
  }
170
+ // ── v0.7.0: Watcher ownership helpers ─────────────────────────────
171
+ /**
172
+ * Read the current watcher's instance_id from the heartbeat file.
173
+ * Returns null when the heartbeat is missing or unreadable.
174
+ *
175
+ * This is the source of truth for "which watcher is currently alive".
176
+ * Tasks whose runtime.watcher_instance_id differs from this value are
177
+ * considered orphaned by the diagnosis layer.
178
+ */
179
+ export function readWatcherInstanceId(config = getConfig()) {
180
+ const heartbeatPath = getWatcherHeartbeatPath(config);
181
+ if (!existsSync(heartbeatPath))
182
+ return null;
183
+ try {
184
+ const raw = readFileSync(heartbeatPath, "utf-8");
185
+ const data = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
186
+ return typeof data.instance_id === "string" ? data.instance_id : null;
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ /**
193
+ * v0.7.0: Determine whether the current watcher still owns a task.
194
+ *
195
+ * Ownership is established by comparing the watcher_instance_id recorded in
196
+ * the task's runtime.json with the instance_id of the currently alive
197
+ * watcher. A task is considered "owned" only when:
198
+ * - the current watcher heartbeat is healthy, AND
199
+ * - the task runtime recorded a watcher_instance_id, AND
200
+ * - both instance IDs match.
201
+ *
202
+ * If the task has no recorded watcher_instance_id (legacy tasks created
203
+ * before v0.7.0), ownership cannot be confirmed and this returns false.
204
+ * The diagnosis layer treats "no owner" as a stale signal, not as active.
205
+ */
206
+ export function isWatcherOwningTask(taskDir, config = getConfig()) {
207
+ const runtimePath = join(taskDir, "runtime.json");
208
+ let taskWatcherInstanceId = null;
209
+ if (existsSync(runtimePath)) {
210
+ try {
211
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf-8"));
212
+ if (typeof runtime.watcher_instance_id === "string") {
213
+ taskWatcherInstanceId = runtime.watcher_instance_id;
214
+ }
215
+ }
216
+ catch {
217
+ // corrupted runtime — treat as no record
218
+ }
219
+ }
220
+ if (!taskWatcherInstanceId) {
221
+ return {
222
+ owned: false,
223
+ reason: "no_runtime_record",
224
+ task_watcher_instance_id: null,
225
+ current_watcher_instance_id: null,
226
+ };
227
+ }
228
+ const watcher = readWatcherStatus(config);
229
+ if (watcher.status !== "healthy") {
230
+ return {
231
+ owned: false,
232
+ reason: watcher.status === "missing" ? "watcher_missing" : "watcher_unhealthy",
233
+ task_watcher_instance_id: taskWatcherInstanceId,
234
+ current_watcher_instance_id: watcher.instance_id,
235
+ };
236
+ }
237
+ const currentInstanceId = watcher.instance_id;
238
+ if (!currentInstanceId) {
239
+ return {
240
+ owned: false,
241
+ reason: "watcher_missing",
242
+ task_watcher_instance_id: taskWatcherInstanceId,
243
+ current_watcher_instance_id: null,
244
+ };
245
+ }
246
+ if (currentInstanceId !== taskWatcherInstanceId) {
247
+ return {
248
+ owned: false,
249
+ reason: "instance_mismatch",
250
+ task_watcher_instance_id: taskWatcherInstanceId,
251
+ current_watcher_instance_id: currentInstanceId,
252
+ };
253
+ }
254
+ return {
255
+ owned: true,
256
+ reason: "owned",
257
+ task_watcher_instance_id: taskWatcherInstanceId,
258
+ current_watcher_instance_id: currentInstanceId,
259
+ };
260
+ }
@@ -92,6 +92,37 @@ safety boundaries and complete evidence.
92
92
  7. Changes remain uncommitted for review; commit, push, and publish are outside
93
93
  the ordinary task scope.
94
94
 
95
+ For routine guarded work, `run_task_loop` can perform the assess-only preflight,
96
+ task creation, waiting, safe summary review, audit, and bounded `fix_tests`
97
+ follow-up cycle in one tool call. It still uses the existing Watcher and
98
+ allow-listed verification commands, stops at local confirmation boundaries, and
99
+ returns a `lineage_id` for `get_task_lineage` instead of full logs or diffs.
100
+
101
+ For v1.4 Direct-assisted verification, set `direct_verify=true` only when the
102
+ local Direct profile is enabled and the desired Direct verification commands are
103
+ already allow-listed. The loop creates a Direct session after the normal task
104
+ and audit have succeeded, runs verification, safe-finalizes, safe-audits, and
105
+ stores bounded Direct evidence in lineage. It does not call Direct patching
106
+ tools, publish, push, tag, create releases, or restart live services.
107
+
108
+ For v1.5 isolated loop work, set `agent="auto"` when you want PatchWarden to
109
+ pick from configured local agents using bounded routing, and set
110
+ `isolation_mode="worktree"` only when the target repo is a git repository and
111
+ you want the task to run in an isolated worktree. Worktree mode records evidence
112
+ in lineage but never auto-merges or auto-deletes the worktree. After a loop
113
+ finishes, call `export_task_evidence_pack(lineage_id)` to write bounded
114
+ `evidence.json` and `EVIDENCE.md` files without stdout/stderr tails, full diffs,
115
+ verification logs, or sensitive file content.
116
+
117
+ For v1.3 policy-aware work, call `get_project_policy` before release-oriented
118
+ changes. It reads the bounded effective `.patchwarden/project-policy.json`
119
+ summary and release readiness without granting new command permissions. Release
120
+ mode tools are full-profile only: `release_check` wraps the existing release
121
+ gate, `release_prepare` runs only already allow-listed local commands,
122
+ `release_verify` performs read-only npm/GitHub/CI checks, and `release_cleanup`
123
+ defaults to dry run. None of these tools publish, push, tag, create GitHub
124
+ Releases, restart live tunnels/watchers, or return full logs/diffs.
125
+
95
126
  `needs_confirm` assessments must be confirmed locally with
96
127
  `patchwarden-confirm <full_assessment_id>`. The confirmation command is not an
97
128
  MCP tool. A `blocked` assessment cannot be confirmed.
@@ -27,6 +27,15 @@ automation-friendly commands.
27
27
 
28
28
  ## Design Notes
29
29
 
30
+ - v1.3 Dashboard panels show bounded task lineage, project policy, and release
31
+ readiness summaries. The backing APIs are read-only and do not expose full
32
+ stdout/stderr, full diffs, long logs, or secret-bearing files.
33
+ - v1.4 extends the lineage panel with Direct-assisted verification status and
34
+ exposes a safe Direct session summary API without stdout/stderr tails or diffs.
35
+ - v1.5 adds an Evidence Pack dashboard card plus read-only
36
+ `/api/evidence-packs` and `/api/evidence-packs/:lineage_id` routes. These
37
+ APIs return bounded lineage/policy/catalog evidence and omit stdout/stderr,
38
+ full diffs, verification logs, and sensitive file content.
30
39
  - `control-center-mvp.md`: first Web dashboard scope.
31
40
  - `control-center-phase2.md`: follow-up management and diagnostics scope.
32
41
  - `control-center-daily-driver.md`: current daily-use contract.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchwarden",
3
- "version": "0.6.4",
4
- "description": "A secure local MCP bridge: clients save plans, local agents execute tasks, and results are returned through workspace-scoped files.",
3
+ "version": "1.5.0",
4
+ "description": "Secure local MCP bridge for ChatGPT and local agents, with workspace-scoped task loops, safe summaries, audits, and Direct editing.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -50,6 +50,7 @@
50
50
  "test:watcher-supervisor": "npm run build && node scripts/checks/watcher-supervisor-smoke.js",
51
51
  "check:tool-manifest": "npm run build && node scripts/checks/mcp-manifest-check.js",
52
52
  "check:direct-tool-manifest": "npm run build && node scripts/checks/mcp-manifest-check.js --profile chatgpt_direct",
53
+ "check:search-tool-manifest": "npm run build && node scripts/checks/mcp-manifest-check.js --profile chatgpt_search",
53
54
  "check:brand": "node scripts/checks/brand-check.js",
54
55
  "test:mcp": "npm run build && node scripts/checks/mcp-smoke.js",
55
56
  "test:http-mcp": "npm run build && node scripts/checks/http-mcp-smoke.js",
@@ -229,6 +229,7 @@ async function testStaticFiles() {
229
229
  if (!rootCt.includes("text/html")) checks.push(`GET / -> Content-Type ${rootCt} (expected text/html)`);
230
230
  if (!root.body.includes("setup-checklist-card")) checks.push("dashboard missing setup checklist card");
231
231
  if (!root.body.includes("Show Core / Direct log tails")) checks.push("dashboard activity log is not collapsed behind a summary");
232
+ if (!root.body.includes("evidence-pack-card")) checks.push("dashboard missing v1.5 evidence pack card");
232
233
 
233
234
  if (vendor.status !== 200) checks.push(`GET /vendor/tailwindcss-browser.js -> status ${vendor.status}`);
234
235
  const vendorCt = vendor.headers["content-type"] || "";
@@ -667,6 +668,92 @@ async function testOtherGetApis() {
667
668
  problems.push(`/api/audit error: ${err.message}`);
668
669
  }
669
670
 
671
+ // /api/lineages
672
+ try {
673
+ const res = await httpGet(`${BASE_URL}/api/lineages`);
674
+ if (res.status !== 200) problems.push(`/api/lineages -> ${res.status} (expected 200)`);
675
+ else {
676
+ const json = tryJson(res.body);
677
+ if (!json || !Array.isArray(json.lineages) || typeof json.total !== "number") {
678
+ problems.push(`/api/lineages missing bounded lineage summary fields: ${res.body.slice(0, 120)}`);
679
+ }
680
+ }
681
+ } catch (err) {
682
+ problems.push(`/api/lineages error: ${err.message}`);
683
+ }
684
+
685
+ // /api/project-policy
686
+ try {
687
+ const res = await httpGet(`${BASE_URL}/api/project-policy?repo_path=.`);
688
+ if (res.status !== 200) problems.push(`/api/project-policy -> ${res.status} (expected 200)`);
689
+ else {
690
+ const json = tryJson(res.body);
691
+ if (!json || !("effective_policy" in json) || !Array.isArray(json.issues)) {
692
+ problems.push(`/api/project-policy missing policy summary fields: ${res.body.slice(0, 120)}`);
693
+ }
694
+ const text = res.body.toLowerCase();
695
+ if (text.includes("stdout") || text.includes("stderr") || text.includes("diff_patch")) {
696
+ problems.push("/api/project-policy leaked log/diff-shaped fields");
697
+ }
698
+ }
699
+ } catch (err) {
700
+ problems.push(`/api/project-policy error: ${err.message}`);
701
+ }
702
+
703
+ // /api/release/status
704
+ try {
705
+ const res = await httpGet(`${BASE_URL}/api/release/status?repo_path=.`);
706
+ if (res.status !== 200) problems.push(`/api/release/status -> ${res.status} (expected 200)`);
707
+ else {
708
+ const json = tryJson(res.body);
709
+ if (!json || !("release_readiness" in json) || json.remote_write_performed !== false) {
710
+ problems.push(`/api/release/status missing read-only release status fields: ${res.body.slice(0, 120)}`);
711
+ }
712
+ const text = res.body.toLowerCase();
713
+ if (text.includes("stdout") || text.includes("stderr") || text.includes("diff_patch")) {
714
+ problems.push("/api/release/status leaked log/diff-shaped fields");
715
+ }
716
+ }
717
+ } catch (err) {
718
+ problems.push(`/api/release/status error: ${err.message}`);
719
+ }
720
+
721
+ // /api/evidence-packs
722
+ try {
723
+ const res = await httpGet(`${BASE_URL}/api/evidence-packs`);
724
+ if (res.status !== 200) problems.push(`/api/evidence-packs -> ${res.status} (expected 200)`);
725
+ else {
726
+ const json = tryJson(res.body);
727
+ if (!json || !Array.isArray(json.evidence_packs) || typeof json.total !== "number") {
728
+ problems.push(`/api/evidence-packs missing bounded evidence summary fields: ${res.body.slice(0, 120)}`);
729
+ }
730
+ const text = res.body.toLowerCase();
731
+ if (text.includes("stdout_tail") || text.includes("stderr_tail") || text.includes("diff.patch") || text.includes("verification.log")) {
732
+ problems.push("/api/evidence-packs leaked log/diff-shaped fields");
733
+ }
734
+ }
735
+ } catch (err) {
736
+ problems.push(`/api/evidence-packs error: ${err.message}`);
737
+ }
738
+
739
+ // /api/direct-sessions/:id/summary (safe, bounded route; missing id is still non-leaky)
740
+ try {
741
+ const res = await httpGet(`${BASE_URL}/api/direct-sessions/direct_missing_summary/summary`);
742
+ if (res.status !== 200) problems.push(`/api/direct-sessions/:id/summary -> ${res.status} (expected 200)`);
743
+ else {
744
+ const json = tryJson(res.body);
745
+ if (!json || json.session_id !== "direct_missing_summary") {
746
+ problems.push(`/api/direct-sessions/:id/summary missing safe summary envelope: ${res.body.slice(0, 120)}`);
747
+ }
748
+ const text = res.body.toLowerCase();
749
+ if (text.includes("stdout_tail") || text.includes("stderr_tail") || text.includes("diff_patch") || text.includes("verification.log")) {
750
+ problems.push("/api/direct-sessions/:id/summary leaked log/diff-shaped fields");
751
+ }
752
+ }
753
+ } catch (err) {
754
+ problems.push(`/api/direct-sessions/:id/summary error: ${err.message}`);
755
+ }
756
+
670
757
  // /api/logs/core
671
758
  try {
672
759
  const res = await httpGet(`${BASE_URL}/api/logs/core`);
@@ -76,7 +76,7 @@ try {
76
76
  ready: false,
77
77
  pid: null,
78
78
  tool_profile: "chatgpt_direct",
79
- tool_count: 9,
79
+ tool_count: 14,
80
80
  tools_ready: true,
81
81
  }), "utf8");
82
82
  writeFileSync(join(directRuntime, "tunnel-client.pid"), String(fakeTunnel.pid), "utf8");
@@ -114,7 +114,7 @@ try {
114
114
  reason_code: "stale_fixture",
115
115
  last_error: "stale failure",
116
116
  tool_profile: "chatgpt_core",
117
- tool_count: 16,
117
+ tool_count: 26,
118
118
  tools_ready: true,
119
119
  }), "utf8");
120
120
  healthServer = spawn(
@@ -42,6 +42,7 @@ try {
42
42
  "[OK] Task directory writable",
43
43
  "[OK] Example task directory read/write",
44
44
  "[OK] allowedTestCommands is non-empty",
45
+ "[OK] Release gate module loadable",
45
46
  ];
46
47
  for (const line of requiredLines) {
47
48
  if (!result.stdout.includes(line)) throw new Error(`doctor output missing stable line: ${line}`);
@@ -12,6 +12,10 @@ import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { setTimeout as sleep } from "node:timers/promises";
14
14
 
15
+ // v0.7.2: tasks complete with "done_by_agent" (pending acceptance) instead of "done".
16
+ // Both are valid successful completion statuses for lifecycle smoke tests.
17
+ const isDoneStatus = (s) => s === "done" || s === "done_by_agent";
18
+
15
19
  const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-lifecycle-"));
16
20
  const workspaceRoot = join(tempRoot, "workspace");
17
21
  const repoPath = join(workspaceRoot, "repo");
@@ -178,7 +182,7 @@ try {
178
182
  verify_commands: ["node --check main.js", "node --check second.js"],
179
183
  });
180
184
  const result = await runTask(task.task_id);
181
- if (result.status !== "done") throw new Error(`Writer task ended ${result.status}: ${result.error}`);
185
+ if (!isDoneStatus(result.status)) throw new Error(`Writer task ended ${result.status}: ${result.error}`);
182
186
  const status = getTaskStatus(task.task_id);
183
187
  const changed = status.changed_files || [];
184
188
  if (!changed.some((file) => file.path === "README.md" && file.change === "modified")) {
@@ -253,7 +257,7 @@ try {
253
257
  });
254
258
  const result = await runTask(task.task_id);
255
259
  const verify = JSON.parse(readFileSync(join(task.path, "verify.json"), "utf-8"));
256
- if (result.status !== "done" || verify.status !== "passed" || verify.commands.length !== 1) {
260
+ if (!isDoneStatus(result.status) || verify.status !== "passed" || verify.commands.length !== 1) {
257
261
  throw new Error(`Legacy verification mismatch: ${JSON.stringify({ result, verify })}`);
258
262
  }
259
263
  if (verify.commands[0].cwd !== repoPath || !("stdout_tail" in verify.commands[0]) || !("stderr_tail" in verify.commands[0])) {
@@ -265,7 +269,7 @@ try {
265
269
  const plan = savePlan({ title: "No diff", content: "Do not change files." });
266
270
  const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo" });
267
271
  const result = await runTask(task.task_id);
268
- if (result.status !== "done") throw new Error(`No-op task failed: ${JSON.stringify(result)}`);
272
+ if (!isDoneStatus(result.status)) throw new Error(`No-op task failed: ${JSON.stringify(result)}`);
269
273
  const diff = getDiff(task.task_id);
270
274
  const verify = JSON.parse(readFileSync(join(task.path, "verify.json"), "utf-8"));
271
275
  if (verify.status !== "skipped") throw new Error(`Expected skipped verification: ${JSON.stringify(verify)}`);
@@ -286,7 +290,7 @@ try {
286
290
  const plan = savePlan({ title: "Large diff", content: "Create a large text file." });
287
291
  const task = createTask({ plan_id: plan.plan_id, agent: "largewriter", repo_path: "repo" });
288
292
  const result = await runTask(task.task_id);
289
- if (result.status !== "done") throw new Error(`Large task failed: ${JSON.stringify(result)}`);
293
+ if (!isDoneStatus(result.status)) throw new Error(`Large task failed: ${JSON.stringify(result)}`);
290
294
  const diff = getDiff(task.task_id);
291
295
  const patchSize = readFileSync(join(task.path, "diff.patch"), "utf-8").length;
292
296
  if (!diff.truncated || !diff.patch_head || !diff.diff_patch_path || patchSize <= diff.content.length) {
@@ -298,7 +302,7 @@ try {
298
302
  const plan = savePlan({ title: "Non-Git evidence", content: "Modify files in a non-Git repository." });
299
303
  const task = createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "plain-repo" });
300
304
  const result = await runTask(task.task_id);
301
- if (result.status !== "done") throw new Error(`Non-Git task failed: ${JSON.stringify(result)}`);
305
+ if (!isDoneStatus(result.status)) throw new Error(`Non-Git task failed: ${JSON.stringify(result)}`);
302
306
  const diff = getDiff(task.task_id);
303
307
  if (
304
308
  diff.patch_mode !== "hash_only" ||
@@ -313,7 +317,7 @@ try {
313
317
  const plan = savePlan({ title: "Binary evidence", content: "Create a binary fixture." });
314
318
  const task = createTask({ plan_id: plan.plan_id, agent: "binarywriter", repo_path: "repo" });
315
319
  const result = await runTask(task.task_id);
316
- if (result.status !== "done") throw new Error(`Binary task failed: ${JSON.stringify(result)}`);
320
+ if (!isDoneStatus(result.status)) throw new Error(`Binary task failed: ${JSON.stringify(result)}`);
317
321
  const diff = getDiff(task.task_id);
318
322
  if (diff.patch_mode !== "textual" || !diff.content.includes("GIT binary patch")) {
319
323
  throw new Error(`Binary patch evidence mismatch: ${JSON.stringify(diff)}`);
@@ -324,7 +328,7 @@ try {
324
328
  const plan = savePlan({ title: "Artifact hygiene", content: "Generate representative task outputs." });
325
329
  const task = createTask({ plan_id: plan.plan_id, agent: "artifactwriter", repo_path: "repo" });
326
330
  const result = await runTask(task.task_id);
327
- if (result.status !== "done") throw new Error(`Artifact task failed: ${JSON.stringify(result)}`);
331
+ if (!isDoneStatus(result.status)) throw new Error(`Artifact task failed: ${JSON.stringify(result)}`);
328
332
  const standard = getTaskSummary(task.task_id);
329
333
  const compact = getTaskSummary(task.task_id, { view: "compact", max_items: 1 });
330
334
  const counts = standard.artifact_hygiene?.counts || {};
@@ -353,7 +357,7 @@ try {
353
357
  const plan = savePlan({ title: "Delete fixture", content: "Delete the designated fixture file." });
354
358
  const task = createTask({ plan_id: plan.plan_id, agent: "deleter", repo_path: "repo" });
355
359
  const result = await runTask(task.task_id);
356
- if (result.status !== "done") throw new Error(`Delete task failed: ${JSON.stringify(result)}`);
360
+ if (!isDoneStatus(result.status)) throw new Error(`Delete task failed: ${JSON.stringify(result)}`);
357
361
  const diff = getDiff(task.task_id);
358
362
  if (!diff.file_stats?.some((file) => file.path === "delete-me.txt" && file.status === "deleted")) {
359
363
  throw new Error(`Deleted file stats missing: ${JSON.stringify(diff.file_stats)}`);
@@ -504,7 +508,7 @@ try {
504
508
  const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo", verify_commands: ["node --check main.js"] });
505
509
  const result = await runTask(task.task_id);
506
510
 
507
- if (result.status !== "done") {
511
+ if (!isDoneStatus(result.status)) {
508
512
  throw new Error(`Expected done, got: ${JSON.stringify(result)}`);
509
513
  }
510
514
  const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
@@ -4,18 +4,20 @@ import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
6
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
- import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
7
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, CHATGPT_SEARCH_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
8
8
 
9
9
  const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
10
10
  const jsonOnly = process.argv.includes("--json");
11
11
  const profileIndex = process.argv.indexOf("--profile");
12
12
  const profile = profileIndex >= 0 ? process.argv[profileIndex + 1] : "chatgpt_core";
13
- if (profile !== "chatgpt_core" && profile !== "chatgpt_direct") {
13
+ if (profile !== "chatgpt_core" && profile !== "chatgpt_direct" && profile !== "chatgpt_search") {
14
14
  throw new Error(`Unsupported manifest profile "${profile}".`);
15
15
  }
16
16
  const expectedTools = profile === "chatgpt_direct"
17
17
  ? [...CHATGPT_DIRECT_TOOL_NAMES]
18
- : [...CHATGPT_CORE_TOOL_NAMES];
18
+ : profile === "chatgpt_search"
19
+ ? [...CHATGPT_SEARCH_TOOL_NAMES]
20
+ : [...CHATGPT_CORE_TOOL_NAMES];
19
21
  const defaultConfigPath = resolve(root, "patchwarden.config.json");
20
22
  const transportEnv = {
21
23
  ...process.env,
@@ -102,13 +104,37 @@ try {
102
104
  if (!summaryProperties.includes("view") || !summaryProperties.includes("max_items")) {
103
105
  throw new Error("get_task_summary schema must expose view and max_items");
104
106
  }
105
- } else {
107
+ const safeRequirements = {
108
+ run_task_loop: ["repo_path", "goal", "verify_commands", "direct_verify", "isolation_mode", "worktree_cleanup"],
109
+ recommend_agent_for_task: ["repo_path", "goal"],
110
+ get_task_lineage: ["lineage_id"],
111
+ export_task_evidence_pack: ["lineage_id"],
112
+ get_project_policy: ["repo_path"],
113
+ safe_result: ["task_id"],
114
+ safe_audit: ["task_id"],
115
+ safe_test_summary: ["task_id"],
116
+ safe_diff_summary: ["task_id"],
117
+ };
118
+ for (const [toolName, requiredProperties] of Object.entries(safeRequirements)) {
119
+ const tool = listed.tools.find((entry) => entry.name === toolName);
120
+ const properties = Object.keys(tool?.inputSchema?.properties || {});
121
+ for (const property of requiredProperties) {
122
+ if (!properties.includes(property)) {
123
+ throw new Error(`${toolName} schema is missing ${property}`);
124
+ }
125
+ }
126
+ }
127
+ } else if (profile === "chatgpt_direct") {
106
128
  const directRequirements = {
107
129
  create_direct_session: ["repo_path"],
108
130
  apply_patch: ["session_id", "path", "expected_sha256", "operations"],
109
131
  run_verification: ["session_id", "command"],
132
+ run_direct_verification_bundle: ["session_id", "commands"],
110
133
  finalize_direct_session: ["session_id"],
111
134
  audit_session: ["session_id"],
135
+ safe_direct_summary: ["session_id"],
136
+ safe_finalize_direct_session: ["session_id"],
137
+ safe_audit_direct_session: ["session_id"],
112
138
  };
113
139
  for (const [toolName, requiredProperties] of Object.entries(directRequirements)) {
114
140
  const tool = listed.tools.find((entry) => entry.name === toolName);
@@ -119,6 +145,21 @@ try {
119
145
  }
120
146
  }
121
147
  }
148
+ } else {
149
+ const searchRequirements = {
150
+ discover_tools: ["query"],
151
+ explain_tool: ["name"],
152
+ invoke_discovered_tool: ["toolName", "arguments", "discoveryToken"],
153
+ };
154
+ for (const [toolName, requiredProperties] of Object.entries(searchRequirements)) {
155
+ const tool = listed.tools.find((entry) => entry.name === toolName);
156
+ const properties = Object.keys(tool?.inputSchema?.properties || {});
157
+ for (const property of requiredProperties) {
158
+ if (!properties.includes(property)) {
159
+ throw new Error(`${toolName} schema is missing ${property}`);
160
+ }
161
+ }
162
+ }
122
163
  }
123
164
  const healthResult = await client.callTool({ name: "health_check", arguments: {} });
124
165
  if (healthResult.isError) throw new Error(String(healthResult.content?.[0]?.text || "health_check failed"));
@@ -127,7 +168,8 @@ try {
127
168
  health.tool_profile !== profile ||
128
169
  health.tool_count !== expectedTools.length ||
129
170
  !health.tool_manifest_sha256 ||
130
- (profile === "chatgpt_direct" && health.direct_profile_enabled !== true)
171
+ (profile === "chatgpt_direct" && health.direct_profile_enabled !== true) ||
172
+ (profile === "chatgpt_search" && health.search_tool_count !== expectedTools.length)
131
173
  ) {
132
174
  throw new Error(`health_check catalog mismatch: ${JSON.stringify(health)}`);
133
175
  }
@@ -164,12 +206,29 @@ try {
164
206
  health_check: ["detail"],
165
207
  list_tasks: ["repo_path", "active_only"],
166
208
  get_task_summary: ["view", "max_items"],
167
- } : {
209
+ run_task_loop: ["repo_path", "goal", "verify_commands", "direct_verify", "isolation_mode", "worktree_cleanup"],
210
+ recommend_agent_for_task: ["repo_path", "goal"],
211
+ get_task_lineage: ["lineage_id"],
212
+ export_task_evidence_pack: ["lineage_id"],
213
+ get_project_policy: ["repo_path"],
214
+ safe_result: ["task_id"],
215
+ safe_audit: ["task_id"],
216
+ safe_test_summary: ["task_id"],
217
+ safe_diff_summary: ["task_id"],
218
+ } : profile === "chatgpt_direct" ? {
168
219
  create_direct_session: ["repo_path"],
169
220
  apply_patch: ["session_id", "path", "expected_sha256", "operations"],
170
221
  run_verification: ["session_id", "command"],
222
+ run_direct_verification_bundle: ["session_id", "commands"],
171
223
  finalize_direct_session: ["session_id"],
172
224
  audit_session: ["session_id"],
225
+ safe_direct_summary: ["session_id"],
226
+ safe_finalize_direct_session: ["session_id"],
227
+ safe_audit_direct_session: ["session_id"],
228
+ } : {
229
+ discover_tools: ["query"],
230
+ explain_tool: ["name"],
231
+ invoke_discovered_tool: ["toolName", "arguments", "discoveryToken"],
173
232
  },
174
233
  };
175
234
  console.log(jsonOnly ? JSON.stringify(output) : JSON.stringify(output, null, 2));