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,450 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { join, relative, resolve, sep, isAbsolute } from "node:path";
4
+ import { getConfig } from "../config.js";
5
+ import {
6
+ checkCiVerified,
7
+ checkGitHubReleaseVerified,
8
+ checkPublishedVerified,
9
+ runReleaseGateCheck,
10
+ type ReleaseStage,
11
+ type ReleaseStageStatus,
12
+ } from "../release/releaseGate.js";
13
+ import { guardTestCommand } from "../security/commandGuard.js";
14
+ import { guardWorkspacePath } from "../security/pathGuard.js";
15
+ import {
16
+ commandAllowedByProjectPolicy,
17
+ getProjectPolicySummary,
18
+ isProtectedByProjectPolicy,
19
+ readPackageJson,
20
+ resolveVersionFromPolicy,
21
+ type ProjectPolicySummary,
22
+ } from "../policy/projectPolicy.js";
23
+
24
+ export interface ReleaseCheckInput {
25
+ repo_path: string;
26
+ target_stage?: ReleaseStage;
27
+ package_name?: string;
28
+ version?: string;
29
+ github_repo?: string;
30
+ branch?: string;
31
+ }
32
+
33
+ export interface ReleasePrepareInput {
34
+ repo_path: string;
35
+ required_commands?: string[];
36
+ timeout_seconds?: number;
37
+ }
38
+
39
+ export interface ReleaseVerifyInput {
40
+ repo_path: string;
41
+ package_name?: string;
42
+ version?: string;
43
+ github_repo?: string;
44
+ branch?: string;
45
+ }
46
+
47
+ export interface ReleaseCleanupInput {
48
+ repo_path: string;
49
+ dry_run?: boolean;
50
+ patterns?: string[];
51
+ }
52
+
53
+ export interface ReleaseCommandResult {
54
+ command: string;
55
+ status: "passed" | "failed" | "blocked";
56
+ exit_code: number | null;
57
+ reason: string | null;
58
+ }
59
+
60
+ export interface ReleaseModeResult {
61
+ ok: boolean;
62
+ mode: "release_check" | "release_prepare" | "release_verify" | "release_cleanup";
63
+ repo_path: string;
64
+ resolved_repo_path: string;
65
+ policy: {
66
+ valid: boolean;
67
+ issue_count: number;
68
+ issues: ProjectPolicySummary["issues"];
69
+ };
70
+ summary: Record<string, unknown>;
71
+ }
72
+
73
+ const LOCAL_TIMEOUT_MS = 300000;
74
+ const CLEANUP_REPORT_DIR = ".patchwarden/release-cleanup";
75
+
76
+ export function getProjectPolicyTool(repoPath: string): ProjectPolicySummary {
77
+ return getProjectPolicySummary(repoPath);
78
+ }
79
+
80
+ export async function releaseCheck(input: ReleaseCheckInput): Promise<ReleaseModeResult> {
81
+ const config = getConfig();
82
+ const repoPath = guardWorkspacePath(input.repo_path, config.workspaceRoot);
83
+ const policy = getProjectPolicySummary(input.repo_path);
84
+ const packageJson = readPackageJson(repoPath);
85
+ const version = input.version || safeResolveVersion(repoPath, policy) || packageJson.version || undefined;
86
+ const packageName = input.package_name || packageJson.name || undefined;
87
+ const githubRepo = input.github_repo || packageJson.githubRepo || undefined;
88
+ const branch = input.branch || "main";
89
+ const targetStage = input.target_stage || "local_ready";
90
+ const result = await runReleaseGateCheck(repoPath, targetStage, {
91
+ packageName,
92
+ version,
93
+ githubRepo,
94
+ branch,
95
+ });
96
+ return {
97
+ ok: !result.blocked_reason,
98
+ mode: "release_check",
99
+ repo_path: input.repo_path,
100
+ resolved_repo_path: repoPath,
101
+ policy: summarizePolicy(policy),
102
+ summary: {
103
+ target_stage: targetStage,
104
+ stages: result.stages,
105
+ blocked_reason: result.blocked_reason || null,
106
+ package_name: packageName || null,
107
+ version: version || null,
108
+ github_repo: githubRepo || null,
109
+ branch,
110
+ },
111
+ };
112
+ }
113
+
114
+ export function releasePrepare(input: ReleasePrepareInput): ReleaseModeResult {
115
+ const config = getConfig();
116
+ const repoPath = guardWorkspacePath(input.repo_path, config.workspaceRoot);
117
+ const policy = getProjectPolicySummary(input.repo_path);
118
+ const commands = (input.required_commands && input.required_commands.length > 0)
119
+ ? input.required_commands.map((command) => command.trim()).filter(Boolean)
120
+ : policy.effective_policy.release_mode.required_commands;
121
+ const timeoutMs = Math.min(
122
+ Math.max(Number(input.timeout_seconds || 300) * 1000, 1000),
123
+ LOCAL_TIMEOUT_MS,
124
+ );
125
+ const results: ReleaseCommandResult[] = [];
126
+
127
+ if (policy.release_readiness.version_consistent === false) {
128
+ return buildPrepareResult(input.repo_path, repoPath, policy, results, "version_mismatch");
129
+ }
130
+
131
+ for (const command of commands) {
132
+ try {
133
+ guardTestCommand(command, config, repoPath);
134
+ } catch (err) {
135
+ results.push({
136
+ command,
137
+ status: "blocked",
138
+ exit_code: null,
139
+ reason: `not_allowlisted: ${err instanceof Error ? err.message : String(err)}`,
140
+ });
141
+ continue;
142
+ }
143
+ const projectAllowed = commandAllowedByProjectPolicy(command, policy);
144
+ if (!projectAllowed.allowed) {
145
+ results.push({ command, status: "blocked", exit_code: null, reason: projectAllowed.reason });
146
+ continue;
147
+ }
148
+ try {
149
+ execSync(command, {
150
+ cwd: repoPath,
151
+ encoding: "utf-8",
152
+ timeout: timeoutMs,
153
+ stdio: ["ignore", "pipe", "pipe"],
154
+ });
155
+ results.push({ command, status: "passed", exit_code: 0, reason: null });
156
+ } catch (err) {
157
+ const code = err && typeof err === "object" && "status" in err
158
+ ? Number((err as { status: unknown }).status)
159
+ : null;
160
+ results.push({
161
+ command,
162
+ status: "failed",
163
+ exit_code: Number.isFinite(code) ? code : null,
164
+ reason: err instanceof Error ? err.message.split(/\r?\n/)[0] : String(err),
165
+ });
166
+ }
167
+ }
168
+
169
+ return buildPrepareResult(input.repo_path, repoPath, policy, results, null);
170
+ }
171
+
172
+ export async function releaseVerify(input: ReleaseVerifyInput): Promise<ReleaseModeResult> {
173
+ const config = getConfig();
174
+ const repoPath = guardWorkspacePath(input.repo_path, config.workspaceRoot);
175
+ const policy = getProjectPolicySummary(input.repo_path);
176
+ const packageJson = readPackageJson(repoPath);
177
+ const version = input.version || safeResolveVersion(repoPath, policy) || packageJson.version || "";
178
+ const packageName = input.package_name || packageJson.name || "";
179
+ const githubRepo = input.github_repo || packageJson.githubRepo || "";
180
+ const branch = input.branch || "main";
181
+ const stages: Record<"published_verified" | "github_release_verified" | "ci_verified", ReleaseStageStatus> = {
182
+ published_verified: "not_checked",
183
+ github_release_verified: "not_checked",
184
+ ci_verified: "not_checked",
185
+ };
186
+ const reasons: Record<string, string | null> = {};
187
+
188
+ if (!packageName || !version) {
189
+ stages.published_verified = "failed";
190
+ reasons.published_verified = "package_name and version are required";
191
+ } else {
192
+ const published = await checkPublishedVerified(packageName, version);
193
+ stages.published_verified = published.status;
194
+ reasons.published_verified = published.reason || null;
195
+ }
196
+
197
+ if (!githubRepo || !version) {
198
+ stages.github_release_verified = "failed";
199
+ reasons.github_release_verified = "github_repo and version are required";
200
+ } else {
201
+ const github = await checkGitHubReleaseVerified(githubRepo, `v${version}`);
202
+ stages.github_release_verified = github.status;
203
+ reasons.github_release_verified = github.reason || null;
204
+ }
205
+
206
+ if (!githubRepo || !branch) {
207
+ stages.ci_verified = "failed";
208
+ reasons.ci_verified = "github_repo and branch are required";
209
+ } else {
210
+ const ci = await checkCiVerified(githubRepo, branch);
211
+ stages.ci_verified = ci.status;
212
+ reasons.ci_verified = ci.reason || null;
213
+ }
214
+
215
+ return {
216
+ ok: Object.values(stages).every((status) => status === "passed" || status === "not_checked"),
217
+ mode: "release_verify",
218
+ repo_path: input.repo_path,
219
+ resolved_repo_path: repoPath,
220
+ policy: summarizePolicy(policy),
221
+ summary: {
222
+ stages,
223
+ reasons,
224
+ package_name: packageName || null,
225
+ version: version || null,
226
+ github_repo: githubRepo || null,
227
+ branch,
228
+ remote_write_performed: false,
229
+ },
230
+ };
231
+ }
232
+
233
+ export function releaseCleanup(input: ReleaseCleanupInput): ReleaseModeResult {
234
+ const config = getConfig();
235
+ const repoPath = guardWorkspacePath(input.repo_path, config.workspaceRoot);
236
+ const policy = getProjectPolicySummary(input.repo_path);
237
+ const dryRun = input.dry_run !== false;
238
+ const cleanupEnabled = policy.effective_policy.auto_cleanup.enabled !== false;
239
+ const patterns = cleanupEnabled && input.patterns && input.patterns.length > 0
240
+ ? input.patterns.map((pattern) => pattern.trim()).filter(Boolean)
241
+ : cleanupEnabled
242
+ ? policy.effective_policy.auto_cleanup.patterns
243
+ : [];
244
+ const candidates = collectCleanupCandidates(repoPath, patterns);
245
+ const removed: string[] = [];
246
+ const skipped: Array<{ path: string; reason: string }> = [];
247
+
248
+ for (const candidate of candidates) {
249
+ if (isProtectedByProjectPolicy(candidate.relPath, policy.effective_policy)) {
250
+ skipped.push({ path: candidate.relPath, reason: "protected_or_sensitive" });
251
+ continue;
252
+ }
253
+ if (policy.effective_policy.auto_cleanup.exclude.some((pattern) => matchesPattern(candidate.relPath, pattern))) {
254
+ skipped.push({ path: candidate.relPath, reason: "excluded_by_policy" });
255
+ continue;
256
+ }
257
+ if (isTrackedByGit(repoPath, candidate.relPath)) {
258
+ skipped.push({ path: candidate.relPath, reason: "tracked_by_git" });
259
+ continue;
260
+ }
261
+ if (!isIgnoredOrUntracked(repoPath, candidate.relPath)) {
262
+ skipped.push({ path: candidate.relPath, reason: "not_ignored_or_untracked" });
263
+ continue;
264
+ }
265
+ if (!dryRun) {
266
+ rmSync(candidate.path, { recursive: true, force: true });
267
+ }
268
+ removed.push(candidate.relPath);
269
+ }
270
+
271
+ const report = {
272
+ generated_at: new Date().toISOString(),
273
+ dry_run: dryRun,
274
+ patterns,
275
+ cleanup_disabled_by_policy: !cleanupEnabled,
276
+ removed,
277
+ skipped,
278
+ candidate_count: candidates.length,
279
+ };
280
+ const reportDir = join(repoPath, CLEANUP_REPORT_DIR);
281
+ mkdirSync(reportDir, { recursive: true });
282
+ const reportPath = join(reportDir, `${report.generated_at.replace(/[-:.TZ]/g, "").slice(0, 14)}.json`);
283
+ writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n", "utf-8");
284
+
285
+ return {
286
+ ok: skipped.length === 0 || dryRun,
287
+ mode: "release_cleanup",
288
+ repo_path: input.repo_path,
289
+ resolved_repo_path: repoPath,
290
+ policy: summarizePolicy(policy),
291
+ summary: {
292
+ ...report,
293
+ report_path: relative(repoPath, reportPath).replace(/\\/g, "/"),
294
+ },
295
+ };
296
+ }
297
+
298
+ function buildPrepareResult(
299
+ repoPathInput: string,
300
+ repoPath: string,
301
+ policy: ProjectPolicySummary,
302
+ commands: ReleaseCommandResult[],
303
+ blockedReason: string | null,
304
+ ): ReleaseModeResult {
305
+ const blocked = commands.some((entry) => entry.status === "blocked");
306
+ const failed = commands.some((entry) => entry.status === "failed");
307
+ return {
308
+ ok: !blockedReason && !blocked && !failed,
309
+ mode: "release_prepare",
310
+ repo_path: repoPathInput,
311
+ resolved_repo_path: repoPath,
312
+ policy: summarizePolicy(policy),
313
+ summary: {
314
+ blocked_reason: blockedReason,
315
+ version_source: policy.release_readiness.version_source,
316
+ version: policy.release_readiness.version,
317
+ package_json_version: policy.release_readiness.package_json_version,
318
+ version_consistent: policy.release_readiness.version_consistent,
319
+ commands,
320
+ remote_write_performed: false,
321
+ },
322
+ };
323
+ }
324
+
325
+ function summarizePolicy(policy: ProjectPolicySummary): ReleaseModeResult["policy"] {
326
+ return {
327
+ valid: policy.valid,
328
+ issue_count: policy.issues.length,
329
+ issues: policy.issues.slice(0, 20),
330
+ };
331
+ }
332
+
333
+ function safeResolveVersion(repoPath: string, policy: ProjectPolicySummary): string | null {
334
+ try {
335
+ return resolveVersionFromPolicy(repoPath, policy.effective_policy);
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ function collectCleanupCandidates(repoPath: string, patterns: string[]): Array<{ path: string; relPath: string }> {
342
+ const candidates = new Map<string, string>();
343
+ for (const pattern of patterns) {
344
+ const normalized = normalizeRel(pattern);
345
+ if (!normalized || normalized.includes("..") || isAbsolute(normalized)) continue;
346
+ if (normalized.includes("*")) {
347
+ walk(repoPath, (fullPath, relPath) => {
348
+ if (matchesPattern(relPath, normalized)) candidates.set(relPath, fullPath);
349
+ });
350
+ continue;
351
+ }
352
+ const fullPath = resolve(repoPath, normalized);
353
+ const relPath = toRepoRelative(repoPath, fullPath);
354
+ if (relPath && existsSync(fullPath)) candidates.set(relPath, fullPath);
355
+ }
356
+ return [...candidates].map(([relPath, path]) => ({ path, relPath }));
357
+ }
358
+
359
+ function walk(root: string, visit: (path: string, relPath: string) => void): void {
360
+ walkFrom(root, root, visit);
361
+ }
362
+
363
+ function walkFrom(base: string, dir: string, visit: (path: string, relPath: string) => void): void {
364
+ let entries: import("node:fs").Dirent[];
365
+ try {
366
+ entries = readdirSync(dir, { withFileTypes: true });
367
+ } catch {
368
+ return;
369
+ }
370
+ for (const entry of entries) {
371
+ const fullPath = join(dir, entry.name);
372
+ const repoRel = toRepoRelative(base, fullPath);
373
+ if (!repoRel || repoRel.startsWith(".git/") || repoRel.startsWith("node_modules/")) continue;
374
+ visit(fullPath, repoRel);
375
+ if (entry.isDirectory()) walkFrom(base, fullPath, visit);
376
+ }
377
+ }
378
+
379
+ function isTrackedByGit(root: string, relPath: string): boolean {
380
+ if (!isGitWorktree(root)) return false;
381
+ try {
382
+ execFileSync("git", ["ls-files", "--error-unmatch", "--", relPath], {
383
+ cwd: root,
384
+ stdio: "ignore",
385
+ windowsHide: true,
386
+ });
387
+ return true;
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+
393
+ function isIgnoredOrUntracked(root: string, relPath: string): boolean {
394
+ if (!isGitWorktree(root)) return true;
395
+ try {
396
+ execFileSync("git", ["check-ignore", "-q", "--", relPath], {
397
+ cwd: root,
398
+ stdio: "ignore",
399
+ windowsHide: true,
400
+ });
401
+ return true;
402
+ } catch {
403
+ try {
404
+ const output = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "--", relPath], {
405
+ cwd: root,
406
+ encoding: "utf-8",
407
+ windowsHide: true,
408
+ });
409
+ return output.trim().split(/\r?\n/).filter(Boolean).includes(relPath);
410
+ } catch {
411
+ return false;
412
+ }
413
+ }
414
+ }
415
+
416
+ function isGitWorktree(root: string): boolean {
417
+ try {
418
+ return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
419
+ cwd: root,
420
+ encoding: "utf-8",
421
+ windowsHide: true,
422
+ }).trim() === "true";
423
+ } catch {
424
+ return false;
425
+ }
426
+ }
427
+
428
+ function toRepoRelative(root: string, target: string): string {
429
+ const rel = relative(resolve(root), resolve(target));
430
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) return "";
431
+ return rel.replace(/\\/g, "/");
432
+ }
433
+
434
+ function normalizeRel(value: string): string {
435
+ return value.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
436
+ }
437
+
438
+ function matchesPattern(relPath: string, pattern: string): boolean {
439
+ const normalized = normalizeRel(pattern);
440
+ if (!normalized) return false;
441
+ if (!normalized.includes("*")) {
442
+ return relPath.toLowerCase() === normalized.toLowerCase() ||
443
+ relPath.toLowerCase().startsWith(`${normalized.toLowerCase()}/`);
444
+ }
445
+ const escaped = normalized
446
+ .split("*")
447
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
448
+ .join("[^/]*");
449
+ return new RegExp(`^${escaped}$`, "i").test(relPath);
450
+ }
@@ -0,0 +1,98 @@
1
+ import { getConfig } from "../config.js";
2
+ import { runVerification } from "./runVerification.js";
3
+
4
+ export interface RunDirectVerificationBundleInput {
5
+ session_id: string;
6
+ commands: string[];
7
+ timeout_seconds?: number;
8
+ }
9
+
10
+ export interface RunDirectVerificationBundleOutput {
11
+ session_id: string;
12
+ status: "passed" | "failed";
13
+ command_count: number;
14
+ passed_commands: number;
15
+ failed_commands: number;
16
+ timed_out_commands: number;
17
+ commands: Array<{
18
+ command: string;
19
+ passed: boolean;
20
+ exit_code: number | null;
21
+ timed_out: boolean;
22
+ redacted: boolean;
23
+ redaction_categories: string[];
24
+ started_at: string;
25
+ finished_at: string;
26
+ }>;
27
+ large_logs_omitted: true;
28
+ next_action: string;
29
+ }
30
+
31
+ export async function runDirectVerificationBundle(
32
+ input: RunDirectVerificationBundleInput
33
+ ): Promise<RunDirectVerificationBundleOutput> {
34
+ const normalized = normalizeInput(input);
35
+ const results: RunDirectVerificationBundleOutput["commands"] = [];
36
+
37
+ for (const command of normalized.commands) {
38
+ const result = await runVerification({
39
+ session_id: normalized.session_id,
40
+ command,
41
+ timeout_seconds: normalized.timeout_seconds,
42
+ });
43
+ results.push({
44
+ command: result.command,
45
+ passed: result.passed,
46
+ exit_code: result.exit_code,
47
+ timed_out: result.timed_out,
48
+ redacted: result.redacted,
49
+ redaction_categories: result.redaction_categories,
50
+ started_at: result.started_at,
51
+ finished_at: result.finished_at,
52
+ });
53
+ }
54
+
55
+ const failedCommands = results.filter((entry) => !entry.passed).length;
56
+ const timedOutCommands = results.filter((entry) => entry.timed_out).length;
57
+ return {
58
+ session_id: normalized.session_id,
59
+ status: failedCommands === 0 ? "passed" : "failed",
60
+ command_count: results.length,
61
+ passed_commands: results.length - failedCommands,
62
+ failed_commands: failedCommands,
63
+ timed_out_commands: timedOutCommands,
64
+ commands: results,
65
+ large_logs_omitted: true,
66
+ next_action: failedCommands === 0
67
+ ? "Call safe_finalize_direct_session, then safe_audit_direct_session."
68
+ : "Review bounded verification status and create a normal follow-up task if fixes are needed.",
69
+ };
70
+ }
71
+
72
+ function normalizeInput(input: RunDirectVerificationBundleInput): Required<RunDirectVerificationBundleInput> {
73
+ const sessionId = String(input.session_id || "").trim();
74
+ if (!sessionId) throw new Error("session_id is required.");
75
+ if (!Array.isArray(input.commands) || input.commands.length === 0) {
76
+ throw new Error("commands must contain at least one command.");
77
+ }
78
+ if (input.commands.length > 20) {
79
+ throw new Error("commands may contain at most 20 commands.");
80
+ }
81
+ const commands = input.commands.map((command) => String(command).trim());
82
+ if (commands.some((command) => command === "")) {
83
+ throw new Error("commands must contain only non-empty strings.");
84
+ }
85
+
86
+ const config = getConfig();
87
+ const maxTimeout = Math.min(config.maxTaskTimeoutSeconds, config.directSessionTtlSeconds);
88
+ const timeoutSeconds = input.timeout_seconds ?? 120;
89
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > maxTimeout) {
90
+ throw new Error(`timeout_seconds must be an integer from 1 to ${maxTimeout}.`);
91
+ }
92
+
93
+ return {
94
+ session_id: sessionId,
95
+ commands,
96
+ timeout_seconds: timeoutSeconds,
97
+ };
98
+ }