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
@@ -0,0 +1,526 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { getConfig } from "../config.js";
3
+ import { createWorktree } from "../goal/worktreeManager.js";
4
+ import { guardWorkspacePath } from "../security/pathGuard.js";
5
+ import { createDirectSession } from "./createDirectSession.js";
6
+ import { createTask, type CreateTaskInput } from "./createTask.js";
7
+ import { recommendAgentForTask } from "./recommendAgentForTask.js";
8
+ import { runDirectVerificationBundle } from "./runDirectVerificationBundle.js";
9
+ import { waitForTask } from "./waitForTask.js";
10
+ import { safeAudit, safeAuditDirectSession, safeFinalizeDirectSession, safeResult, safeTestSummary } from "./safeViews.js";
11
+ import type { TaskTemplateName } from "./taskTemplates.js";
12
+ import {
13
+ createLineageId,
14
+ writeTaskLineage,
15
+ type SafeTaskLineage,
16
+ type TaskLineageDirectSession,
17
+ type TaskLineageRecord,
18
+ type TaskLineageRound,
19
+ type TaskLineageWorktree,
20
+ type TaskLoopStopReason,
21
+ } from "./taskLineage.js";
22
+
23
+ export interface RunTaskLoopInput {
24
+ repo_path: string;
25
+ goal: string;
26
+ verify_commands: string[];
27
+ agent?: string;
28
+ template?: TaskTemplateName;
29
+ max_iterations?: number;
30
+ task_timeout_seconds?: number;
31
+ auto_fix_tests?: boolean;
32
+ auto_cleanup_artifacts?: boolean;
33
+ stop_on_high_risk?: boolean;
34
+ direct_verify?: boolean;
35
+ direct_verify_commands?: string[];
36
+ direct_verify_timeout_seconds?: number;
37
+ scope_files?: string[];
38
+ isolation_mode?: "current_repo" | "worktree";
39
+ worktree_base_branch?: string;
40
+ worktree_cleanup?: "keep" | "archive" | "delete_ignored_only";
41
+ }
42
+
43
+ export interface RunTaskLoopOutput extends SafeTaskLineage {
44
+ created_task_count: number;
45
+ auto_fix_tests: boolean;
46
+ auto_cleanup_artifacts: boolean;
47
+ direct_verify: boolean;
48
+ isolation_mode: "current_repo" | "worktree";
49
+ worktree: TaskLineageWorktree;
50
+ stopped_before_execution: boolean;
51
+ }
52
+
53
+ interface RunTaskLoopDeps {
54
+ createTask: typeof createTask;
55
+ waitForTask: typeof waitForTask;
56
+ safeResult: typeof safeResult;
57
+ safeAudit: typeof safeAudit;
58
+ safeTestSummary: typeof safeTestSummary;
59
+ createDirectSession: typeof createDirectSession;
60
+ runDirectVerificationBundle: typeof runDirectVerificationBundle;
61
+ safeFinalizeDirectSession: typeof safeFinalizeDirectSession;
62
+ safeAuditDirectSession: typeof safeAuditDirectSession;
63
+ writeTaskLineage: typeof writeTaskLineage;
64
+ createLineageId: typeof createLineageId;
65
+ recommendAgentForTask: typeof recommendAgentForTask;
66
+ createWorktree: typeof createWorktree;
67
+ now: () => Date;
68
+ sleep: (ms: number) => Promise<void>;
69
+ }
70
+
71
+ const DEFAULT_DEPS: RunTaskLoopDeps = {
72
+ createTask,
73
+ waitForTask,
74
+ safeResult,
75
+ safeAudit,
76
+ safeTestSummary,
77
+ createDirectSession,
78
+ runDirectVerificationBundle,
79
+ safeFinalizeDirectSession,
80
+ safeAuditDirectSession,
81
+ writeTaskLineage,
82
+ createLineageId,
83
+ recommendAgentForTask,
84
+ createWorktree,
85
+ now: () => new Date(),
86
+ sleep,
87
+ };
88
+
89
+ const TERMINAL_STATUSES = new Set([
90
+ "done",
91
+ "done_by_agent",
92
+ "failed",
93
+ "failed_verification",
94
+ "failed_scope_violation",
95
+ "failed_policy_violation",
96
+ "canceled",
97
+ ]);
98
+
99
+ export async function runTaskLoop(input: RunTaskLoopInput): Promise<RunTaskLoopOutput> {
100
+ return runTaskLoopWithDeps(input, DEFAULT_DEPS);
101
+ }
102
+
103
+ export async function runTaskLoopWithDeps(
104
+ input: RunTaskLoopInput,
105
+ deps: RunTaskLoopDeps
106
+ ): Promise<RunTaskLoopOutput> {
107
+ const normalized = normalizeInput(input);
108
+ const resolvedRepoPath = guardWorkspacePath(normalized.repo_path, getConfig().workspaceRoot);
109
+ const routing = resolveAgentRouting(normalized, deps);
110
+ const selectedAgent = routing.selected_agent;
111
+ const now = deps.now().toISOString();
112
+ const lineage: TaskLineageRecord = {
113
+ lineage_id: deps.createLineageId(deps.now()),
114
+ goal: normalized.goal,
115
+ repo_path: resolvedRepoPath,
116
+ created_at: now,
117
+ updated_at: now,
118
+ final_status: "blocked",
119
+ stop_reason: "policy_blocked",
120
+ next_action: "inspect_lineage",
121
+ main_task: null,
122
+ fix_tasks: [],
123
+ cleanup_tasks: [],
124
+ direct_sessions: [],
125
+ rounds: [],
126
+ warnings: [],
127
+ errors: [],
128
+ worktree: {
129
+ isolation_mode: normalized.isolation_mode,
130
+ cleanup: normalized.worktree_cleanup,
131
+ status: normalized.isolation_mode === "worktree" ? "active" : "not_used",
132
+ requested_base_branch: normalized.worktree_base_branch,
133
+ next_action: normalized.isolation_mode === "worktree"
134
+ ? "Review and explicitly merge or discard the worktree after acceptance."
135
+ : "none",
136
+ },
137
+ agent_routing: routing,
138
+ };
139
+
140
+ const finalize = (
141
+ finalStatus: TaskLineageRecord["final_status"],
142
+ stopReason: TaskLoopStopReason,
143
+ nextAction: string,
144
+ error?: string
145
+ ): RunTaskLoopOutput => {
146
+ lineage.final_status = finalStatus;
147
+ lineage.stop_reason = stopReason;
148
+ lineage.next_action = nextAction;
149
+ lineage.updated_at = deps.now().toISOString();
150
+ if (error) lineage.errors.push(error);
151
+ const safe = deps.writeTaskLineage(lineage);
152
+ return {
153
+ ...safe,
154
+ created_task_count: [lineage.main_task, ...lineage.fix_tasks, ...lineage.cleanup_tasks].filter(Boolean).length,
155
+ auto_fix_tests: normalized.auto_fix_tests,
156
+ auto_cleanup_artifacts: normalized.auto_cleanup_artifacts,
157
+ direct_verify: normalized.direct_verify,
158
+ isolation_mode: normalized.isolation_mode,
159
+ worktree: safe.worktree,
160
+ stopped_before_execution: lineage.main_task === null,
161
+ };
162
+ };
163
+
164
+ let taskRepoPath = resolvedRepoPath;
165
+ if (normalized.isolation_mode === "worktree") {
166
+ try {
167
+ const worktree = deps.createWorktree(lineage.lineage_id, "task_loop", resolvedRepoPath);
168
+ taskRepoPath = worktree.worktreePath;
169
+ lineage.repo_path = taskRepoPath;
170
+ lineage.worktree = {
171
+ isolation_mode: "worktree",
172
+ worktree_id: worktree.worktreeId,
173
+ worktree_path: worktree.worktreePath,
174
+ branch: worktree.branch,
175
+ requested_base_branch: normalized.worktree_base_branch,
176
+ cleanup: normalized.worktree_cleanup,
177
+ status: "active",
178
+ next_action: "Explicitly inspect and merge_worktree or discard_worktree after reviewing this lineage.",
179
+ };
180
+ } catch (err) {
181
+ lineage.worktree = {
182
+ isolation_mode: "worktree",
183
+ cleanup: normalized.worktree_cleanup,
184
+ requested_base_branch: normalized.worktree_base_branch,
185
+ status: "failed",
186
+ next_action: "Fix worktree creation prerequisites or rerun with isolation_mode=current_repo.",
187
+ };
188
+ return finalize("blocked", "policy_blocked", "Fix worktree creation prerequisites or rerun without worktree isolation.", err instanceof Error ? err.message : String(err));
189
+ }
190
+ }
191
+
192
+ let role: "main" | "fix_tests" = "main";
193
+ let latestFailurePrompt = normalized.goal;
194
+
195
+ for (let iteration = 1; iteration <= normalized.max_iterations; iteration++) {
196
+ const assessmentInput: CreateTaskInput = {
197
+ template: role === "main" ? normalized.template : "fix_tests",
198
+ goal: role === "main" ? normalized.goal : latestFailurePrompt,
199
+ repo_path: taskRepoPath,
200
+ agent: selectedAgent,
201
+ verify_commands: normalized.verify_commands,
202
+ timeout_seconds: normalized.task_timeout_seconds,
203
+ execution_mode: "assess_only",
204
+ };
205
+ const assessment = deps.createTask(assessmentInput as CreateTaskInput & { execution_mode: "assess_only" }) as any;
206
+ if (assessment.decision === "blocked") {
207
+ return finalize("blocked", "high_risk_blocked", "Risk assessment blocked task execution.", assessment.reason_codes?.join(", "));
208
+ }
209
+ if (assessment.decision === "needs_confirm") {
210
+ return finalize("blocked", "user_confirmation_required", "Ask the user to confirm the assessment before executing the loop.");
211
+ }
212
+
213
+ const created = deps.createTask({
214
+ execution_mode: "execute",
215
+ assessment_id: String(assessment.assessment_id || ""),
216
+ }) as any;
217
+ const taskId = String(created.task_id || "");
218
+ if (!taskId) {
219
+ return finalize("failed", "policy_blocked", "create_task returned no task_id.", "create_task returned no task_id");
220
+ }
221
+ if (role === "main") lineage.main_task = taskId;
222
+ else lineage.fix_tasks.push(taskId);
223
+
224
+ const wait = await waitUntilTerminal(taskId, normalized.task_timeout_seconds, deps);
225
+ if (wait.stop_reason) {
226
+ return finalize("blocked", wait.stop_reason, wait.next_action, wait.error);
227
+ }
228
+
229
+ const result = deps.safeResult(taskId, { max_items: 8 }) as any;
230
+ const tests = deps.safeTestSummary(taskId) as any;
231
+ const audit = deps.safeAudit(taskId, { max_items: 8 }) as any;
232
+ const round = buildRound(iteration, taskId, role, result, tests, audit);
233
+ lineage.rounds.push(round);
234
+ lineage.updated_at = deps.now().toISOString();
235
+
236
+ if (isSuccessfulRound(round)) {
237
+ if (normalized.direct_verify) {
238
+ const direct = await runDirectVerification(lineage.lineage_id, normalized, taskRepoPath, deps);
239
+ lineage.direct_sessions.push(direct.evidence);
240
+ if (direct.warning) lineage.warnings.push(direct.warning);
241
+ if (direct.stop_reason) {
242
+ return finalize(direct.final_status, direct.stop_reason, direct.next_action, direct.error);
243
+ }
244
+ }
245
+ return finalize("accepted", "success", "accept");
246
+ }
247
+
248
+ if (isHardStop(round, result, audit, normalized.stop_on_high_risk)) {
249
+ return finalize("blocked", hardStopReason(round, result), round.next_action || "review_task");
250
+ }
251
+
252
+ latestFailurePrompt = buildFixGoal(normalized.goal, result, round);
253
+ if (!normalized.auto_fix_tests || round.status !== "failed_verification") {
254
+ return finalize("needs_fix", "verification_failed", round.next_action || "create_followup_task");
255
+ }
256
+
257
+ role = "fix_tests";
258
+ }
259
+
260
+ return finalize("needs_fix", "max_iterations_reached", "review_lineage_and_create_manual_followup");
261
+ }
262
+
263
+ async function waitUntilTerminal(
264
+ taskId: string,
265
+ timeoutSeconds: number,
266
+ deps: RunTaskLoopDeps
267
+ ): Promise<{ stop_reason?: TaskLoopStopReason; next_action: string; error?: string }> {
268
+ const deadline = Date.now() + timeoutSeconds * 1000;
269
+ while (Date.now() < deadline) {
270
+ const waited = await deps.waitForTask(taskId, 30) as any;
271
+ if (waited.terminal || TERMINAL_STATUSES.has(String(waited.status))) {
272
+ return { next_action: waited.next_action || "safe_audit" };
273
+ }
274
+ if (waited.next_tool_call?.name === "health_check" || waited.continuation_required === false) {
275
+ return {
276
+ stop_reason: "watcher_blocked",
277
+ next_action: waited.next_action || "health_check",
278
+ error: waited.progress_summary?.hint || "Watcher is blocked or unavailable.",
279
+ };
280
+ }
281
+ await deps.sleep(250);
282
+ }
283
+ return {
284
+ stop_reason: "agent_timeout",
285
+ next_action: "inspect_task_status",
286
+ error: `Task loop timed out waiting for ${taskId}.`,
287
+ };
288
+ }
289
+
290
+ function buildRound(
291
+ iteration: number,
292
+ taskId: string,
293
+ role: TaskLineageRound["role"],
294
+ result: any,
295
+ tests: any,
296
+ audit: any
297
+ ): TaskLineageRound {
298
+ const failChecks = Array.isArray(audit.fail_checks) ? audit.fail_checks.map((entry: any) => String(entry.name || entry)) : [];
299
+ const warnChecks = Array.isArray(audit.warn_checks) ? audit.warn_checks.map((entry: any) => String(entry.name || entry)) : [];
300
+ return {
301
+ iteration,
302
+ task_id: taskId,
303
+ role,
304
+ status: String(result.status || "unknown"),
305
+ terminal: Boolean(result.terminal),
306
+ verification_status: String(tests.status || result.verification?.status || "not_available"),
307
+ audit_verdict: String(audit.verdict || audit.acceptance?.verdict || "unknown"),
308
+ fail_checks: failChecks,
309
+ warn_checks: warnChecks,
310
+ next_action: String(result.next_action || audit.recommended_next_actions?.[0] || "review_task"),
311
+ };
312
+ }
313
+
314
+ function isSuccessfulRound(round: TaskLineageRound): boolean {
315
+ return (
316
+ round.terminal &&
317
+ ["done_by_agent", "done", "accepted"].includes(round.status) &&
318
+ round.verification_status === "passed" &&
319
+ round.fail_checks.length === 0 &&
320
+ round.warn_checks.length === 0 &&
321
+ round.audit_verdict === "pass"
322
+ );
323
+ }
324
+
325
+ function isHardStop(round: TaskLineageRound, result: any, audit: any, stopOnHighRisk: boolean): boolean {
326
+ if (["failed_scope_violation", "failed_policy_violation", "canceled"].includes(round.status)) return true;
327
+ if (!stopOnHighRisk) return false;
328
+ const checkNames = [...round.fail_checks, ...round.warn_checks].join(" ").toLowerCase();
329
+ const reason = String(result.failure_reason || audit.acceptance?.reason || "").toLowerCase();
330
+ return /scope|secret|sensitive|publish|release|policy|push/.test(`${checkNames} ${reason}`);
331
+ }
332
+
333
+ function hardStopReason(round: TaskLineageRound, result: any): TaskLoopStopReason {
334
+ if (round.status === "failed_scope_violation" || round.status === "failed_policy_violation") return "policy_blocked";
335
+ if (String(result.failure_reason || "").toLowerCase().includes("timeout")) return "agent_timeout";
336
+ return "high_risk_blocked";
337
+ }
338
+
339
+ function buildFixGoal(originalGoal: string, result: any, round: TaskLineageRound): string {
340
+ const failedCommand = result.failed_command ? ` Failed command: ${result.failed_command}.` : "";
341
+ return [
342
+ `Fix the failing verification for this PatchWarden loop without changing unrelated behavior.`,
343
+ `Original goal: ${originalGoal}`,
344
+ `Previous task: ${round.task_id}. Status: ${round.status}. Verification: ${round.verification_status}.${failedCommand}`,
345
+ "Do not commit, push, publish, weaken tests, or touch files outside the resolved repository path.",
346
+ ].join("\n");
347
+ }
348
+
349
+ function resolveAgentRouting(
350
+ normalized: ReturnType<typeof normalizeInput>,
351
+ deps: RunTaskLoopDeps
352
+ ) {
353
+ if (normalized.agent && normalized.agent !== "auto") {
354
+ return {
355
+ requested_agent: normalized.agent,
356
+ selected_agent: normalized.agent,
357
+ reason: "explicit agent supplied",
358
+ fallback: false,
359
+ };
360
+ }
361
+ const recommendation = deps.recommendAgentForTask({
362
+ repo_path: normalized.repo_path,
363
+ goal: normalized.goal,
364
+ scope_files: normalized.scope_files,
365
+ template: normalized.template,
366
+ });
367
+ return {
368
+ requested_agent: normalized.agent || null,
369
+ selected_agent: recommendation.recommended_agent,
370
+ reason: recommendation.reason,
371
+ fallback: recommendation.fallback,
372
+ };
373
+ }
374
+
375
+ async function runDirectVerification(
376
+ lineageId: string,
377
+ normalized: ReturnType<typeof normalizeInput>,
378
+ repoPath: string,
379
+ deps: RunTaskLoopDeps
380
+ ): Promise<{
381
+ evidence: TaskLineageDirectSession;
382
+ stop_reason?: TaskLoopStopReason;
383
+ final_status: TaskLineageRecord["final_status"];
384
+ next_action: string;
385
+ error?: string;
386
+ warning?: string;
387
+ }> {
388
+ const config = getConfig();
389
+ if (config.enableDirectProfile !== true) {
390
+ return {
391
+ evidence: {
392
+ session_id: "not_created",
393
+ status: "skipped",
394
+ audit_decision: "not_run",
395
+ next_action: "Enable Direct profile locally before requesting direct_verify.",
396
+ },
397
+ stop_reason: "direct_profile_disabled",
398
+ final_status: "blocked",
399
+ next_action: "Enable enableDirectProfile locally or rerun run_task_loop with direct_verify=false.",
400
+ error: "Direct profile is disabled by local config.",
401
+ };
402
+ }
403
+
404
+ let sessionId = "";
405
+ try {
406
+ const session = deps.createDirectSession({
407
+ repo_path: repoPath,
408
+ title: `Direct verification for ${lineageId}`,
409
+ });
410
+ sessionId = session.session_id;
411
+ const bundle = await deps.runDirectVerificationBundle({
412
+ session_id: sessionId,
413
+ commands: normalized.direct_verify_commands,
414
+ timeout_seconds: normalized.direct_verify_timeout_seconds,
415
+ });
416
+ const finalized = deps.safeFinalizeDirectSession(sessionId, { max_items: 8 }) as any;
417
+ const audit = deps.safeAuditDirectSession(sessionId, { max_items: 8 }) as any;
418
+ const evidence: TaskLineageDirectSession = {
419
+ session_id: sessionId,
420
+ status: bundle.status,
421
+ command_count: bundle.command_count,
422
+ passed_commands: bundle.passed_commands,
423
+ failed_commands: bundle.failed_commands,
424
+ timed_out_commands: bundle.timed_out_commands,
425
+ audit_decision: audit.decision || "not_run",
426
+ changed_files_total: Number(finalized.changed_files_total || audit.evidence?.changed_files_total || 0),
427
+ next_action: String(audit.next_action || bundle.next_action || "review_direct_session"),
428
+ };
429
+ if (bundle.status !== "passed") {
430
+ return {
431
+ evidence,
432
+ stop_reason: "direct_verification_failed",
433
+ final_status: "needs_fix",
434
+ next_action: "Review Direct verification summary and create a normal follow-up task.",
435
+ error: "Direct verification failed.",
436
+ };
437
+ }
438
+ if (audit.decision === "fail") {
439
+ return {
440
+ evidence,
441
+ stop_reason: "direct_audit_failed",
442
+ final_status: "blocked",
443
+ next_action: "Review Direct audit findings before accepting the loop.",
444
+ error: "Direct audit failed.",
445
+ };
446
+ }
447
+ return {
448
+ evidence,
449
+ final_status: "accepted",
450
+ next_action: "accept",
451
+ warning: audit.decision === "warn" ? "Direct audit completed with warnings." : undefined,
452
+ };
453
+ } catch (err) {
454
+ const message = err instanceof Error ? err.message : String(err);
455
+ return {
456
+ evidence: {
457
+ session_id: sessionId || "not_created",
458
+ status: "failed",
459
+ audit_decision: "not_run",
460
+ next_action: "Review Direct verification configuration and command allow-list.",
461
+ },
462
+ stop_reason: "direct_verification_failed",
463
+ final_status: "needs_fix",
464
+ next_action: "Review Direct verification configuration and command allow-list.",
465
+ error: message,
466
+ };
467
+ }
468
+ }
469
+
470
+ function normalizeInput(input: RunTaskLoopInput): Required<Omit<RunTaskLoopInput, "agent" | "scope_files" | "worktree_base_branch">> & { agent?: string; scope_files?: string[]; worktree_base_branch?: string } {
471
+ const config = getConfig();
472
+ const repoPath = String(input.repo_path || "").trim();
473
+ const goal = String(input.goal || "").trim();
474
+ if (!repoPath) throw new Error("repo_path is required.");
475
+ if (!goal) throw new Error("goal is required.");
476
+ if (!Array.isArray(input.verify_commands) || input.verify_commands.length === 0) {
477
+ throw new Error("verify_commands must contain at least one command.");
478
+ }
479
+ const maxIterations = input.max_iterations ?? 3;
480
+ if (!Number.isInteger(maxIterations) || maxIterations < 1 || maxIterations > 5) {
481
+ throw new Error("max_iterations must be an integer from 1 to 5.");
482
+ }
483
+ const timeoutSeconds = input.task_timeout_seconds ?? config.defaultTaskTimeoutSeconds;
484
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > config.maxTaskTimeoutSeconds) {
485
+ throw new Error(`task_timeout_seconds must be an integer from 1 to ${config.maxTaskTimeoutSeconds}.`);
486
+ }
487
+ const template = input.template || "feature_small";
488
+ if (template !== "inspect_only" && template !== "feature_small" && template !== "release_check") {
489
+ throw new Error('template must be "inspect_only", "feature_small", or "release_check".');
490
+ }
491
+ const directVerifyCommands = Array.isArray(input.direct_verify_commands) && input.direct_verify_commands.length > 0
492
+ ? input.direct_verify_commands
493
+ : input.verify_commands;
494
+ if (input.direct_verify === true && (!Array.isArray(directVerifyCommands) || directVerifyCommands.length === 0)) {
495
+ throw new Error("direct_verify_commands must contain at least one command when provided.");
496
+ }
497
+ const directVerifyTimeout = input.direct_verify_timeout_seconds ?? 120;
498
+ const maxDirectTimeout = Math.min(config.maxTaskTimeoutSeconds, config.directSessionTtlSeconds);
499
+ if (!Number.isInteger(directVerifyTimeout) || directVerifyTimeout < 1 || directVerifyTimeout > maxDirectTimeout) {
500
+ throw new Error(`direct_verify_timeout_seconds must be an integer from 1 to ${maxDirectTimeout}.`);
501
+ }
502
+ return {
503
+ repo_path: repoPath,
504
+ goal,
505
+ verify_commands: input.verify_commands.map((command) => String(command).trim()),
506
+ agent: input.agent ? String(input.agent) : undefined,
507
+ template,
508
+ max_iterations: maxIterations,
509
+ task_timeout_seconds: timeoutSeconds,
510
+ auto_fix_tests: input.auto_fix_tests !== false,
511
+ auto_cleanup_artifacts: input.auto_cleanup_artifacts !== false,
512
+ stop_on_high_risk: input.stop_on_high_risk !== false,
513
+ direct_verify: input.direct_verify === true,
514
+ direct_verify_commands: directVerifyCommands.map((command) => String(command).trim()),
515
+ direct_verify_timeout_seconds: directVerifyTimeout,
516
+ scope_files: Array.isArray(input.scope_files)
517
+ ? input.scope_files.map((entry) => String(entry).trim()).filter(Boolean).slice(0, 50)
518
+ : undefined,
519
+ isolation_mode: input.isolation_mode === "worktree" ? "worktree" : "current_repo",
520
+ worktree_base_branch: input.worktree_base_branch ? String(input.worktree_base_branch).trim().slice(0, 160) : undefined,
521
+ worktree_cleanup:
522
+ input.worktree_cleanup === "archive" || input.worktree_cleanup === "delete_ignored_only"
523
+ ? input.worktree_cleanup
524
+ : "keep",
525
+ };
526
+ }
@@ -16,6 +16,10 @@ export interface RunVerificationOutput {
16
16
  exit_code: number | null;
17
17
  passed: boolean;
18
18
  timed_out: boolean;
19
+ redacted: boolean;
20
+ redaction_categories: string[];
21
+ started_at: string;
22
+ finished_at: string;
19
23
  stdout_tail: string;
20
24
  stderr_tail: string;
21
25
  log_path: string;
@@ -48,6 +52,10 @@ export async function runVerification(
48
52
  exit_code: result.run.exit_code,
49
53
  passed: result.run.passed,
50
54
  timed_out: result.run.timed_out,
55
+ redacted: Boolean(result.run.redacted),
56
+ redaction_categories: result.run.redaction_categories || [],
57
+ started_at: result.run.started_at,
58
+ finished_at: result.run.finished_at,
51
59
  stdout_tail: result.run.stdout_tail,
52
60
  stderr_tail: result.run.stderr_tail,
53
61
  log_path: result.run.log_path,
@@ -3,13 +3,26 @@ import { resolve, join } from "node:path";
3
3
  import { getTasksDir, getConfig, type PatchWardenConfig } from "../config.js";
4
4
  import { guardReadPath } from "../security/pathGuard.js";
5
5
  import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
- import type { TaskStatus, TaskPhase } from "./createTask.js";
6
+ import type { TaskStatus, TaskPhase, AcceptanceStatus } from "./createTask.js";
7
7
  import { readTaskRuntime } from "../taskRuntime.js";
8
8
  import { readWatcherStatus } from "../watcherStatus.js";
9
+ import { diagnoseTask, type DiagnosisType, type DiagnosisConfidence } from "./diagnoseTask.js";
9
10
 
10
11
  export interface SafeStatusOutput {
11
12
  task_id: string;
12
13
  status: TaskStatus | "not_found";
14
+ /**
15
+ * v0.7.0: For done_by_agent or accepted tasks, legacy_status echoes "done"
16
+ * so older UI/clients that do not understand those statuses still see a familiar value.
17
+ * Null for all other statuses.
18
+ */
19
+ legacy_status: "done" | null;
20
+ /**
21
+ * v0.7.0: Acceptance status — only meaningful for done_by_agent.
22
+ * - "pending": done_by_agent reached but not yet audited/accepted
23
+ * - null: status has no acceptance semantics
24
+ */
25
+ acceptance_status: AcceptanceStatus;
13
26
  phase: TaskPhase | null;
14
27
  created_at: string | null;
15
28
  started_at: string | null;
@@ -22,6 +35,20 @@ export interface SafeStatusOutput {
22
35
  watcher_state: string | null;
23
36
  error_code: string | null;
24
37
  error_summary: string | null;
38
+ /**
39
+ * v0.7.0: Seconds since the last heartbeat. Null when heartbeat is missing
40
+ * or unparseable. Used by callers to detect stale running tasks.
41
+ */
42
+ stale_seconds: number | null;
43
+ /**
44
+ * v0.7.0: Lightweight diagnosis snapshot derived from the same multi-signal
45
+ * logic as diagnose_task. Only the type/confidence are surfaced here;
46
+ * call diagnose_task for the full evidence and safe_actions.
47
+ *
48
+ * This field is null when status is already terminal (no diagnosis needed)
49
+ * or when diagnosis itself fails (do not block safe_status).
50
+ */
51
+ diagnosis: { type: DiagnosisType; confidence: DiagnosisConfidence } | null;
25
52
  }
26
53
 
27
54
  export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStatusOutput {
@@ -37,6 +64,8 @@ export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStat
37
64
  return {
38
65
  task_id: taskId,
39
66
  status: "not_found",
67
+ legacy_status: null,
68
+ acceptance_status: null,
40
69
  phase: null,
41
70
  created_at: null,
42
71
  started_at: null,
@@ -49,6 +78,8 @@ export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStat
49
78
  watcher_state: null,
50
79
  error_code: null,
51
80
  error_summary: null,
81
+ stale_seconds: null,
82
+ diagnosis: null,
52
83
  };
53
84
  }
54
85
 
@@ -77,9 +108,47 @@ export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStat
77
108
  errorSummary = `Task ended with status: ${status.status}`;
78
109
  }
79
110
 
111
+ // v0.7.2: derive legacy_status and acceptance_status
112
+ const statusStr = (status.status as TaskStatus) || "not_found";
113
+ const legacyStatus: "done" | null = (statusStr === "done_by_agent" || statusStr === "accepted") ? "done" : null;
114
+ const VALID_ACCEPTANCE: AcceptanceStatus[] = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
115
+ const acceptanceStatus: AcceptanceStatus = statusStr === "done_by_agent"
116
+ ? (typeof status.acceptance_status === "string" && VALID_ACCEPTANCE.includes(status.acceptance_status as AcceptanceStatus)
117
+ ? (status.acceptance_status as AcceptanceStatus)
118
+ : "pending") // default for done_by_agent when field missing
119
+ : null;
120
+
121
+ // v0.7.0: compute stale_seconds from the most recent heartbeat
122
+ const heartbeatStr =
123
+ (typeof runtime.last_heartbeat_at === "string" && runtime.last_heartbeat_at) ||
124
+ (typeof status.last_heartbeat_at === "string" && status.last_heartbeat_at) ||
125
+ (typeof status.updated_at === "string" && status.updated_at) ||
126
+ null;
127
+ let staleSeconds: number | null = null;
128
+ if (heartbeatStr) {
129
+ const heartbeatMs = Date.parse(heartbeatStr);
130
+ if (Number.isFinite(heartbeatMs)) {
131
+ staleSeconds = Math.max(0, Math.round((Date.now() - heartbeatMs) / 1000));
132
+ }
133
+ }
134
+
135
+ // v0.7.0: lightweight diagnosis snapshot — only for non-terminal tasks.
136
+ // Wrap in try/catch so a diagnosis failure never breaks safe_status.
137
+ let diagnosis: { type: DiagnosisType; confidence: DiagnosisConfidence } | null = null;
138
+ if (statusStr === "running" || statusStr === "collecting_artifacts" || statusStr === "pending") {
139
+ try {
140
+ const result = diagnoseTask({ task_id: taskId }, cfg);
141
+ diagnosis = { type: result.diagnosis, confidence: result.confidence };
142
+ } catch {
143
+ diagnosis = null;
144
+ }
145
+ }
146
+
80
147
  return {
81
148
  task_id: taskId,
82
- status: (status.status as TaskStatus) || "not_found",
149
+ status: statusStr,
150
+ legacy_status: legacyStatus,
151
+ acceptance_status: acceptanceStatus,
83
152
  phase,
84
153
  created_at: typeof status.created_at === "string" ? status.created_at : null,
85
154
  started_at: typeof status.started_at === "string" ? status.started_at : null,
@@ -92,5 +161,7 @@ export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStat
92
161
  watcher_state: watcher.status,
93
162
  error_code: errorCode,
94
163
  error_summary: errorSummary,
164
+ stale_seconds: staleSeconds,
165
+ diagnosis,
95
166
  };
96
167
  }