patchwarden 0.6.4 → 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 (212) hide show
  1. package/README.en.md +18 -18
  2. package/README.md +20 -20
  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 +15 -2
  8. package/dist/doctor.js +32 -1
  9. package/dist/goal/acceptanceEngine.d.ts +62 -0
  10. package/dist/goal/acceptanceEngine.js +103 -0
  11. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  12. package/dist/goal/acceptanceTemplate.js +104 -0
  13. package/dist/goal/goalGraph.d.ts +58 -0
  14. package/dist/goal/goalGraph.js +189 -0
  15. package/dist/goal/goalProgress.d.ts +81 -0
  16. package/dist/goal/goalProgress.js +216 -0
  17. package/dist/goal/goalStatus.d.ts +60 -0
  18. package/dist/goal/goalStatus.js +137 -0
  19. package/dist/goal/goalStore.d.ts +79 -0
  20. package/dist/goal/goalStore.js +211 -0
  21. package/dist/goal/handoffExport.d.ts +34 -0
  22. package/dist/goal/handoffExport.js +183 -0
  23. package/dist/goal/subgoalSync.d.ts +40 -0
  24. package/dist/goal/subgoalSync.js +72 -0
  25. package/dist/goal/worktreeManager.d.ts +96 -0
  26. package/dist/goal/worktreeManager.js +292 -0
  27. package/dist/logging.d.ts +44 -0
  28. package/dist/logging.js +68 -0
  29. package/dist/release/releaseGate.d.ts +99 -0
  30. package/dist/release/releaseGate.js +475 -0
  31. package/dist/runner/postTaskCleanup.d.ts +13 -0
  32. package/dist/runner/postTaskCleanup.js +147 -0
  33. package/dist/runner/runTask.js +50 -10
  34. package/dist/security/discoveryTokenStore.d.ts +62 -0
  35. package/dist/security/discoveryTokenStore.js +100 -0
  36. package/dist/security/toolInvocationGuard.d.ts +35 -0
  37. package/dist/security/toolInvocationGuard.js +153 -0
  38. package/dist/smoke-test.js +18 -10
  39. package/dist/taskRuntime.d.ts +17 -0
  40. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  41. package/dist/test/unit/acceptance-engine.test.js +228 -0
  42. package/dist/test/unit/agent-router.test.d.ts +1 -0
  43. package/dist/test/unit/agent-router.test.js +287 -0
  44. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  45. package/dist/test/unit/audit-checks.test.js +350 -0
  46. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  47. package/dist/test/unit/diagnose-task.test.js +457 -0
  48. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  49. package/dist/test/unit/discovery-token-store.test.js +139 -0
  50. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  51. package/dist/test/unit/goal-graph.test.js +298 -0
  52. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  53. package/dist/test/unit/goal-progress.test.js +381 -0
  54. package/dist/test/unit/goal-status.test.d.ts +1 -0
  55. package/dist/test/unit/goal-status.test.js +215 -0
  56. package/dist/test/unit/goal-store.test.d.ts +1 -0
  57. package/dist/test/unit/goal-store.test.js +253 -0
  58. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  59. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  60. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  61. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  62. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  63. package/dist/test/unit/handoff-export.test.js +263 -0
  64. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  65. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  66. package/dist/test/unit/logging.test.js +127 -5
  67. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  68. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  69. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  70. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  71. package/dist/test/unit/release-gate.test.d.ts +1 -0
  72. package/dist/test/unit/release-gate.test.js +242 -0
  73. package/dist/test/unit/safe-views.test.d.ts +1 -0
  74. package/dist/test/unit/safe-views.test.js +171 -0
  75. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  76. package/dist/test/unit/schema-drift-check.test.js +175 -0
  77. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  78. package/dist/test/unit/subgoal-sync.test.js +183 -0
  79. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  80. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  81. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  82. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  83. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  84. package/dist/test/unit/toolSearch.test.js +571 -0
  85. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  86. package/dist/test/unit/worktree-manager.test.js +176 -0
  87. package/dist/tools/auditTask.d.ts +103 -1
  88. package/dist/tools/auditTask.js +461 -8
  89. package/dist/tools/cancelTask.js +1 -1
  90. package/dist/tools/checkReleaseGate.d.ts +21 -0
  91. package/dist/tools/checkReleaseGate.js +22 -0
  92. package/dist/tools/createTask.d.ts +18 -2
  93. package/dist/tools/createTask.js +29 -1
  94. package/dist/tools/diagnoseTask.d.ts +45 -0
  95. package/dist/tools/diagnoseTask.js +347 -0
  96. package/dist/tools/discardWorktree.d.ts +24 -0
  97. package/dist/tools/discardWorktree.js +27 -0
  98. package/dist/tools/discoverTools.d.ts +11 -0
  99. package/dist/tools/discoverTools.js +39 -0
  100. package/dist/tools/explainTool.d.ts +11 -0
  101. package/dist/tools/explainTool.js +21 -0
  102. package/dist/tools/getTaskSummary.js +2 -1
  103. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  104. package/dist/tools/goalSubgoalTask.js +110 -0
  105. package/dist/tools/healthCheck.d.ts +1 -0
  106. package/dist/tools/healthCheck.js +5 -1
  107. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  108. package/dist/tools/invokeDiscoveredTool.js +112 -0
  109. package/dist/tools/listTasks.d.ts +3 -1
  110. package/dist/tools/listTasks.js +14 -1
  111. package/dist/tools/mergeWorktree.d.ts +24 -0
  112. package/dist/tools/mergeWorktree.js +27 -0
  113. package/dist/tools/reconcileTasks.d.ts +41 -0
  114. package/dist/tools/reconcileTasks.js +352 -0
  115. package/dist/tools/registry.js +550 -0
  116. package/dist/tools/safeStatus.d.ts +31 -1
  117. package/dist/tools/safeStatus.js +43 -1
  118. package/dist/tools/safeViews.d.ts +256 -0
  119. package/dist/tools/safeViews.js +250 -0
  120. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  121. package/dist/tools/schemaDriftCheck.js +80 -0
  122. package/dist/tools/toolCatalog.d.ts +4 -3
  123. package/dist/tools/toolCatalog.js +33 -11
  124. package/dist/tools/toolRegistry.d.ts +61 -0
  125. package/dist/tools/toolRegistry.js +724 -0
  126. package/dist/tools/toolSearch.d.ts +110 -0
  127. package/dist/tools/toolSearch.js +331 -0
  128. package/dist/tools/toolUsageStats.d.ts +41 -0
  129. package/dist/tools/toolUsageStats.js +116 -0
  130. package/dist/tools/waitForTask.js +1 -0
  131. package/dist/version.d.ts +2 -2
  132. package/dist/version.js +2 -2
  133. package/dist/watcherStatus.d.ts +29 -0
  134. package/dist/watcherStatus.js +92 -1
  135. package/package.json +3 -2
  136. package/scripts/checks/control-smoke.js +2 -2
  137. package/scripts/checks/doctor-smoke.js +1 -0
  138. package/scripts/checks/lifecycle-smoke.js +13 -9
  139. package/scripts/checks/mcp-manifest-check.js +53 -6
  140. package/scripts/checks/mcp-smoke.js +36 -9
  141. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  142. package/src/agents/agentRouter.ts +149 -0
  143. package/src/config.ts +9 -3
  144. package/src/controlCenter.ts +17 -3
  145. package/src/doctor.ts +36 -1
  146. package/src/goal/acceptanceEngine.ts +160 -0
  147. package/src/goal/acceptanceTemplate.ts +121 -0
  148. package/src/goal/goalGraph.ts +225 -0
  149. package/src/goal/goalProgress.ts +301 -0
  150. package/src/goal/goalStatus.ts +234 -0
  151. package/src/goal/goalStore.ts +306 -0
  152. package/src/goal/handoffExport.ts +211 -0
  153. package/src/goal/subgoalSync.ts +82 -0
  154. package/src/goal/worktreeManager.ts +404 -0
  155. package/src/logging.ts +91 -0
  156. package/src/release/releaseGate.ts +567 -0
  157. package/src/runner/postTaskCleanup.ts +154 -0
  158. package/src/runner/runTask.ts +49 -10
  159. package/src/security/discoveryTokenStore.ts +157 -0
  160. package/src/security/toolInvocationGuard.ts +251 -0
  161. package/src/smoke-test.ts +15 -7
  162. package/src/taskRuntime.ts +17 -0
  163. package/src/test/unit/acceptance-engine.test.ts +261 -0
  164. package/src/test/unit/agent-router.test.ts +342 -0
  165. package/src/test/unit/audit-checks.test.ts +567 -0
  166. package/src/test/unit/diagnose-task.test.ts +544 -0
  167. package/src/test/unit/discovery-token-store.test.ts +181 -0
  168. package/src/test/unit/goal-graph.test.ts +347 -0
  169. package/src/test/unit/goal-progress.test.ts +538 -0
  170. package/src/test/unit/goal-status.test.ts +270 -0
  171. package/src/test/unit/goal-store.test.ts +318 -0
  172. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  173. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  174. package/src/test/unit/handoff-export.test.ts +295 -0
  175. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  176. package/src/test/unit/logging.test.ts +177 -5
  177. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  178. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  179. package/src/test/unit/release-gate.test.ts +314 -0
  180. package/src/test/unit/safe-views.test.ts +184 -0
  181. package/src/test/unit/schema-drift-check.test.ts +258 -0
  182. package/src/test/unit/subgoal-sync.test.ts +236 -0
  183. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  184. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  185. package/src/test/unit/toolSearch.test.ts +756 -0
  186. package/src/test/unit/worktree-manager.test.ts +247 -0
  187. package/src/tools/auditTask.ts +831 -402
  188. package/src/tools/cancelTask.ts +1 -1
  189. package/src/tools/checkReleaseGate.ts +45 -0
  190. package/src/tools/createTask.ts +64 -6
  191. package/src/tools/diagnoseTask.ts +460 -0
  192. package/src/tools/discardWorktree.ts +42 -0
  193. package/src/tools/discoverTools.ts +51 -0
  194. package/src/tools/explainTool.ts +26 -0
  195. package/src/tools/getTaskSummary.ts +2 -1
  196. package/src/tools/goalSubgoalTask.ts +170 -0
  197. package/src/tools/healthCheck.ts +4 -1
  198. package/src/tools/invokeDiscoveredTool.ts +163 -0
  199. package/src/tools/listTasks.ts +16 -2
  200. package/src/tools/mergeWorktree.ts +42 -0
  201. package/src/tools/reconcileTasks.ts +464 -0
  202. package/src/tools/registry.ts +618 -1
  203. package/src/tools/safeStatus.ts +73 -2
  204. package/src/tools/safeViews.ts +271 -0
  205. package/src/tools/schemaDriftCheck.ts +120 -0
  206. package/src/tools/toolCatalog.ts +34 -11
  207. package/src/tools/toolRegistry.ts +786 -0
  208. package/src/tools/toolSearch.ts +464 -0
  209. package/src/tools/toolUsageStats.ts +151 -0
  210. package/src/tools/waitForTask.ts +1 -0
  211. package/src/version.ts +2 -2
  212. package/src/watcherStatus.ts +97 -1
@@ -0,0 +1,154 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+
5
+ const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
6
+ const CLEANUP_DIRS = new Set(["__pycache__", "dist", "release_packages", "temp_files"]);
7
+ const CLEANUP_FILES = [/\.pyc$/i];
8
+
9
+ export interface PostTaskCleanupEntry {
10
+ path: string;
11
+ reason: string;
12
+ }
13
+
14
+ export interface PostTaskCleanupReport {
15
+ enabled: boolean;
16
+ removed: PostTaskCleanupEntry[];
17
+ skipped: Array<PostTaskCleanupEntry & { skip_reason: string }>;
18
+ source_files_touched: number;
19
+ }
20
+
21
+ export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskCleanupReport {
22
+ const report: PostTaskCleanupReport = {
23
+ enabled: true,
24
+ removed: [],
25
+ skipped: [],
26
+ source_files_touched: 0,
27
+ };
28
+ const root = resolve(repoPath);
29
+ const candidates = collectCandidates(root);
30
+ for (const candidate of candidates) {
31
+ const rel = toRepoRelative(root, candidate.path);
32
+ if (!rel || isExcluded(rel)) {
33
+ report.skipped.push({ path: rel || ".", reason: candidate.reason, skip_reason: "excluded_path" });
34
+ continue;
35
+ }
36
+ if (hasTrackedGitContent(root, rel)) {
37
+ report.skipped.push({ path: rel, reason: candidate.reason, skip_reason: "tracked_by_git" });
38
+ continue;
39
+ }
40
+ if (!isIgnoredOrUntracked(root, rel)) {
41
+ report.skipped.push({ path: rel, reason: candidate.reason, skip_reason: "not_ignored_or_untracked" });
42
+ continue;
43
+ }
44
+ try {
45
+ rmSync(candidate.path, { recursive: true, force: true });
46
+ report.removed.push({ path: rel, reason: candidate.reason });
47
+ } catch (error) {
48
+ report.skipped.push({
49
+ path: rel,
50
+ reason: candidate.reason,
51
+ skip_reason: error instanceof Error ? error.message : String(error),
52
+ });
53
+ }
54
+ }
55
+ writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(report, null, 2), "utf-8");
56
+ return report;
57
+ }
58
+
59
+ function collectCandidates(root: string): PostTaskCleanupEntry[] {
60
+ const found = new Map<string, string>();
61
+ walk(root, (path, isDir) => {
62
+ const name = basename(path);
63
+ if (isDir) {
64
+ if (name === "__pycache__") found.set(path, "python_bytecode_cache");
65
+ if (name === "dist" && toRepoRelative(root, path) === "frontend/dist") found.set(path, "frontend_build_output");
66
+ if (name === "release_packages") found.set(path, "release_package_output");
67
+ if (name === "temp_files" && toRepoRelative(root, path) === "backend/temp_files") found.set(path, "backend_temp_files");
68
+ } else if (CLEANUP_FILES.some((pattern) => pattern.test(name))) {
69
+ found.set(path, "python_bytecode_file");
70
+ }
71
+ });
72
+ return [...found].map(([path, reason]) => ({ path, reason }));
73
+ }
74
+
75
+ function walk(dir: string, visit: (path: string, isDir: boolean) => void): void {
76
+ if (!existsSync(dir)) return;
77
+ let entries;
78
+ try {
79
+ entries = readdirSync(dir, { withFileTypes: true });
80
+ } catch {
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ const full = join(dir, entry.name);
85
+ const isDir = entry.isDirectory();
86
+ visit(full, isDir);
87
+ if (isDir && !EXCLUDED_DIRS.has(entry.name) && !CLEANUP_DIRS.has(entry.name)) {
88
+ walk(full, visit);
89
+ }
90
+ }
91
+ }
92
+
93
+ function hasTrackedGitContent(root: string, rel: string): boolean {
94
+ if (!isGitWorktree(root)) return false;
95
+ try {
96
+ const output = execFileSync("git", ["ls-files", "--", rel], {
97
+ cwd: root,
98
+ encoding: "utf-8",
99
+ windowsHide: true,
100
+ });
101
+ return output.trim().length > 0;
102
+ } catch {
103
+ return true;
104
+ }
105
+ }
106
+
107
+ function isIgnoredOrUntracked(root: string, rel: string): boolean {
108
+ if (!isGitWorktree(root)) return true;
109
+ try {
110
+ execFileSync("git", ["check-ignore", "-q", "--", rel], {
111
+ cwd: root,
112
+ stdio: "ignore",
113
+ windowsHide: true,
114
+ });
115
+ return true;
116
+ } catch {
117
+ try {
118
+ const output = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "--", rel], {
119
+ cwd: root,
120
+ encoding: "utf-8",
121
+ windowsHide: true,
122
+ });
123
+ return output.trim().length > 0 || !existsSync(resolve(root, rel));
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+ }
129
+
130
+ function isGitWorktree(root: string): boolean {
131
+ try {
132
+ return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
133
+ cwd: root,
134
+ encoding: "utf-8",
135
+ windowsHide: true,
136
+ }).trim() === "true";
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ function toRepoRelative(root: string, target: string): string {
143
+ const resolved = resolve(target);
144
+ const rel = relative(root, resolved);
145
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
146
+ return "";
147
+ }
148
+ return rel.replace(/\\/g, "/");
149
+ }
150
+
151
+ function isExcluded(rel: string): boolean {
152
+ const parts = rel.split("/");
153
+ return parts.some((part) => EXCLUDED_DIRS.has(part));
154
+ }
@@ -34,6 +34,7 @@ import {
34
34
  } from "./changeCapture.js";
35
35
  import { PatchWardenError, errorPayload } from "../errors.js";
36
36
  import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
37
+ import { runPostTaskCleanup, type PostTaskCleanupReport } from "./postTaskCleanup.js";
37
38
 
38
39
  const HEARTBEAT_INTERVAL_MS = 2000;
39
40
  const GRACEFUL_KILL_MS = 2000;
@@ -150,6 +151,14 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
150
151
  current_command: null,
151
152
  error: null,
152
153
  });
154
+ // v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
155
+ // can detect PID reuse and orphaned tasks. watcher_instance_id comes from
156
+ // the watcher process env; when run_task MCP tool is used directly, this
157
+ // field is intentionally left undefined so ownership cannot be falsely claimed.
158
+ writeTaskRuntime(taskDir, {
159
+ task_started_at: new Date(startedAtMs).toISOString(),
160
+ watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
161
+ });
153
162
  setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
154
163
 
155
164
  let beforeSnapshot;
@@ -230,19 +239,40 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
230
239
  ? `Verification command \"${failedVerification.command}\" could not start: ${failedVerification.spawnError}`
231
240
  : `Verification command \"${failedVerification.command}\" exited with code ${failedVerification.exitCode}.`;
232
241
  } else if (verifyResults.length === verifyCommands.length) {
233
- finalStatus = "done";
242
+ finalStatus = "done_by_agent";
234
243
  } else {
235
244
  finalError = "Verification did not complete all configured commands.";
236
245
  }
237
246
  } else {
238
247
  testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
239
- finalStatus = "done";
248
+ finalStatus = "done_by_agent";
240
249
  }
241
250
  } catch (error) {
242
251
  lastCaughtError = error;
243
252
  finalError = errorMessage(error);
244
253
  }
245
254
 
255
+ let cleanupReport: PostTaskCleanupReport = {
256
+ enabled: true,
257
+ removed: [],
258
+ skipped: [],
259
+ source_files_touched: 0,
260
+ };
261
+ if (finalStatus !== "canceled") {
262
+ try {
263
+ cleanupReport = runPostTaskCleanup(repoPath, taskDir);
264
+ updateStatus(taskDir, { cleanup: cleanupReport });
265
+ } catch (error) {
266
+ cleanupReport = {
267
+ enabled: true,
268
+ removed: [],
269
+ skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
270
+ source_files_touched: 0,
271
+ };
272
+ writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
273
+ updateStatus(taskDir, { cleanup: cleanupReport });
274
+ }
275
+ }
246
276
  setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
247
277
  const artifactCollectionStartedAt = new Date().toISOString();
248
278
  let changes: ChangeArtifacts;
@@ -310,7 +340,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
310
340
  "",
311
341
  ].join("\n"), "utf-8");
312
342
  // Don't override test failure with artifact failure
313
- if (finalStatus === "done") {
343
+ if (finalStatus === "done_by_agent") {
314
344
  finalError = `Change capture failed: ${artifactCollectionError}`;
315
345
  finalStatus = "failed";
316
346
  } else {
@@ -335,7 +365,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
335
365
  );
336
366
  } catch (error) {
337
367
  finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
338
- if (finalStatus === "done") finalStatus = "failed";
368
+ if (finalStatus === "done_by_agent") finalStatus = "failed";
339
369
  }
340
370
 
341
371
  // Phase 4: Pre-existing external dirty files are warnings, not failures
@@ -400,8 +430,8 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
400
430
  writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
401
431
  writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
402
432
 
403
- if (!["canceled", "done", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus)) finalStatus = "failed";
404
- const finalPhase: TaskPhase = finalStatus === "done" ? "completed" : finalStatus;
433
+ if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus)) finalStatus = "failed";
434
+ const finalPhase: TaskPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
405
435
  const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
406
436
 
407
437
  // Phase 7: Run Android build environment diagnostics if android_app exists
@@ -462,6 +492,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
462
492
  artifact_collection_error: artifactCollectionError,
463
493
  artifact_collection_started_at: artifactCollectionStartedAt,
464
494
  artifact_collection_finished_at: artifactCollectionFinishedAt,
495
+ cleanup: cleanupReport,
465
496
  artifact_manifest: artifactManifest,
466
497
  out_of_scope_changes: outOfScopeChanges,
467
498
  new_out_of_scope_changes: newOutOfScopeChanges,
@@ -479,7 +510,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
479
510
  commands_observed: [],
480
511
  verify: verifyJson,
481
512
  artifacts: [
482
- "result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json",
513
+ "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",
483
514
  ...(outOfScopeChanges.length > 0 ? ["rollback_scope_violation_plan.md", "rollback-plan.json"] : []),
484
515
  ...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
485
516
  ],
@@ -495,7 +526,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
495
526
  failed_command: followup.failed_command,
496
527
  suggested_next_action: followup.suggested_next_action,
497
528
  safe_followup_prompt: followup.safe_followup_prompt,
498
- next_steps: finalStatus === "done"
529
+ next_steps: finalStatus === "done_by_agent"
499
530
  ? ["Review get_task_summary and audit_task before accepting the work."]
500
531
  : ["Resolve the reported failure before accepting the work."],
501
532
  }, null, 2), "utf-8");
@@ -522,6 +553,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
522
553
  artifact_collection_error: artifactCollectionError,
523
554
  artifact_collection_started_at: artifactCollectionStartedAt,
524
555
  artifact_collection_finished_at: artifactCollectionFinishedAt,
556
+ cleanup: cleanupReport,
525
557
  out_of_scope_changes: outOfScopeChanges,
526
558
  new_out_of_scope_changes: newOutOfScopeChanges,
527
559
  preexisting_external_dirty_files: preexistingExternalDirty,
@@ -532,6 +564,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
532
564
  workspace_dirty_before: changes.workspace_dirty_before,
533
565
  workspace_dirty_after: changes.workspace_dirty_after,
534
566
  workspace_dirty: changes.workspace_dirty_after,
567
+ acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
535
568
  });
536
569
  writeTaskRuntime(taskDir, {
537
570
  phase: finalPhase,
@@ -559,7 +592,7 @@ function buildFailureFollowup(
559
592
  safe_followup_prompt: string | null;
560
593
  } {
561
594
  const failed = commands.find((command) => command.status !== "passed");
562
- if (status === "done") {
595
+ if (status === "done_by_agent" || status === "done") {
563
596
  return {
564
597
  failure_reason: null,
565
598
  failed_command: null,
@@ -634,6 +667,12 @@ async function runManagedProcess(options: {
634
667
  } catch (error) {
635
668
  return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
636
669
  }
670
+ // v0.7.0: record child_started_at immediately so diagnose_task can detect
671
+ // PID reuse by comparing this timestamp with the live process start time.
672
+ writeTaskRuntime(options.taskDir, {
673
+ child_pid: child.pid,
674
+ child_started_at: new Date().toISOString(),
675
+ });
637
676
 
638
677
  const stdoutStream = openStream(options.stdoutPath);
639
678
  const stderrStream = openStream(options.stderrPath);
@@ -904,7 +943,7 @@ function buildResultMarkdown(input: {
904
943
  input.error || "Agent execution and configured verification completed successfully.",
905
944
  "",
906
945
  "## Risks",
907
- input.status === "done"
946
+ input.status === "done_by_agent" || input.status === "done"
908
947
  ? "- Review git.diff and changed-files.json before accepting the task."
909
948
  : "- Task did not complete successfully; outputs may be partial.",
910
949
  "",
@@ -0,0 +1,157 @@
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
+
13
+ import { randomBytes } from "node:crypto";
14
+ import { PatchWardenError } from "../errors.js";
15
+ import type { ToolRisk } from "../tools/toolRegistry.js";
16
+ import type { ToolProfile } from "../tools/toolCatalog.js";
17
+
18
+ // ── 类型定义 ──────────────────────────────────────────────────────
19
+
20
+ export interface DiscoveryTokenRecord {
21
+ token: string; // token id,格式 dst_{YYYYMMDD}_{randomHex12}
22
+ toolName: string; // 该 token 授权调用的工具名
23
+ risk: ToolRisk; // 该工具的风险等级
24
+ allowedScope?: string[]; // 允许的文件 scope(可选)
25
+ issuedAt: string; // ISO 时间戳
26
+ expiresAt: string; // ISO 时间戳,默认 issuedAt + 10 分钟
27
+ query: string; // discover_tools 时的查询词
28
+ schemaDigest: string; // 工具 inputSchema 的 sha256 digest
29
+ profile: ToolProfile; // discover 时的 profile
30
+ }
31
+
32
+ export interface IssueTokenInput {
33
+ toolName: string;
34
+ risk: ToolRisk;
35
+ query: string;
36
+ schemaDigest: string;
37
+ profile: ToolProfile;
38
+ allowedScope?: string[];
39
+ ttlMs?: number; // 默认 10 * 60 * 1000(10 分钟)
40
+ }
41
+
42
+ // ── 常量 ──────────────────────────────────────────────────────────
43
+
44
+ const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
45
+
46
+ // ── 模块级 token store(单例,进程内共享) ───────────────────────
47
+
48
+ const tokenStore = new Map<string, DiscoveryTokenRecord>();
49
+
50
+ // ── 内部工具 ──────────────────────────────────────────────────────
51
+
52
+ function formatDatePart(date: Date): string {
53
+ const y = date.getFullYear();
54
+ const m = String(date.getMonth() + 1).padStart(2, "0");
55
+ const d = String(date.getDate()).padStart(2, "0");
56
+ return `${y}${m}${d}`;
57
+ }
58
+
59
+ function generateTokenId(): string {
60
+ const datePart = formatDatePart(new Date());
61
+ const randomPart = randomBytes(6).toString("hex"); // 12 位 hex
62
+ return `dst_${datePart}_${randomPart}`;
63
+ }
64
+
65
+ // ── 公共 API ──────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * 生成并保存 discovery token,返回 token id。
69
+ * token id 格式:dst_{YYYYMMDD}_{randomHex12}
70
+ */
71
+ export function issueToken(input: IssueTokenInput): string {
72
+ const now = new Date();
73
+ const issuedAt = now.toISOString();
74
+ const expiresAt = new Date(Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
75
+ const token = generateTokenId();
76
+
77
+ const record: DiscoveryTokenRecord = {
78
+ token,
79
+ toolName: input.toolName,
80
+ risk: input.risk,
81
+ allowedScope: input.allowedScope,
82
+ issuedAt,
83
+ expiresAt,
84
+ query: input.query,
85
+ schemaDigest: input.schemaDigest,
86
+ profile: input.profile,
87
+ };
88
+
89
+ tokenStore.set(token, record);
90
+ return token;
91
+ }
92
+
93
+ /**
94
+ * 消费 token(单次使用语义)。
95
+ * - 不存在 → 抛 PatchWardenError("token_not_found")
96
+ * - 过期(expiresAt 早于现在)→ 抛 PatchWardenError("token_expired")
97
+ * - 有效 → 从 store 删除并返回 record
98
+ */
99
+ export function consumeToken(tokenId: string): DiscoveryTokenRecord {
100
+ const record = tokenStore.get(tokenId);
101
+ if (!record) {
102
+ throw new PatchWardenError(
103
+ "token_not_found",
104
+ `Discovery token not found: ${tokenId}`,
105
+ "Call discover_tools to obtain a fresh discovery token before invoking invoke_discovered_tool.",
106
+ true,
107
+ { token: tokenId }
108
+ );
109
+ }
110
+
111
+ const now = Date.now();
112
+ const expiresAtMs = Date.parse(record.expiresAt);
113
+ if (expiresAtMs < now) {
114
+ // 过期 token 从 store 删除,避免累积
115
+ tokenStore.delete(tokenId);
116
+ throw new PatchWardenError(
117
+ "token_expired",
118
+ `Discovery token expired: ${tokenId}`,
119
+ "Call discover_tools again to obtain a fresh discovery token.",
120
+ true,
121
+ { token: tokenId, expired_at: record.expiresAt }
122
+ );
123
+ }
124
+
125
+ tokenStore.delete(tokenId);
126
+ return record;
127
+ }
128
+
129
+ /**
130
+ * 只读查看 token,不消费、不校验过期。
131
+ * 不存在返回 null,存在返回 record(不删除)。
132
+ */
133
+ export function peekToken(tokenId: string): DiscoveryTokenRecord | null {
134
+ const record = tokenStore.get(tokenId);
135
+ return record ?? null;
136
+ }
137
+
138
+ /**
139
+ * 撤销 token,返回是否删除成功。
140
+ */
141
+ export function revokeToken(tokenId: string): boolean {
142
+ return tokenStore.delete(tokenId);
143
+ }
144
+
145
+ /**
146
+ * 清空所有 token(测试用)。
147
+ */
148
+ export function clearAllTokens(): void {
149
+ tokenStore.clear();
150
+ }
151
+
152
+ /**
153
+ * 返回当前 store 中的 token 数量(测试/诊断用)。
154
+ */
155
+ export function getActiveTokenCount(): number {
156
+ return tokenStore.size;
157
+ }