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
@@ -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;