patchwarden 0.6.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/PatchWarden-Control-Tray.cmd +11 -0
  2. package/PatchWarden-Control.cmd +6 -0
  3. package/PatchWarden-Desktop.cmd +5 -0
  4. package/PatchWarden.cmd +1 -1
  5. package/README.en.md +112 -24
  6. package/README.md +36 -25
  7. package/Restart-PatchWarden-Control.cmd +6 -0
  8. package/Stop-PatchWarden.cmd +11 -0
  9. package/dist/agents/agentRouter.d.ts +40 -0
  10. package/dist/agents/agentRouter.js +95 -0
  11. package/dist/config.d.ts +1 -1
  12. package/dist/config.js +6 -2
  13. package/dist/controlCenter.d.ts +14 -0
  14. package/dist/controlCenter.js +2015 -0
  15. package/dist/doctor.js +35 -4
  16. package/dist/goal/acceptanceEngine.d.ts +62 -0
  17. package/dist/goal/acceptanceEngine.js +103 -0
  18. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  19. package/dist/goal/acceptanceTemplate.js +104 -0
  20. package/dist/goal/goalGraph.d.ts +58 -0
  21. package/dist/goal/goalGraph.js +189 -0
  22. package/dist/goal/goalProgress.d.ts +81 -0
  23. package/dist/goal/goalProgress.js +216 -0
  24. package/dist/goal/goalStatus.d.ts +60 -0
  25. package/dist/goal/goalStatus.js +137 -0
  26. package/dist/goal/goalStore.d.ts +79 -0
  27. package/dist/goal/goalStore.js +211 -0
  28. package/dist/goal/handoffExport.d.ts +34 -0
  29. package/dist/goal/handoffExport.js +183 -0
  30. package/dist/goal/subgoalSync.d.ts +40 -0
  31. package/dist/goal/subgoalSync.js +72 -0
  32. package/dist/goal/worktreeManager.d.ts +96 -0
  33. package/dist/goal/worktreeManager.js +292 -0
  34. package/dist/logging.d.ts +44 -0
  35. package/dist/logging.js +68 -0
  36. package/dist/release/releaseGate.d.ts +99 -0
  37. package/dist/release/releaseGate.js +475 -0
  38. package/dist/runner/postTaskCleanup.d.ts +13 -0
  39. package/dist/runner/postTaskCleanup.js +147 -0
  40. package/dist/runner/runTask.js +50 -10
  41. package/dist/security/discoveryTokenStore.d.ts +62 -0
  42. package/dist/security/discoveryTokenStore.js +100 -0
  43. package/dist/security/toolInvocationGuard.d.ts +35 -0
  44. package/dist/security/toolInvocationGuard.js +153 -0
  45. package/dist/smoke-test.js +18 -10
  46. package/dist/taskRuntime.d.ts +17 -0
  47. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  48. package/dist/test/unit/acceptance-engine.test.js +228 -0
  49. package/dist/test/unit/agent-router.test.d.ts +1 -0
  50. package/dist/test/unit/agent-router.test.js +287 -0
  51. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  52. package/dist/test/unit/audit-checks.test.js +350 -0
  53. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  54. package/dist/test/unit/diagnose-task.test.js +457 -0
  55. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  56. package/dist/test/unit/discovery-token-store.test.js +139 -0
  57. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  58. package/dist/test/unit/goal-graph.test.js +298 -0
  59. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  60. package/dist/test/unit/goal-progress.test.js +381 -0
  61. package/dist/test/unit/goal-status.test.d.ts +1 -0
  62. package/dist/test/unit/goal-status.test.js +215 -0
  63. package/dist/test/unit/goal-store.test.d.ts +1 -0
  64. package/dist/test/unit/goal-store.test.js +253 -0
  65. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  66. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  67. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  68. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  69. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  70. package/dist/test/unit/handoff-export.test.js +263 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  72. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  73. package/dist/test/unit/logging.test.js +127 -5
  74. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  75. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  76. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  77. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  78. package/dist/test/unit/release-gate.test.d.ts +1 -0
  79. package/dist/test/unit/release-gate.test.js +242 -0
  80. package/dist/test/unit/safe-views.test.d.ts +1 -0
  81. package/dist/test/unit/safe-views.test.js +171 -0
  82. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  83. package/dist/test/unit/schema-drift-check.test.js +175 -0
  84. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  85. package/dist/test/unit/subgoal-sync.test.js +183 -0
  86. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  87. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  88. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  89. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  90. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  91. package/dist/test/unit/toolSearch.test.js +571 -0
  92. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  93. package/dist/test/unit/worktree-manager.test.js +176 -0
  94. package/dist/tools/auditTask.d.ts +103 -1
  95. package/dist/tools/auditTask.js +461 -8
  96. package/dist/tools/cancelTask.js +1 -1
  97. package/dist/tools/checkReleaseGate.d.ts +21 -0
  98. package/dist/tools/checkReleaseGate.js +22 -0
  99. package/dist/tools/createTask.d.ts +18 -2
  100. package/dist/tools/createTask.js +29 -1
  101. package/dist/tools/diagnoseTask.d.ts +45 -0
  102. package/dist/tools/diagnoseTask.js +347 -0
  103. package/dist/tools/discardWorktree.d.ts +24 -0
  104. package/dist/tools/discardWorktree.js +27 -0
  105. package/dist/tools/discoverTools.d.ts +11 -0
  106. package/dist/tools/discoverTools.js +39 -0
  107. package/dist/tools/explainTool.d.ts +11 -0
  108. package/dist/tools/explainTool.js +21 -0
  109. package/dist/tools/getTaskSummary.js +2 -1
  110. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  111. package/dist/tools/goalSubgoalTask.js +110 -0
  112. package/dist/tools/healthCheck.d.ts +1 -0
  113. package/dist/tools/healthCheck.js +5 -1
  114. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  115. package/dist/tools/invokeDiscoveredTool.js +112 -0
  116. package/dist/tools/listTasks.d.ts +3 -1
  117. package/dist/tools/listTasks.js +14 -1
  118. package/dist/tools/mergeWorktree.d.ts +24 -0
  119. package/dist/tools/mergeWorktree.js +27 -0
  120. package/dist/tools/reconcileTasks.d.ts +41 -0
  121. package/dist/tools/reconcileTasks.js +352 -0
  122. package/dist/tools/registry.js +550 -0
  123. package/dist/tools/safeStatus.d.ts +31 -1
  124. package/dist/tools/safeStatus.js +43 -1
  125. package/dist/tools/safeViews.d.ts +256 -0
  126. package/dist/tools/safeViews.js +250 -0
  127. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  128. package/dist/tools/schemaDriftCheck.js +80 -0
  129. package/dist/tools/toolCatalog.d.ts +4 -3
  130. package/dist/tools/toolCatalog.js +33 -11
  131. package/dist/tools/toolRegistry.d.ts +61 -0
  132. package/dist/tools/toolRegistry.js +724 -0
  133. package/dist/tools/toolSearch.d.ts +110 -0
  134. package/dist/tools/toolSearch.js +331 -0
  135. package/dist/tools/toolUsageStats.d.ts +41 -0
  136. package/dist/tools/toolUsageStats.js +116 -0
  137. package/dist/tools/waitForTask.js +1 -0
  138. package/dist/version.d.ts +2 -2
  139. package/dist/version.js +2 -2
  140. package/dist/watcherStatus.d.ts +29 -0
  141. package/dist/watcherStatus.js +92 -1
  142. package/docs/control-center/README.md +33 -0
  143. package/docs/control-center/control-center-daily-driver.md +211 -0
  144. package/docs/control-center/control-center-mvp.md +205 -0
  145. package/docs/control-center/control-center-phase2.md +159 -0
  146. package/docs/demo.md +3 -0
  147. package/docs/release-v0.6.4.md +45 -0
  148. package/examples/openai-tunnel/README.md +5 -5
  149. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  150. package/package.json +25 -16
  151. package/scripts/README.md +47 -0
  152. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  153. package/scripts/checks/control-center-smoke.js +1098 -0
  154. package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
  155. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
  156. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
  157. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
  158. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
  159. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
  160. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  161. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  162. package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
  163. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
  164. package/scripts/control/control-center-tray.ps1 +281 -0
  165. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  166. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  167. package/scripts/control/restart-control-center.ps1 +173 -0
  168. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  169. package/scripts/control/start-control-center.ps1 +263 -0
  170. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  171. package/scripts/control/stop-patchwarden.ps1 +114 -0
  172. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  173. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  174. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  175. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  176. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  177. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  178. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  179. package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
  180. package/src/agents/agentRouter.ts +149 -0
  181. package/src/config.ts +9 -3
  182. package/src/controlCenter.ts +2166 -0
  183. package/src/doctor.ts +40 -5
  184. package/src/goal/acceptanceEngine.ts +160 -0
  185. package/src/goal/acceptanceTemplate.ts +121 -0
  186. package/src/goal/goalGraph.ts +225 -0
  187. package/src/goal/goalProgress.ts +301 -0
  188. package/src/goal/goalStatus.ts +234 -0
  189. package/src/goal/goalStore.ts +306 -0
  190. package/src/goal/handoffExport.ts +211 -0
  191. package/src/goal/subgoalSync.ts +82 -0
  192. package/src/goal/worktreeManager.ts +404 -0
  193. package/src/logging.ts +91 -0
  194. package/src/release/releaseGate.ts +567 -0
  195. package/src/runner/postTaskCleanup.ts +154 -0
  196. package/src/runner/runTask.ts +49 -10
  197. package/src/security/discoveryTokenStore.ts +157 -0
  198. package/src/security/toolInvocationGuard.ts +251 -0
  199. package/src/smoke-test.ts +15 -7
  200. package/src/taskRuntime.ts +17 -0
  201. package/src/test/unit/acceptance-engine.test.ts +261 -0
  202. package/src/test/unit/agent-router.test.ts +342 -0
  203. package/src/test/unit/audit-checks.test.ts +567 -0
  204. package/src/test/unit/diagnose-task.test.ts +544 -0
  205. package/src/test/unit/discovery-token-store.test.ts +181 -0
  206. package/src/test/unit/goal-graph.test.ts +347 -0
  207. package/src/test/unit/goal-progress.test.ts +538 -0
  208. package/src/test/unit/goal-status.test.ts +270 -0
  209. package/src/test/unit/goal-store.test.ts +318 -0
  210. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  211. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  212. package/src/test/unit/handoff-export.test.ts +295 -0
  213. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  214. package/src/test/unit/logging.test.ts +177 -5
  215. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  216. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  217. package/src/test/unit/release-gate.test.ts +314 -0
  218. package/src/test/unit/safe-views.test.ts +184 -0
  219. package/src/test/unit/schema-drift-check.test.ts +258 -0
  220. package/src/test/unit/subgoal-sync.test.ts +236 -0
  221. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  222. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  223. package/src/test/unit/toolSearch.test.ts +756 -0
  224. package/src/test/unit/worktree-manager.test.ts +247 -0
  225. package/src/tools/auditTask.ts +831 -402
  226. package/src/tools/cancelTask.ts +1 -1
  227. package/src/tools/checkReleaseGate.ts +45 -0
  228. package/src/tools/createTask.ts +64 -6
  229. package/src/tools/diagnoseTask.ts +460 -0
  230. package/src/tools/discardWorktree.ts +42 -0
  231. package/src/tools/discoverTools.ts +51 -0
  232. package/src/tools/explainTool.ts +26 -0
  233. package/src/tools/getTaskSummary.ts +2 -1
  234. package/src/tools/goalSubgoalTask.ts +170 -0
  235. package/src/tools/healthCheck.ts +4 -1
  236. package/src/tools/invokeDiscoveredTool.ts +163 -0
  237. package/src/tools/listTasks.ts +16 -2
  238. package/src/tools/mergeWorktree.ts +42 -0
  239. package/src/tools/reconcileTasks.ts +464 -0
  240. package/src/tools/registry.ts +618 -1
  241. package/src/tools/safeStatus.ts +73 -2
  242. package/src/tools/safeViews.ts +271 -0
  243. package/src/tools/schemaDriftCheck.ts +120 -0
  244. package/src/tools/toolCatalog.ts +34 -11
  245. package/src/tools/toolRegistry.ts +786 -0
  246. package/src/tools/toolSearch.ts +464 -0
  247. package/src/tools/toolUsageStats.ts +151 -0
  248. package/src/tools/waitForTask.ts +1 -0
  249. package/src/version.ts +2 -2
  250. package/src/watcherStatus.ts +97 -1
  251. package/ui/colors_and_type.css +141 -0
  252. package/ui/pages/audit.html +743 -0
  253. package/ui/pages/dashboard.html +1154 -0
  254. package/ui/pages/direct-sessions.html +652 -0
  255. package/ui/pages/logs.html +502 -0
  256. package/ui/pages/task-detail.html +1229 -0
  257. package/ui/pages/tasks.html +702 -0
  258. package/ui/pages/workspace.html +947 -0
  259. package/ui/partials/project-shell.html +362 -0
  260. package/ui/vendor/lucide.js +12 -0
  261. package/ui/vendor/tailwindcss-browser.js +947 -0
@@ -5,6 +5,7 @@ import { guardReadPath } from "../security/pathGuard.js";
5
5
  import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
6
  import { readTaskRuntime } from "../taskRuntime.js";
7
7
  import { readWatcherStatus } from "../watcherStatus.js";
8
+ import { diagnoseTask } from "./diagnoseTask.js";
8
9
  export function safeStatus(taskId, config) {
9
10
  const cfg = config || getConfig();
10
11
  const tasksDir = getTasksDir(cfg);
@@ -16,6 +17,8 @@ export function safeStatus(taskId, config) {
16
17
  return {
17
18
  task_id: taskId,
18
19
  status: "not_found",
20
+ legacy_status: null,
21
+ acceptance_status: null,
19
22
  phase: null,
20
23
  created_at: null,
21
24
  started_at: null,
@@ -28,6 +31,8 @@ export function safeStatus(taskId, config) {
28
31
  watcher_state: null,
29
32
  error_code: null,
30
33
  error_summary: null,
34
+ stale_seconds: null,
35
+ diagnosis: null,
31
36
  };
32
37
  }
33
38
  guardReadPath(statusFile, cfg.workspaceRoot, cfg.tasksDir);
@@ -53,9 +58,44 @@ export function safeStatus(taskId, config) {
53
58
  errorCode = status.status;
54
59
  errorSummary = `Task ended with status: ${status.status}`;
55
60
  }
61
+ // v0.7.2: derive legacy_status and acceptance_status
62
+ const statusStr = status.status || "not_found";
63
+ const legacyStatus = (statusStr === "done_by_agent" || statusStr === "accepted") ? "done" : null;
64
+ const VALID_ACCEPTANCE = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
65
+ const acceptanceStatus = statusStr === "done_by_agent"
66
+ ? (typeof status.acceptance_status === "string" && VALID_ACCEPTANCE.includes(status.acceptance_status)
67
+ ? status.acceptance_status
68
+ : "pending") // default for done_by_agent when field missing
69
+ : null;
70
+ // v0.7.0: compute stale_seconds from the most recent heartbeat
71
+ const heartbeatStr = (typeof runtime.last_heartbeat_at === "string" && runtime.last_heartbeat_at) ||
72
+ (typeof status.last_heartbeat_at === "string" && status.last_heartbeat_at) ||
73
+ (typeof status.updated_at === "string" && status.updated_at) ||
74
+ null;
75
+ let staleSeconds = null;
76
+ if (heartbeatStr) {
77
+ const heartbeatMs = Date.parse(heartbeatStr);
78
+ if (Number.isFinite(heartbeatMs)) {
79
+ staleSeconds = Math.max(0, Math.round((Date.now() - heartbeatMs) / 1000));
80
+ }
81
+ }
82
+ // v0.7.0: lightweight diagnosis snapshot — only for non-terminal tasks.
83
+ // Wrap in try/catch so a diagnosis failure never breaks safe_status.
84
+ let diagnosis = null;
85
+ if (statusStr === "running" || statusStr === "collecting_artifacts" || statusStr === "pending") {
86
+ try {
87
+ const result = diagnoseTask({ task_id: taskId }, cfg);
88
+ diagnosis = { type: result.diagnosis, confidence: result.confidence };
89
+ }
90
+ catch {
91
+ diagnosis = null;
92
+ }
93
+ }
56
94
  return {
57
95
  task_id: taskId,
58
- status: status.status || "not_found",
96
+ status: statusStr,
97
+ legacy_status: legacyStatus,
98
+ acceptance_status: acceptanceStatus,
59
99
  phase,
60
100
  created_at: typeof status.created_at === "string" ? status.created_at : null,
61
101
  started_at: typeof status.started_at === "string" ? status.started_at : null,
@@ -68,5 +108,7 @@ export function safeStatus(taskId, config) {
68
108
  watcher_state: watcher.status,
69
109
  error_code: errorCode,
70
110
  error_summary: errorSummary,
111
+ stale_seconds: staleSeconds,
112
+ diagnosis,
71
113
  };
72
114
  }
@@ -0,0 +1,256 @@
1
+ export interface SafeViewOptions {
2
+ max_items?: number;
3
+ }
4
+ export declare function safeResult(taskId: string, options?: SafeViewOptions): {
5
+ task_id: string;
6
+ status: string;
7
+ terminal: boolean;
8
+ acceptance_status: "failed" | "pending" | "ready_for_review" | "needs_review";
9
+ phase: string;
10
+ repo_path: string;
11
+ changed_files_total: number;
12
+ out_of_scope_changes_total: number;
13
+ artifact_hygiene_counts: Record<string, number>;
14
+ release_artifacts_count: number;
15
+ artifact_status: string | null;
16
+ verification: {
17
+ status: string;
18
+ command_count: number;
19
+ passed_commands: number;
20
+ failed_commands: number;
21
+ skipped_commands: number;
22
+ headline: string;
23
+ };
24
+ warnings: string[];
25
+ errors: string[];
26
+ failure_reason: string | null;
27
+ failed_command: string | null;
28
+ next_action: string;
29
+ execution_blocked: boolean;
30
+ pending_reason: string | null;
31
+ redacted: boolean;
32
+ redaction_categories: string[];
33
+ };
34
+ export declare function safeTestSummary(taskId: string): {
35
+ task_id: string;
36
+ status: string;
37
+ command_count: number;
38
+ passed_commands: number;
39
+ failed_commands: number;
40
+ skipped_commands: number;
41
+ commands: {
42
+ command: string;
43
+ status: string;
44
+ exit_code: any;
45
+ duration_ms: number;
46
+ }[];
47
+ headline: string;
48
+ };
49
+ export declare function safeDiffSummary(taskId: string, options?: SafeViewOptions): {
50
+ task_id: string;
51
+ changed_files_total: number;
52
+ additions: number;
53
+ deletions: number;
54
+ diff_available: boolean;
55
+ diff_truncated: boolean;
56
+ patch_mode: any;
57
+ files: {
58
+ path: string;
59
+ old_path: string | null;
60
+ change: "added" | "modified" | "deleted" | "renamed";
61
+ kind: "source" | "build_artifact" | "runtime_generated";
62
+ tracked: boolean;
63
+ ignored: boolean;
64
+ }[];
65
+ artifact_hygiene_counts: Record<string, any>;
66
+ large_diff_omitted: boolean;
67
+ truncated: boolean;
68
+ };
69
+ export declare function safeAudit(taskId: string, options?: SafeViewOptions): {
70
+ task_id: string;
71
+ verdict: "pass" | "warn" | "fail";
72
+ acceptance: {
73
+ verdict: string;
74
+ status: string;
75
+ reason: string;
76
+ fail_checks: string[];
77
+ warn_checks: string[];
78
+ next_suggested_task: string;
79
+ };
80
+ check_counts: {
81
+ pass: number;
82
+ warn: number;
83
+ fail: number;
84
+ };
85
+ checks: {
86
+ name: string;
87
+ result: "pass" | "warn" | "fail";
88
+ detail: string;
89
+ }[];
90
+ fail_checks: {
91
+ name: string;
92
+ result: "pass" | "warn" | "fail";
93
+ detail: string;
94
+ }[];
95
+ warn_checks: {
96
+ name: string;
97
+ result: "pass" | "warn" | "fail";
98
+ detail: string;
99
+ }[];
100
+ possible_false_positives: {
101
+ check: string;
102
+ reason: string;
103
+ }[];
104
+ manual_verification_required: boolean;
105
+ manual_verification_items: string[];
106
+ recommended_next_actions: string[];
107
+ };
108
+ export declare function safeDirectSummary(sessionId: string, options?: SafeViewOptions): {
109
+ view: "summary" | "finalize";
110
+ session_id: string;
111
+ title: string;
112
+ repo_path: string;
113
+ created_at: string;
114
+ expires_at: string;
115
+ finalized: boolean;
116
+ finalized_at: string | null;
117
+ audited: boolean;
118
+ changed_files_total: number;
119
+ files: {
120
+ path: string;
121
+ old_path: string | null;
122
+ change: "added" | "modified" | "deleted" | "renamed";
123
+ kind: "source" | "build_artifact" | "runtime_generated";
124
+ tracked: boolean;
125
+ ignored: boolean;
126
+ }[];
127
+ artifact_hygiene_counts: {};
128
+ source_changes: {
129
+ path: string;
130
+ change: "added" | "modified" | "deleted" | "renamed";
131
+ kind: "source" | "build_artifact" | "runtime_generated";
132
+ tracked: boolean;
133
+ ignored: boolean;
134
+ reason: string;
135
+ }[];
136
+ tracked_build_artifacts: {
137
+ path: string;
138
+ change: "added" | "modified" | "deleted" | "renamed";
139
+ kind: "source" | "build_artifact" | "runtime_generated";
140
+ tracked: boolean;
141
+ ignored: boolean;
142
+ reason: string;
143
+ }[];
144
+ runtime_generated_files: {
145
+ path: string;
146
+ change: "added" | "modified" | "deleted" | "renamed";
147
+ kind: "source" | "build_artifact" | "runtime_generated";
148
+ tracked: boolean;
149
+ ignored: boolean;
150
+ reason: string;
151
+ }[];
152
+ suspicious_changes: {
153
+ path: string;
154
+ change: "added" | "modified" | "deleted" | "renamed";
155
+ kind: "source" | "build_artifact" | "runtime_generated";
156
+ tracked: boolean;
157
+ ignored: boolean;
158
+ reason: string;
159
+ }[];
160
+ verification: {
161
+ command: string;
162
+ exit_code: number | null;
163
+ passed: boolean;
164
+ timed_out: boolean;
165
+ started_at: string;
166
+ finished_at: string;
167
+ }[];
168
+ large_diff_omitted: boolean;
169
+ truncated: boolean;
170
+ };
171
+ export declare function safeFinalizeDirectSession(sessionId: string, options?: SafeViewOptions): {
172
+ finalized: boolean;
173
+ next_action: string;
174
+ view: "summary" | "finalize";
175
+ session_id: string;
176
+ title: string;
177
+ repo_path: string;
178
+ created_at: string;
179
+ expires_at: string;
180
+ finalized_at: string | null;
181
+ audited: boolean;
182
+ changed_files_total: number;
183
+ files: {
184
+ path: string;
185
+ old_path: string | null;
186
+ change: "added" | "modified" | "deleted" | "renamed";
187
+ kind: "source" | "build_artifact" | "runtime_generated";
188
+ tracked: boolean;
189
+ ignored: boolean;
190
+ }[];
191
+ artifact_hygiene_counts: {};
192
+ source_changes: {
193
+ path: string;
194
+ change: "added" | "modified" | "deleted" | "renamed";
195
+ kind: "source" | "build_artifact" | "runtime_generated";
196
+ tracked: boolean;
197
+ ignored: boolean;
198
+ reason: string;
199
+ }[];
200
+ tracked_build_artifacts: {
201
+ path: string;
202
+ change: "added" | "modified" | "deleted" | "renamed";
203
+ kind: "source" | "build_artifact" | "runtime_generated";
204
+ tracked: boolean;
205
+ ignored: boolean;
206
+ reason: string;
207
+ }[];
208
+ runtime_generated_files: {
209
+ path: string;
210
+ change: "added" | "modified" | "deleted" | "renamed";
211
+ kind: "source" | "build_artifact" | "runtime_generated";
212
+ tracked: boolean;
213
+ ignored: boolean;
214
+ reason: string;
215
+ }[];
216
+ suspicious_changes: {
217
+ path: string;
218
+ change: "added" | "modified" | "deleted" | "renamed";
219
+ kind: "source" | "build_artifact" | "runtime_generated";
220
+ tracked: boolean;
221
+ ignored: boolean;
222
+ reason: string;
223
+ }[];
224
+ verification: {
225
+ command: string;
226
+ exit_code: number | null;
227
+ passed: boolean;
228
+ timed_out: boolean;
229
+ started_at: string;
230
+ finished_at: string;
231
+ }[];
232
+ large_diff_omitted: boolean;
233
+ truncated: boolean;
234
+ };
235
+ export declare function safeAuditDirectSession(sessionId: string, options?: SafeViewOptions): {
236
+ session_id: string;
237
+ decision: "pass" | "warn" | "fail";
238
+ reason_codes: string[];
239
+ blocking_findings: string[];
240
+ warnings: string[];
241
+ evidence: {
242
+ changed_files_total: number;
243
+ verification_runs: {
244
+ command: string;
245
+ exit_code: number | null;
246
+ passed: boolean;
247
+ timed_out: boolean;
248
+ started_at: string;
249
+ finished_at: string;
250
+ }[];
251
+ diff_available: boolean;
252
+ summary_available: boolean;
253
+ audit_available: boolean;
254
+ };
255
+ next_action: string;
256
+ };
@@ -0,0 +1,250 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { getConfig, getTasksDir } from "../config.js";
4
+ import { auditSession } from "./auditSession.js";
5
+ import { auditTask } from "./auditTask.js";
6
+ import { finalizeDirectSession } from "./finalizeDirectSession.js";
7
+ import { getTaskSummary } from "./getTaskSummary.js";
8
+ import { guardReadPath } from "../security/pathGuard.js";
9
+ import { redactSensitiveValue } from "../security/contentRedaction.js";
10
+ import { readDirectSession } from "../direct/directSessionStore.js";
11
+ export function safeResult(taskId, options = {}) {
12
+ const maxItems = normalizeMaxItems(options.max_items);
13
+ const summary = getTaskSummary(taskId, { view: "compact", max_items: maxItems });
14
+ return redact({
15
+ task_id: summary.task_id,
16
+ status: summary.status,
17
+ terminal: summary.terminal,
18
+ acceptance_status: summary.acceptance_status,
19
+ phase: summary.phase,
20
+ repo_path: summary.repo_path,
21
+ changed_files_total: summary.changed_files_total,
22
+ out_of_scope_changes_total: summary.out_of_scope_changes_total,
23
+ artifact_hygiene_counts: summary.artifact_hygiene.counts,
24
+ release_artifacts_count: summary.release_artifacts_count,
25
+ artifact_status: summary.artifact_status,
26
+ verification: summary.verification_summary,
27
+ warnings: limitStrings(summary.warnings, maxItems),
28
+ errors: limitStrings(summary.errors, maxItems),
29
+ failure_reason: summary.failure_reason,
30
+ failed_command: summary.failed_command,
31
+ next_action: summary.suggested_next_action,
32
+ execution_blocked: summary.execution_blocked,
33
+ pending_reason: summary.pending_reason,
34
+ redacted: summary.redacted,
35
+ redaction_categories: summary.redaction_categories,
36
+ });
37
+ }
38
+ export function safeTestSummary(taskId) {
39
+ const { taskDir, config } = getTaskDir(taskId);
40
+ const verify = readJson(join(taskDir, "verify.json"), config);
41
+ const summary = getTaskSummary(taskId, { view: "compact", max_items: 8 });
42
+ const commands = asArray(verify.commands).map((entry) => ({
43
+ command: String(entry.command || ""),
44
+ status: String(entry.status || "unknown"),
45
+ exit_code: entry.exit_code ?? null,
46
+ duration_ms: Number(entry.duration_ms || 0),
47
+ }));
48
+ return redact({
49
+ task_id: taskId,
50
+ status: String(verify.status || summary.verification_summary.status || "not_available"),
51
+ command_count: commands.length,
52
+ passed_commands: commands.filter((entry) => entry.status === "passed").length,
53
+ failed_commands: commands.filter((entry) => ["failed", "timed_out", "canceled"].includes(entry.status)).length,
54
+ skipped_commands: commands.filter((entry) => entry.status === "skipped").length,
55
+ commands,
56
+ headline: summary.verification_summary.headline,
57
+ });
58
+ }
59
+ export function safeDiffSummary(taskId, options = {}) {
60
+ const maxItems = normalizeMaxItems(options.max_items);
61
+ const { taskDir, config } = getTaskDir(taskId);
62
+ const changes = readJson(join(taskDir, "changed-files.json"), config);
63
+ const changedFiles = asArray(changes.changed_files);
64
+ const hygiene = asRecord(changes.artifact_hygiene);
65
+ return redact({
66
+ task_id: taskId,
67
+ changed_files_total: changedFiles.length,
68
+ additions: Number(changes.additions || 0),
69
+ deletions: Number(changes.deletions || 0),
70
+ diff_available: Boolean(changes.diff_available),
71
+ diff_truncated: Boolean(changes.diff_truncated),
72
+ patch_mode: changes.patch_mode || null,
73
+ files: changedFiles.slice(0, maxItems).map((file) => ({
74
+ path: file.path,
75
+ old_path: file.old_path || null,
76
+ change: file.change,
77
+ kind: file.kind,
78
+ tracked: Boolean(file.tracked),
79
+ ignored: Boolean(file.ignored),
80
+ })),
81
+ artifact_hygiene_counts: asRecord(hygiene.counts),
82
+ large_diff_omitted: true,
83
+ truncated: changedFiles.length > maxItems,
84
+ });
85
+ }
86
+ export function safeAudit(taskId, options = {}) {
87
+ const maxItems = normalizeMaxItems(options.max_items);
88
+ return auditToSafe(auditTask(taskId), maxItems);
89
+ }
90
+ export function safeDirectSummary(sessionId, options = {}) {
91
+ const maxItems = normalizeMaxItems(options.max_items);
92
+ const session = readDirectSession(sessionId);
93
+ return directSessionToSafe(session, maxItems, "summary");
94
+ }
95
+ export function safeFinalizeDirectSession(sessionId, options = {}) {
96
+ const maxItems = normalizeMaxItems(options.max_items);
97
+ const finalized = finalizeDirectSession({ session_id: sessionId });
98
+ const session = readDirectSession(sessionId);
99
+ return redact({
100
+ ...directSessionToSafe(session, maxItems, "finalize"),
101
+ finalized: finalized.finalized,
102
+ next_action: finalized.next_action,
103
+ });
104
+ }
105
+ export function safeAuditDirectSession(sessionId, options = {}) {
106
+ const maxItems = normalizeMaxItems(options.max_items);
107
+ const audit = auditSession({ session_id: sessionId });
108
+ return redact({
109
+ session_id: audit.session_id,
110
+ decision: audit.decision,
111
+ reason_codes: audit.reason_codes.slice(0, maxItems),
112
+ blocking_findings: limitStrings(audit.blocking_findings, maxItems),
113
+ warnings: limitStrings(audit.warnings, maxItems),
114
+ evidence: {
115
+ changed_files_total: audit.evidence.changed_files_total,
116
+ verification_runs: summarizeVerificationRuns(audit.evidence.verification_runs),
117
+ diff_available: Boolean(audit.evidence.diff_path),
118
+ summary_available: Boolean(audit.evidence.summary_path),
119
+ audit_available: Boolean(audit.evidence.audit_path),
120
+ },
121
+ next_action: audit.next_action,
122
+ });
123
+ }
124
+ function auditToSafe(audit, maxItems) {
125
+ const checks = audit.checks.map((check) => ({
126
+ name: check.name,
127
+ result: check.result,
128
+ detail: truncate(check.detail, 240),
129
+ }));
130
+ return redact({
131
+ task_id: audit.task_id,
132
+ verdict: audit.verdict,
133
+ acceptance: {
134
+ verdict: audit.acceptance.verdict,
135
+ status: audit.acceptance.status,
136
+ reason: truncate(audit.acceptance.reason, 240),
137
+ fail_checks: audit.acceptance.fail_checks.slice(0, maxItems).map((check) => check.name),
138
+ warn_checks: audit.acceptance.warn_checks.slice(0, maxItems).map((check) => check.name),
139
+ next_suggested_task: truncate(audit.acceptance.next_suggested_task, 240),
140
+ },
141
+ check_counts: {
142
+ pass: checks.filter((check) => check.result === "pass").length,
143
+ warn: checks.filter((check) => check.result === "warn").length,
144
+ fail: checks.filter((check) => check.result === "fail").length,
145
+ },
146
+ checks: checks.slice(0, maxItems),
147
+ fail_checks: checks.filter((check) => check.result === "fail").slice(0, maxItems),
148
+ warn_checks: checks.filter((check) => check.result === "warn").slice(0, maxItems),
149
+ possible_false_positives: audit.possible_false_positives.slice(0, maxItems).map((item) => ({
150
+ check: item.check,
151
+ reason: truncate(item.reason, 240),
152
+ })),
153
+ manual_verification_required: audit.manual_verification_required,
154
+ manual_verification_items: limitStrings(audit.manual_verification_items, maxItems),
155
+ recommended_next_actions: limitStrings(audit.recommended_next_actions, maxItems),
156
+ });
157
+ }
158
+ function directSessionToSafe(session, maxItems, view) {
159
+ const artifacts = session.change_artifacts;
160
+ const changedFiles = artifacts?.changed_files || [];
161
+ return redact({
162
+ view,
163
+ session_id: session.session_id,
164
+ title: session.title || "",
165
+ repo_path: session.repo_path,
166
+ created_at: session.created_at,
167
+ expires_at: session.expires_at,
168
+ finalized: session.finalized,
169
+ finalized_at: session.finalized_at,
170
+ audited: session.audited,
171
+ changed_files_total: changedFiles.length,
172
+ files: changedFiles.slice(0, maxItems).map((file) => ({
173
+ path: file.path,
174
+ old_path: file.old_path || null,
175
+ change: file.change,
176
+ kind: file.kind,
177
+ tracked: Boolean(file.tracked),
178
+ ignored: Boolean(file.ignored),
179
+ })),
180
+ artifact_hygiene_counts: artifacts?.artifact_hygiene.counts || {},
181
+ source_changes: limitClassified(artifacts?.artifact_hygiene.source_changes, maxItems),
182
+ tracked_build_artifacts: limitClassified(artifacts?.artifact_hygiene.tracked_build_artifacts, maxItems),
183
+ runtime_generated_files: limitClassified(artifacts?.artifact_hygiene.runtime_generated_files, maxItems),
184
+ suspicious_changes: limitClassified(artifacts?.artifact_hygiene.suspicious_changes, maxItems),
185
+ verification: summarizeVerificationRuns(session.verification_runs),
186
+ large_diff_omitted: true,
187
+ truncated: changedFiles.length > maxItems,
188
+ });
189
+ }
190
+ function summarizeVerificationRuns(runs) {
191
+ return runs.map((run) => ({
192
+ command: run.command,
193
+ exit_code: run.exit_code,
194
+ passed: run.passed,
195
+ timed_out: run.timed_out,
196
+ started_at: run.started_at,
197
+ finished_at: run.finished_at,
198
+ }));
199
+ }
200
+ function limitClassified(value, maxItems) {
201
+ return (value || []).slice(0, maxItems).map((entry) => ({
202
+ path: entry.path,
203
+ change: entry.change,
204
+ kind: entry.kind,
205
+ tracked: entry.tracked,
206
+ ignored: entry.ignored,
207
+ reason: truncate(entry.reason, 160),
208
+ }));
209
+ }
210
+ function getTaskDir(taskId) {
211
+ const config = getConfig();
212
+ const taskDir = resolve(getTasksDir(config), taskId);
213
+ guardReadPath(join(taskDir, "status.json"), config.workspaceRoot, config.tasksDir);
214
+ return { config, taskDir };
215
+ }
216
+ function readJson(path, config) {
217
+ guardReadPath(path, config.workspaceRoot, config.tasksDir);
218
+ if (!existsSync(path))
219
+ return {};
220
+ try {
221
+ const raw = readFileSync(path, "utf-8").replace(/^\uFEFF/, "");
222
+ return asRecord(JSON.parse(raw));
223
+ }
224
+ catch {
225
+ return {};
226
+ }
227
+ }
228
+ function normalizeMaxItems(value) {
229
+ if (value === undefined)
230
+ return 8;
231
+ if (!Number.isInteger(value) || value < 1 || value > 50) {
232
+ throw new Error("max_items must be an integer from 1 to 50.");
233
+ }
234
+ return value;
235
+ }
236
+ function asArray(value) {
237
+ return Array.isArray(value) ? value : [];
238
+ }
239
+ function asRecord(value) {
240
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
241
+ }
242
+ function limitStrings(values, maxItems) {
243
+ return values.slice(0, maxItems).map((value) => truncate(value, 240));
244
+ }
245
+ function truncate(value, maxChars) {
246
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
247
+ }
248
+ function redact(value) {
249
+ return redactSensitiveValue(value).value;
250
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * v0.9.0: Schema 漂移检测模块。
3
+ *
4
+ * 检测 registry 元数据与实际工具 schema 是否漂移,
5
+ * 并聚合为可被 doctor 自检消费的 SchemaDriftResult。
6
+ *
7
+ * 本模块只做检测和报告,不做运行时阻断。
8
+ */
9
+ import type { PatchWardenToolMeta } from "./toolRegistry.js";
10
+ export interface SchemaDriftResult {
11
+ /** true 表示无 drift,false 表示有 drift */
12
+ ok: boolean;
13
+ /** drift 描述列表(每个 drift 一条警告消息) */
14
+ warnings: string[];
15
+ }
16
+ /** toolDefs Map 的值类型:至少包含 inputSchema。 */
17
+ export interface ToolDefLike {
18
+ inputSchema: unknown;
19
+ }
20
+ /**
21
+ * 检查 registry 中记录的 inputSchemaDigest 与实际 toolDef 的 inputSchema digest 是否一致。
22
+ * toolDefs 中找不到的工具会被跳过(不报 drift)。
23
+ */
24
+ export declare function checkRegistrySchemaDigest(registry: PatchWardenToolMeta[], toolDefs: Map<string, ToolDefLike>): SchemaDriftResult;
25
+ /**
26
+ * 校验 chatgpt_core profile 的工具 manifest 是否稳定。
27
+ *
28
+ * toolCatalog.ts 未导出独立的 manifest hash 计算函数(stableJson 为私有),
29
+ * 因此采用 fallback 方案:校验 CHATGPT_CORE_TOOL_NAMES 中的工具名
30
+ * 全部存在于 toolDefs 中(集合比较,不比较顺序——Map 迭代顺序取决于构造
31
+ * 方式,不一定与 chatgpt_core profile 顺序一致)。
32
+ *
33
+ * 真正的顺序稳定性由 CHATGPT_CORE_TOOL_NAMES 常量本身保证(顺序固定),
34
+ * 如果常量被修改,这里的名称集合比较也会检测到变化。
35
+ */
36
+ export declare function checkChatgptCoreManifestStable(toolDefs: Map<string, ToolDefLike>): SchemaDriftResult;
37
+ /**
38
+ * 校验 chatgpt_core profile 的工具数仍与 CHATGPT_CORE_TOOL_NAMES 一致。
39
+ * 用于检测是否有新工具被意外追加到 chatgpt_core profile。
40
+ */
41
+ export declare function checkNewToolsProfileAppend(registry: PatchWardenToolMeta[]): SchemaDriftResult;
42
+ /**
43
+ * 聚合执行所有 schema drift 检查,合并 warnings。
44
+ * ok = warnings.length === 0
45
+ */
46
+ export declare function runAllSchemaDriftChecks(registry: PatchWardenToolMeta[], toolDefs: Map<string, ToolDefLike>): SchemaDriftResult;