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
@@ -11,6 +11,7 @@ import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js
11
11
  import { buildChangeArtifacts, captureRepoSnapshot, compareSnapshots, emptyArtifactHygiene, writeSnapshot, extractExternalDirtyFiles, findNewExternalDirtyFiles, buildArtifactManifest, groupChangedFiles, } from "./changeCapture.js";
12
12
  import { errorPayload } from "../errors.js";
13
13
  import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
14
+ import { runPostTaskCleanup } from "./postTaskCleanup.js";
14
15
  const HEARTBEAT_INTERVAL_MS = 2000;
15
16
  const GRACEFUL_KILL_MS = 2000;
16
17
  const MAX_CAPTURE_CHARS = 100_000;
@@ -81,6 +82,14 @@ export async function runTask(taskId) {
81
82
  current_command: null,
82
83
  error: null,
83
84
  });
85
+ // v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
86
+ // can detect PID reuse and orphaned tasks. watcher_instance_id comes from
87
+ // the watcher process env; when run_task MCP tool is used directly, this
88
+ // field is intentionally left undefined so ownership cannot be falsely claimed.
89
+ writeTaskRuntime(taskDir, {
90
+ task_started_at: new Date(startedAtMs).toISOString(),
91
+ watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
92
+ });
84
93
  setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
85
94
  let beforeSnapshot;
86
95
  let beforeWorkspaceSnapshot;
@@ -166,7 +175,7 @@ export async function runTask(taskId) {
166
175
  : `Verification command \"${failedVerification.command}\" exited with code ${failedVerification.exitCode}.`;
167
176
  }
168
177
  else if (verifyResults.length === verifyCommands.length) {
169
- finalStatus = "done";
178
+ finalStatus = "done_by_agent";
170
179
  }
171
180
  else {
172
181
  finalError = "Verification did not complete all configured commands.";
@@ -174,13 +183,35 @@ export async function runTask(taskId) {
174
183
  }
175
184
  else {
176
185
  testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
177
- finalStatus = "done";
186
+ finalStatus = "done_by_agent";
178
187
  }
179
188
  }
180
189
  catch (error) {
181
190
  lastCaughtError = error;
182
191
  finalError = errorMessage(error);
183
192
  }
193
+ let cleanupReport = {
194
+ enabled: true,
195
+ removed: [],
196
+ skipped: [],
197
+ source_files_touched: 0,
198
+ };
199
+ if (finalStatus !== "canceled") {
200
+ try {
201
+ cleanupReport = runPostTaskCleanup(repoPath, taskDir);
202
+ updateStatus(taskDir, { cleanup: cleanupReport });
203
+ }
204
+ catch (error) {
205
+ cleanupReport = {
206
+ enabled: true,
207
+ removed: [],
208
+ skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
209
+ source_files_touched: 0,
210
+ };
211
+ writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
212
+ updateStatus(taskDir, { cleanup: cleanupReport });
213
+ }
214
+ }
184
215
  setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
185
216
  const artifactCollectionStartedAt = new Date().toISOString();
186
217
  let changes;
@@ -252,7 +283,7 @@ export async function runTask(taskId) {
252
283
  "",
253
284
  ].join("\n"), "utf-8");
254
285
  // Don't override test failure with artifact failure
255
- if (finalStatus === "done") {
286
+ if (finalStatus === "done_by_agent") {
256
287
  finalError = `Change capture failed: ${artifactCollectionError}`;
257
288
  finalStatus = "failed";
258
289
  }
@@ -276,7 +307,7 @@ export async function runTask(taskId) {
276
307
  }
277
308
  catch (error) {
278
309
  finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
279
- if (finalStatus === "done")
310
+ if (finalStatus === "done_by_agent")
280
311
  finalStatus = "failed";
281
312
  }
282
313
  // Phase 4: Pre-existing external dirty files are warnings, not failures
@@ -338,9 +369,9 @@ export async function runTask(taskId) {
338
369
  }
339
370
  writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
340
371
  writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
341
- if (!["canceled", "done", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus))
372
+ if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus))
342
373
  finalStatus = "failed";
343
- const finalPhase = finalStatus === "done" ? "completed" : finalStatus;
374
+ const finalPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
344
375
  const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
345
376
  // Phase 7: Run Android build environment diagnostics if android_app exists
346
377
  const androidDiagnostic = diagnoseAndroidBuild(repoPath);
@@ -400,6 +431,7 @@ export async function runTask(taskId) {
400
431
  artifact_collection_error: artifactCollectionError,
401
432
  artifact_collection_started_at: artifactCollectionStartedAt,
402
433
  artifact_collection_finished_at: artifactCollectionFinishedAt,
434
+ cleanup: cleanupReport,
403
435
  artifact_manifest: artifactManifest,
404
436
  out_of_scope_changes: outOfScopeChanges,
405
437
  new_out_of_scope_changes: newOutOfScopeChanges,
@@ -417,7 +449,7 @@ export async function runTask(taskId) {
417
449
  commands_observed: [],
418
450
  verify: verifyJson,
419
451
  artifacts: [
420
- "result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json",
452
+ "result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json", "post-task-cleanup.json",
421
453
  ...(outOfScopeChanges.length > 0 ? ["rollback_scope_violation_plan.md", "rollback-plan.json"] : []),
422
454
  ...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
423
455
  ],
@@ -433,7 +465,7 @@ export async function runTask(taskId) {
433
465
  failed_command: followup.failed_command,
434
466
  suggested_next_action: followup.suggested_next_action,
435
467
  safe_followup_prompt: followup.safe_followup_prompt,
436
- next_steps: finalStatus === "done"
468
+ next_steps: finalStatus === "done_by_agent"
437
469
  ? ["Review get_task_summary and audit_task before accepting the work."]
438
470
  : ["Resolve the reported failure before accepting the work."],
439
471
  }, null, 2), "utf-8");
@@ -459,6 +491,7 @@ export async function runTask(taskId) {
459
491
  artifact_collection_error: artifactCollectionError,
460
492
  artifact_collection_started_at: artifactCollectionStartedAt,
461
493
  artifact_collection_finished_at: artifactCollectionFinishedAt,
494
+ cleanup: cleanupReport,
462
495
  out_of_scope_changes: outOfScopeChanges,
463
496
  new_out_of_scope_changes: newOutOfScopeChanges,
464
497
  preexisting_external_dirty_files: preexistingExternalDirty,
@@ -469,6 +502,7 @@ export async function runTask(taskId) {
469
502
  workspace_dirty_before: changes.workspace_dirty_before,
470
503
  workspace_dirty_after: changes.workspace_dirty_after,
471
504
  workspace_dirty: changes.workspace_dirty_after,
505
+ acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
472
506
  });
473
507
  writeTaskRuntime(taskDir, {
474
508
  phase: finalPhase,
@@ -485,7 +519,7 @@ export async function runTask(taskId) {
485
519
  }
486
520
  function buildFailureFollowup(status, error, commands) {
487
521
  const failed = commands.find((command) => command.status !== "passed");
488
- if (status === "done") {
522
+ if (status === "done_by_agent" || status === "done") {
489
523
  return {
490
524
  failure_reason: null,
491
525
  failed_command: null,
@@ -548,6 +582,12 @@ async function runManagedProcess(options) {
548
582
  catch (error) {
549
583
  return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
550
584
  }
585
+ // v0.7.0: record child_started_at immediately so diagnose_task can detect
586
+ // PID reuse by comparing this timestamp with the live process start time.
587
+ writeTaskRuntime(options.taskDir, {
588
+ child_pid: child.pid,
589
+ child_started_at: new Date().toISOString(),
590
+ });
551
591
  const stdoutStream = openStream(options.stdoutPath);
552
592
  const stderrStream = openStream(options.stderrPath);
553
593
  let stdout = "";
@@ -795,7 +835,7 @@ function buildResultMarkdown(input) {
795
835
  input.error || "Agent execution and configured verification completed successfully.",
796
836
  "",
797
837
  "## Risks",
798
- input.status === "done"
838
+ input.status === "done_by_agent" || input.status === "done"
799
839
  ? "- Review git.diff and changed-files.json before accepting the task."
800
840
  : "- Task did not complete successfully; outputs may be partial.",
801
841
  "",
@@ -0,0 +1,62 @@
1
+ /**
2
+ * v0.8.1: discovery token store — server-side token store for invoke_discovered_tool.
3
+ *
4
+ * discover_tools 为每个搜索结果生成 discoveryToken,server-side 保存 token 真实信息。
5
+ * invoke_discovered_tool 只接受 token id(不信任客户端回传的 token JSON)。
6
+ *
7
+ * 安全契约:
8
+ * - 不引入第三方 npm 依赖(用 node:crypto 的 randomBytes)
9
+ * - 不持久化到磁盘(内存 Map,进程内共享,单例)
10
+ * - token 单次使用语义(consumeToken 后即删除)
11
+ */
12
+ import type { ToolRisk } from "../tools/toolRegistry.js";
13
+ import type { ToolProfile } from "../tools/toolCatalog.js";
14
+ export interface DiscoveryTokenRecord {
15
+ token: string;
16
+ toolName: string;
17
+ risk: ToolRisk;
18
+ allowedScope?: string[];
19
+ issuedAt: string;
20
+ expiresAt: string;
21
+ query: string;
22
+ schemaDigest: string;
23
+ profile: ToolProfile;
24
+ }
25
+ export interface IssueTokenInput {
26
+ toolName: string;
27
+ risk: ToolRisk;
28
+ query: string;
29
+ schemaDigest: string;
30
+ profile: ToolProfile;
31
+ allowedScope?: string[];
32
+ ttlMs?: number;
33
+ }
34
+ /**
35
+ * 生成并保存 discovery token,返回 token id。
36
+ * token id 格式:dst_{YYYYMMDD}_{randomHex12}
37
+ */
38
+ export declare function issueToken(input: IssueTokenInput): string;
39
+ /**
40
+ * 消费 token(单次使用语义)。
41
+ * - 不存在 → 抛 PatchWardenError("token_not_found")
42
+ * - 过期(expiresAt 早于现在)→ 抛 PatchWardenError("token_expired")
43
+ * - 有效 → 从 store 删除并返回 record
44
+ */
45
+ export declare function consumeToken(tokenId: string): DiscoveryTokenRecord;
46
+ /**
47
+ * 只读查看 token,不消费、不校验过期。
48
+ * 不存在返回 null,存在返回 record(不删除)。
49
+ */
50
+ export declare function peekToken(tokenId: string): DiscoveryTokenRecord | null;
51
+ /**
52
+ * 撤销 token,返回是否删除成功。
53
+ */
54
+ export declare function revokeToken(tokenId: string): boolean;
55
+ /**
56
+ * 清空所有 token(测试用)。
57
+ */
58
+ export declare function clearAllTokens(): void;
59
+ /**
60
+ * 返回当前 store 中的 token 数量(测试/诊断用)。
61
+ */
62
+ export declare function getActiveTokenCount(): number;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * v0.8.1: discovery token store — server-side token store for invoke_discovered_tool.
3
+ *
4
+ * discover_tools 为每个搜索结果生成 discoveryToken,server-side 保存 token 真实信息。
5
+ * invoke_discovered_tool 只接受 token id(不信任客户端回传的 token JSON)。
6
+ *
7
+ * 安全契约:
8
+ * - 不引入第三方 npm 依赖(用 node:crypto 的 randomBytes)
9
+ * - 不持久化到磁盘(内存 Map,进程内共享,单例)
10
+ * - token 单次使用语义(consumeToken 后即删除)
11
+ */
12
+ import { randomBytes } from "node:crypto";
13
+ import { PatchWardenError } from "../errors.js";
14
+ // ── 常量 ──────────────────────────────────────────────────────────
15
+ const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
16
+ // ── 模块级 token store(单例,进程内共享) ───────────────────────
17
+ const tokenStore = new Map();
18
+ // ── 内部工具 ──────────────────────────────────────────────────────
19
+ function formatDatePart(date) {
20
+ const y = date.getFullYear();
21
+ const m = String(date.getMonth() + 1).padStart(2, "0");
22
+ const d = String(date.getDate()).padStart(2, "0");
23
+ return `${y}${m}${d}`;
24
+ }
25
+ function generateTokenId() {
26
+ const datePart = formatDatePart(new Date());
27
+ const randomPart = randomBytes(6).toString("hex"); // 12 位 hex
28
+ return `dst_${datePart}_${randomPart}`;
29
+ }
30
+ // ── 公共 API ──────────────────────────────────────────────────────
31
+ /**
32
+ * 生成并保存 discovery token,返回 token id。
33
+ * token id 格式:dst_{YYYYMMDD}_{randomHex12}
34
+ */
35
+ export function issueToken(input) {
36
+ const now = new Date();
37
+ const issuedAt = now.toISOString();
38
+ const expiresAt = new Date(Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
39
+ const token = generateTokenId();
40
+ const record = {
41
+ token,
42
+ toolName: input.toolName,
43
+ risk: input.risk,
44
+ allowedScope: input.allowedScope,
45
+ issuedAt,
46
+ expiresAt,
47
+ query: input.query,
48
+ schemaDigest: input.schemaDigest,
49
+ profile: input.profile,
50
+ };
51
+ tokenStore.set(token, record);
52
+ return token;
53
+ }
54
+ /**
55
+ * 消费 token(单次使用语义)。
56
+ * - 不存在 → 抛 PatchWardenError("token_not_found")
57
+ * - 过期(expiresAt 早于现在)→ 抛 PatchWardenError("token_expired")
58
+ * - 有效 → 从 store 删除并返回 record
59
+ */
60
+ export function consumeToken(tokenId) {
61
+ const record = tokenStore.get(tokenId);
62
+ if (!record) {
63
+ throw new PatchWardenError("token_not_found", `Discovery token not found: ${tokenId}`, "Call discover_tools to obtain a fresh discovery token before invoking invoke_discovered_tool.", true, { token: tokenId });
64
+ }
65
+ const now = Date.now();
66
+ const expiresAtMs = Date.parse(record.expiresAt);
67
+ if (expiresAtMs < now) {
68
+ // 过期 token 从 store 删除,避免累积
69
+ tokenStore.delete(tokenId);
70
+ throw new PatchWardenError("token_expired", `Discovery token expired: ${tokenId}`, "Call discover_tools again to obtain a fresh discovery token.", true, { token: tokenId, expired_at: record.expiresAt });
71
+ }
72
+ tokenStore.delete(tokenId);
73
+ return record;
74
+ }
75
+ /**
76
+ * 只读查看 token,不消费、不校验过期。
77
+ * 不存在返回 null,存在返回 record(不删除)。
78
+ */
79
+ export function peekToken(tokenId) {
80
+ const record = tokenStore.get(tokenId);
81
+ return record ?? null;
82
+ }
83
+ /**
84
+ * 撤销 token,返回是否删除成功。
85
+ */
86
+ export function revokeToken(tokenId) {
87
+ return tokenStore.delete(tokenId);
88
+ }
89
+ /**
90
+ * 清空所有 token(测试用)。
91
+ */
92
+ export function clearAllTokens() {
93
+ tokenStore.clear();
94
+ }
95
+ /**
96
+ * 返回当前 store 中的 token 数量(测试/诊断用)。
97
+ */
98
+ export function getActiveTokenCount() {
99
+ return tokenStore.size;
100
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * v0.8.1: toolInvocationGuard — invoke_discovered_tool 调用前强制校验。
3
+ *
4
+ * consumeToken 已完成 token 存在性与过期校验,guard 收到的是已消费的 record。
5
+ * guard 依次执行 8 项校验,任一失败抛出对应错误码:
6
+ * 1. token_tool_mismatch — toolName 与 record.toolName 一致性
7
+ * 2. profile_not_allowed — toolMeta.profiles 必须包含当前 profile
8
+ * 3. risk_exceeded — toolMeta.risk 不能超过 record.risk(schema 漂移拦截)
9
+ * 4. sensitive_path_blocked — workspace_read_sensitive 路径敏感校验
10
+ * 5. assessment_required — workspace_write 需要 assessmentId
11
+ * 6. command_not_allowed — command 工具 args.command 元字符预检
12
+ * 7. release_confirmation_required — release 工具需要 assessmentId
13
+ * 8. credential_sensitive_blocked — credential_sensitive 总是拒绝
14
+ *
15
+ * 加上 consumeToken 内的 token_not_found / token_expired,共 10 项调用前强制校验。
16
+ */
17
+ import { type PatchWardenToolMeta } from "../tools/toolRegistry.js";
18
+ import type { ToolProfile } from "../tools/toolCatalog.js";
19
+ import type { DiscoveryTokenRecord } from "./discoveryTokenStore.js";
20
+ export interface GuardInput {
21
+ toolName: string;
22
+ toolMeta: PatchWardenToolMeta;
23
+ args: Record<string, unknown>;
24
+ discoveryTokenRecord: DiscoveryTokenRecord;
25
+ profile: ToolProfile;
26
+ assessmentId?: string;
27
+ }
28
+ export interface GuardResult {
29
+ allowed: boolean;
30
+ }
31
+ export type InvocationErrorCode = "token_tool_mismatch" | "profile_not_allowed" | "risk_exceeded" | "sensitive_path_blocked" | "assessment_required" | "command_not_allowed" | "release_confirmation_required" | "credential_sensitive_blocked";
32
+ /**
33
+ * 调用前强制校验。任一校验失败抛 PatchWardenError,全部通过返回 { allowed: true }。
34
+ */
35
+ export declare function checkInvocation(input: GuardInput): GuardResult;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * v0.8.1: toolInvocationGuard — invoke_discovered_tool 调用前强制校验。
3
+ *
4
+ * consumeToken 已完成 token 存在性与过期校验,guard 收到的是已消费的 record。
5
+ * guard 依次执行 8 项校验,任一失败抛出对应错误码:
6
+ * 1. token_tool_mismatch — toolName 与 record.toolName 一致性
7
+ * 2. profile_not_allowed — toolMeta.profiles 必须包含当前 profile
8
+ * 3. risk_exceeded — toolMeta.risk 不能超过 record.risk(schema 漂移拦截)
9
+ * 4. sensitive_path_blocked — workspace_read_sensitive 路径敏感校验
10
+ * 5. assessment_required — workspace_write 需要 assessmentId
11
+ * 6. command_not_allowed — command 工具 args.command 元字符预检
12
+ * 7. release_confirmation_required — release 工具需要 assessmentId
13
+ * 8. credential_sensitive_blocked — credential_sensitive 总是拒绝
14
+ *
15
+ * 加上 consumeToken 内的 token_not_found / token_expired,共 10 项调用前强制校验。
16
+ */
17
+ import { PatchWardenError } from "../errors.js";
18
+ import { guardSensitivePath } from "./sensitiveGuard.js";
19
+ import { TOOL_RISK_RANK } from "../tools/toolRegistry.js";
20
+ // ── 内部常量 ──────────────────────────────────────────────────────
21
+ /**
22
+ * 常见路径参数字段名,用于 workspace_read_sensitive 校验时扫描 args。
23
+ * 任一字段为字符串路径都会触发 guardSensitivePath。
24
+ */
25
+ const PATH_ARG_FIELDS = [
26
+ "path",
27
+ "file",
28
+ "filePath",
29
+ "file_path",
30
+ "filename",
31
+ "repo_path",
32
+ "repoPath",
33
+ "target_path",
34
+ "targetPath",
35
+ "destination",
36
+ "dest",
37
+ ];
38
+ /**
39
+ * shell 元字符黑名单,用于 command 风险工具 args.command 预检。
40
+ * 真正的白名单校验在 handler 内通过 commandGuard 完成,guard 只做元字符预检。
41
+ */
42
+ const SHELL_METACHAR_PATTERN = /[|&;`$()<>]/;
43
+ // ── 公共 API ──────────────────────────────────────────────────────
44
+ /**
45
+ * 调用前强制校验。任一校验失败抛 PatchWardenError,全部通过返回 { allowed: true }。
46
+ */
47
+ export function checkInvocation(input) {
48
+ const { toolName, toolMeta, args, discoveryTokenRecord, profile, assessmentId } = input;
49
+ // ① toolName 一致性:toolName 必须等于 record.toolName
50
+ if (toolName !== discoveryTokenRecord.toolName) {
51
+ throw new PatchWardenError("token_tool_mismatch", `Token tool mismatch: token was issued for "${discoveryTokenRecord.toolName}" but invocation targets "${toolName}".`, "Call discover_tools again and use a token issued for the intended tool name.", true, {
52
+ token: discoveryTokenRecord.token,
53
+ expected_tool: discoveryTokenRecord.toolName,
54
+ actual_tool: toolName,
55
+ });
56
+ }
57
+ // ② profile 允许:toolMeta.profiles 必须包含当前 profile
58
+ if (!toolMeta.profiles.includes(profile)) {
59
+ throw new PatchWardenError("profile_not_allowed", `Profile "${profile}" is not allowed for tool "${toolName}". Allowed profiles: ${toolMeta.profiles.join(", ")}.`, "Switch to a profile that exposes this tool, or call discover_tools under a supported profile.", true, {
60
+ tool: toolName,
61
+ current_profile: profile,
62
+ allowed_profiles: toolMeta.profiles,
63
+ });
64
+ }
65
+ // ③ 风险等级校验:toolMeta.risk 不能超过 record.risk
66
+ // token record 的 risk 是 discover 时记录的工具 risk,应与 toolMeta.risk 一致。
67
+ // 如果工具 schema 漂移导致 risk 变了,这里会拦截。
68
+ const toolRiskRank = TOOL_RISK_RANK[toolMeta.risk];
69
+ const tokenRiskRank = TOOL_RISK_RANK[discoveryTokenRecord.risk];
70
+ if (toolRiskRank > tokenRiskRank) {
71
+ throw new PatchWardenError("risk_exceeded", `Risk exceeded: tool "${toolName}" risk is "${toolMeta.risk}" (rank ${toolRiskRank}) but discovery token was issued for risk "${discoveryTokenRecord.risk}" (rank ${tokenRiskRank}). Tool schema may have drifted.`, "Call discover_tools again to obtain a fresh token reflecting the current tool risk.", true, {
72
+ tool: toolName,
73
+ tool_risk: toolMeta.risk,
74
+ token_risk: discoveryTokenRecord.risk,
75
+ tool_risk_rank: toolRiskRank,
76
+ token_risk_rank: tokenRiskRank,
77
+ });
78
+ }
79
+ // ④ workspace_read_sensitive:检查 args 中的路径参数
80
+ checkSensitivePaths(toolMeta, toolName, args);
81
+ // ⑤ workspace_write:要求 assessmentId 非空
82
+ if (toolMeta.risk === "workspace_write") {
83
+ if (!assessmentId || assessmentId.trim() === "") {
84
+ throw new PatchWardenError("assessment_required", `Assessment required: tool "${toolName}" has workspace_write risk and requires a non-empty assessmentId.`, "Run the assessment flow first and pass the resulting assessmentId to invoke_discovered_tool.", true, {
85
+ tool: toolName,
86
+ risk: toolMeta.risk,
87
+ });
88
+ }
89
+ }
90
+ // ⑥ command:args.command 元字符预检
91
+ if (toolMeta.risk === "command") {
92
+ checkCommandMetachars(toolMeta, toolName, args);
93
+ }
94
+ // ⑦ release:要求 assessmentId 非空(二次确认语义)
95
+ if (toolMeta.risk === "release") {
96
+ if (!assessmentId || assessmentId.trim() === "") {
97
+ throw new PatchWardenError("release_confirmation_required", `Release confirmation required: tool "${toolName}" has release risk and requires a non-empty assessmentId as explicit confirmation.`, "Run the release assessment flow first and pass the resulting assessmentId to confirm the release.", true, {
98
+ tool: toolName,
99
+ risk: toolMeta.risk,
100
+ });
101
+ }
102
+ }
103
+ // ⑧ credential_sensitive:总是拒绝
104
+ if (toolMeta.risk === "credential_sensitive") {
105
+ throw new PatchWardenError("credential_sensitive_blocked", `Credential-sensitive tool "${toolName}" cannot be invoked through invoke_discovered_tool.`, "Use the dedicated credential-handling tooling outside the dynamic invoke path.", true, {
106
+ tool: toolName,
107
+ risk: toolMeta.risk,
108
+ });
109
+ }
110
+ return { allowed: true };
111
+ }
112
+ // ── 内部校验函数 ──────────────────────────────────────────────────
113
+ /**
114
+ * workspace_read_sensitive 风险工具:扫描 args 中的路径参数字段,
115
+ * 对每个字符串路径调用 guardSensitivePath(敏感路径会抛 sensitive_path_blocked)。
116
+ * 没有路径参数则跳过。
117
+ */
118
+ function checkSensitivePaths(toolMeta, toolName, args) {
119
+ if (toolMeta.risk !== "workspace_read_sensitive") {
120
+ return;
121
+ }
122
+ for (const field of PATH_ARG_FIELDS) {
123
+ const value = args[field];
124
+ if (typeof value === "string" && value.length > 0) {
125
+ // guardSensitivePath 内部会在敏感时抛 PatchWardenError("sensitive_path_blocked", ...)
126
+ guardSensitivePath(value);
127
+ }
128
+ }
129
+ }
130
+ /**
131
+ * command 风险工具:args.command 元字符预检。
132
+ * - args.command 不存在或为空 → 跳过(handler 内部会再校验)
133
+ * - 含 shell 元字符 → 抛 command_not_allowed
134
+ * 真正的白名单校验在 handler 内通过 commandGuard 完成。
135
+ */
136
+ function checkCommandMetachars(_toolMeta, toolName, args) {
137
+ const command = args.command;
138
+ if (command === undefined || command === null) {
139
+ return;
140
+ }
141
+ if (typeof command !== "string") {
142
+ return;
143
+ }
144
+ if (command.trim() === "") {
145
+ return;
146
+ }
147
+ if (SHELL_METACHAR_PATTERN.test(command)) {
148
+ throw new PatchWardenError("command_not_allowed", `Command not allowed: tool "${toolName}" args.command contains shell metacharacters. Detected pattern in "${command}".`, "Provide a plain command without shell metacharacters (| & ; ` $ ( ) < >). The handler will validate against the command allowlist.", true, {
149
+ tool: toolName,
150
+ command,
151
+ });
152
+ }
153
+ }
@@ -17,7 +17,7 @@ import { resolve, join, dirname } from "node:path";
17
17
  import { tmpdir } from "node:os";
18
18
  import { fileURLToPath, pathToFileURL } from "node:url";
19
19
  import { spawnSync } from "node:child_process";
20
- import { loadConfig, getConfig, reloadConfig } from "./config.js";
20
+ import { loadConfig, getConfig, getTasksDir, reloadConfig } from "./config.js";
21
21
  import { savePlan } from "./tools/savePlan.js";
22
22
  import { getPlan } from "./tools/getPlan.js";
23
23
  import { createTask } from "./tools/createTask.js";
@@ -533,8 +533,8 @@ test("D8b. tool profiles are exact and schema changes alter the manifest hash",
533
533
  try {
534
534
  process.env.PATCHWARDEN_TOOL_PROFILE = "full";
535
535
  const fullTools = getToolDefs();
536
- if (fullTools.length !== 30)
537
- throw new Error(`Expected 30 full tools, got ${fullTools.length}`);
536
+ if (fullTools.length !== 64)
537
+ throw new Error(`Expected 64 full tools, got ${fullTools.length}`);
538
538
  const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", getConfig().enableDirectProfile);
539
539
  const names = coreTools.map((tool) => tool.name);
540
540
  if (JSON.stringify(names) !== JSON.stringify(CHATGPT_CORE_TOOL_NAMES)) {
@@ -926,6 +926,11 @@ test("I1. list_tasks returns tasks array", () => {
926
926
  mgmtPlanId = savePlan({ title: "Mgmt Test", content: "# Test" }).plan_id;
927
927
  mgmtTaskId = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." }).task_id;
928
928
  mgmtTaskId2 = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." }).task_id;
929
+ const collectingStatusFile = join(getTasksDir(getConfig()), mgmtTaskId2, "status.json");
930
+ const collectingStatus = JSON.parse(readFileSync(collectingStatusFile, "utf-8"));
931
+ collectingStatus.status = "collecting_artifacts";
932
+ collectingStatus.phase = "collecting_artifacts";
933
+ writeFileSync(collectingStatusFile, JSON.stringify(collectingStatus, null, 2), "utf-8");
929
934
  const result = listTasks({ limit: 5 });
930
935
  if (!Array.isArray(result.tasks))
931
936
  throw new Error("tasks not array");
@@ -943,9 +948,12 @@ test("I2b. list_tasks filters by repo and active status with watcher evidence",
943
948
  if (result.returned !== result.tasks.length || !result.watcher?.status) {
944
949
  throw new Error(`Missing list_tasks pagination or watcher evidence: ${JSON.stringify(result)}`);
945
950
  }
946
- if (result.tasks.some((task) => !["pending", "running"].includes(task.status) || task.repo_path !== ".")) {
951
+ if (result.tasks.some((task) => !["pending", "running", "collecting_artifacts"].includes(task.status) || task.repo_path !== ".")) {
947
952
  throw new Error(`list_tasks active/repo filter mismatch: ${JSON.stringify(result.tasks)}`);
948
953
  }
954
+ if (!result.tasks.some((task) => task.task_id === mgmtTaskId2 && task.status === "collecting_artifacts")) {
955
+ throw new Error(`list_tasks active_only omitted collecting_artifacts task: ${JSON.stringify(result.tasks)}`);
956
+ }
949
957
  });
950
958
  test("I3. cancel_task cancels pending task", () => {
951
959
  const task = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." });
@@ -1866,11 +1874,11 @@ writeFileSync(directConfigPath, JSON.stringify({
1866
1874
  process.env.PATCHWARDEN_CONFIG = directConfigPath;
1867
1875
  reloadConfig();
1868
1876
  let directSessionId = "";
1869
- test("M1. chatgpt_core still has 17 tools", () => {
1877
+ test("M1. chatgpt_core still has the expected tool manifest", () => {
1870
1878
  const tools = getToolDefs();
1871
1879
  const coreTools = selectToolsForProfile(tools, "chatgpt_core", true);
1872
- if (coreTools.length !== 17)
1873
- throw new Error(`Expected 17, got ${coreTools.length}`);
1880
+ if (coreTools.length !== CHATGPT_CORE_TOOL_NAMES.length)
1881
+ throw new Error(`Expected ${CHATGPT_CORE_TOOL_NAMES.length}, got ${coreTools.length}`);
1874
1882
  if (JSON.stringify(coreTools.map((t) => t.name)) !== JSON.stringify(CHATGPT_CORE_TOOL_NAMES)) {
1875
1883
  throw new Error("Tool names mismatch");
1876
1884
  }
@@ -1883,11 +1891,11 @@ test("M2. chatgpt_direct disabled exposes only health_check", () => {
1883
1891
  if (disabledTools[0].name !== "health_check")
1884
1892
  throw new Error(`Expected health_check, got ${disabledTools[0].name}`);
1885
1893
  });
1886
- test("M3. chatgpt_direct enabled has 10 tools", () => {
1894
+ test("M3. chatgpt_direct enabled has 14 tools", () => {
1887
1895
  const tools = getToolDefs();
1888
1896
  const directTools = selectToolsForProfile(tools, "chatgpt_direct", true);
1889
- if (directTools.length !== 10)
1890
- throw new Error(`Expected 10, got ${directTools.length}`);
1897
+ if (directTools.length !== CHATGPT_DIRECT_TOOL_NAMES.length)
1898
+ throw new Error(`Expected ${CHATGPT_DIRECT_TOOL_NAMES.length}, got ${directTools.length}`);
1891
1899
  if (JSON.stringify(directTools.map((t) => t.name)) !== JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES)) {
1892
1900
  throw new Error("Tool names mismatch");
1893
1901
  }
@@ -5,6 +5,23 @@ export interface TaskRuntimeData {
5
5
  current_command: string | null;
6
6
  runner_pid?: number;
7
7
  child_pid?: number;
8
+ /**
9
+ * v0.7.0: ISO timestamp when the child process was spawned.
10
+ * Used by diagnose_task to detect PID reuse: if a live PID exists but
11
+ * its start time does not match child_started_at, the OS likely reused the PID.
12
+ */
13
+ child_started_at?: string;
14
+ /**
15
+ * v0.7.0: ISO timestamp when the task itself started (runner began executing).
16
+ * Distinct from child_started_at, which records when the agent child process spawned.
17
+ */
18
+ task_started_at?: string;
19
+ /**
20
+ * v0.7.0: Watcher instance ID that picked up and is executing this task.
21
+ * Used by diagnose_task to detect orphaned tasks when the current watcher
22
+ * instance_id differs from the one recorded at task start.
23
+ */
24
+ watcher_instance_id?: string;
8
25
  }
9
26
  export declare function readTaskRuntime(taskDir: string): Partial<TaskRuntimeData>;
10
27
  export declare function writeTaskRuntime(taskDir: string, patch: Partial<TaskRuntimeData>): TaskRuntimeData;
@@ -0,0 +1 @@
1
+ export {};