@mknightzzz/stw 0.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 (250) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -0
  3. package/dist/agentic-fallback.d.ts +3 -0
  4. package/dist/agentic-fallback.js +32 -0
  5. package/dist/agentic-fallback.js.map +1 -0
  6. package/dist/agentic-prompt.d.ts +2 -0
  7. package/dist/agentic-prompt.js +68 -0
  8. package/dist/agentic-prompt.js.map +1 -0
  9. package/dist/agentic-runtime.d.ts +48 -0
  10. package/dist/agentic-runtime.js +149 -0
  11. package/dist/agentic-runtime.js.map +1 -0
  12. package/dist/agentic-types.d.ts +37 -0
  13. package/dist/agentic-types.js +2 -0
  14. package/dist/agentic-types.js.map +1 -0
  15. package/dist/agents.d.ts +7 -0
  16. package/dist/agents.js +2 -0
  17. package/dist/agents.js.map +1 -0
  18. package/dist/assignments.d.ts +7 -0
  19. package/dist/assignments.js +125 -0
  20. package/dist/assignments.js.map +1 -0
  21. package/dist/checkpoint.d.ts +35 -0
  22. package/dist/checkpoint.js +78 -0
  23. package/dist/checkpoint.js.map +1 -0
  24. package/dist/circuit-breaker.d.ts +17 -0
  25. package/dist/circuit-breaker.js +65 -0
  26. package/dist/circuit-breaker.js.map +1 -0
  27. package/dist/claim.d.ts +6 -0
  28. package/dist/claim.js +135 -0
  29. package/dist/claim.js.map +1 -0
  30. package/dist/clarity-gate.d.ts +12 -0
  31. package/dist/clarity-gate.js +83 -0
  32. package/dist/clarity-gate.js.map +1 -0
  33. package/dist/cli.d.ts +2 -0
  34. package/dist/cli.js +38 -0
  35. package/dist/cli.js.map +1 -0
  36. package/dist/command-dispatch.d.ts +45 -0
  37. package/dist/command-dispatch.js +206 -0
  38. package/dist/command-dispatch.js.map +1 -0
  39. package/dist/command-parser.d.ts +11 -0
  40. package/dist/command-parser.js +101 -0
  41. package/dist/command-parser.js.map +1 -0
  42. package/dist/commands/clean.d.ts +10 -0
  43. package/dist/commands/clean.js +133 -0
  44. package/dist/commands/clean.js.map +1 -0
  45. package/dist/commands/execution.d.ts +2 -0
  46. package/dist/commands/execution.js +327 -0
  47. package/dist/commands/execution.js.map +1 -0
  48. package/dist/commands/go.d.ts +2 -0
  49. package/dist/commands/go.js +197 -0
  50. package/dist/commands/go.js.map +1 -0
  51. package/dist/commands/helpers.d.ts +44 -0
  52. package/dist/commands/helpers.js +231 -0
  53. package/dist/commands/helpers.js.map +1 -0
  54. package/dist/commands/idea.d.ts +2 -0
  55. package/dist/commands/idea.js +89 -0
  56. package/dist/commands/idea.js.map +1 -0
  57. package/dist/commands/init.d.ts +2 -0
  58. package/dist/commands/init.js +94 -0
  59. package/dist/commands/init.js.map +1 -0
  60. package/dist/commands/integration.d.ts +7 -0
  61. package/dist/commands/integration.js +139 -0
  62. package/dist/commands/integration.js.map +1 -0
  63. package/dist/commands/maintenance.d.ts +2 -0
  64. package/dist/commands/maintenance.js +301 -0
  65. package/dist/commands/maintenance.js.map +1 -0
  66. package/dist/commands/run.d.ts +2 -0
  67. package/dist/commands/run.js +356 -0
  68. package/dist/commands/run.js.map +1 -0
  69. package/dist/commands/setup.d.ts +2 -0
  70. package/dist/commands/setup.js +198 -0
  71. package/dist/commands/setup.js.map +1 -0
  72. package/dist/commands/spec.d.ts +2 -0
  73. package/dist/commands/spec.js +35 -0
  74. package/dist/commands/spec.js.map +1 -0
  75. package/dist/commands/stats.d.ts +2 -0
  76. package/dist/commands/stats.js +80 -0
  77. package/dist/commands/stats.js.map +1 -0
  78. package/dist/commands/task-ops.d.ts +2 -0
  79. package/dist/commands/task-ops.js +406 -0
  80. package/dist/commands/task-ops.js.map +1 -0
  81. package/dist/config.d.ts +18 -0
  82. package/dist/config.js +338 -0
  83. package/dist/config.js.map +1 -0
  84. package/dist/cost.d.ts +30 -0
  85. package/dist/cost.js +167 -0
  86. package/dist/cost.js.map +1 -0
  87. package/dist/crash-recovery.d.ts +9 -0
  88. package/dist/crash-recovery.js +42 -0
  89. package/dist/crash-recovery.js.map +1 -0
  90. package/dist/diagnostic.d.ts +48 -0
  91. package/dist/diagnostic.js +328 -0
  92. package/dist/diagnostic.js.map +1 -0
  93. package/dist/doctor.d.ts +31 -0
  94. package/dist/doctor.js +225 -0
  95. package/dist/doctor.js.map +1 -0
  96. package/dist/drift.d.ts +11 -0
  97. package/dist/drift.js +57 -0
  98. package/dist/drift.js.map +1 -0
  99. package/dist/git-utils.d.ts +20 -0
  100. package/dist/git-utils.js +206 -0
  101. package/dist/git-utils.js.map +1 -0
  102. package/dist/gitlab.d.ts +54 -0
  103. package/dist/gitlab.js +101 -0
  104. package/dist/gitlab.js.map +1 -0
  105. package/dist/idea.d.ts +35 -0
  106. package/dist/idea.js +251 -0
  107. package/dist/idea.js.map +1 -0
  108. package/dist/import-resolution.d.ts +13 -0
  109. package/dist/import-resolution.js +111 -0
  110. package/dist/import-resolution.js.map +1 -0
  111. package/dist/inbox-renderer.d.ts +2 -0
  112. package/dist/inbox-renderer.js +67 -0
  113. package/dist/inbox-renderer.js.map +1 -0
  114. package/dist/init.d.ts +105 -0
  115. package/dist/init.js +235 -0
  116. package/dist/init.js.map +1 -0
  117. package/dist/llm-reviewer.d.ts +14 -0
  118. package/dist/llm-reviewer.js +109 -0
  119. package/dist/llm-reviewer.js.map +1 -0
  120. package/dist/lock.d.ts +26 -0
  121. package/dist/lock.js +76 -0
  122. package/dist/lock.js.map +1 -0
  123. package/dist/logger.d.ts +24 -0
  124. package/dist/logger.js +40 -0
  125. package/dist/logger.js.map +1 -0
  126. package/dist/math-utils.d.ts +2 -0
  127. package/dist/math-utils.js +7 -0
  128. package/dist/math-utils.js.map +1 -0
  129. package/dist/mechanical-review.d.ts +30 -0
  130. package/dist/mechanical-review.js +76 -0
  131. package/dist/mechanical-review.js.map +1 -0
  132. package/dist/merge.d.ts +83 -0
  133. package/dist/merge.js +363 -0
  134. package/dist/merge.js.map +1 -0
  135. package/dist/parallel.d.ts +35 -0
  136. package/dist/parallel.js +214 -0
  137. package/dist/parallel.js.map +1 -0
  138. package/dist/plan-validation.d.ts +19 -0
  139. package/dist/plan-validation.js +253 -0
  140. package/dist/plan-validation.js.map +1 -0
  141. package/dist/planner-prompt.d.ts +33 -0
  142. package/dist/planner-prompt.js +244 -0
  143. package/dist/planner-prompt.js.map +1 -0
  144. package/dist/planner.d.ts +29 -0
  145. package/dist/planner.js +511 -0
  146. package/dist/planner.js.map +1 -0
  147. package/dist/poller.d.ts +34 -0
  148. package/dist/poller.js +91 -0
  149. package/dist/poller.js.map +1 -0
  150. package/dist/progress.d.ts +34 -0
  151. package/dist/progress.js +122 -0
  152. package/dist/progress.js.map +1 -0
  153. package/dist/prompt-builder.d.ts +51 -0
  154. package/dist/prompt-builder.js +481 -0
  155. package/dist/prompt-builder.js.map +1 -0
  156. package/dist/provider.d.ts +14 -0
  157. package/dist/provider.js +278 -0
  158. package/dist/provider.js.map +1 -0
  159. package/dist/question-handler.d.ts +18 -0
  160. package/dist/question-handler.js +154 -0
  161. package/dist/question-handler.js.map +1 -0
  162. package/dist/question-triage.d.ts +31 -0
  163. package/dist/question-triage.js +175 -0
  164. package/dist/question-triage.js.map +1 -0
  165. package/dist/repo-detection.d.ts +8 -0
  166. package/dist/repo-detection.js +18 -0
  167. package/dist/repo-detection.js.map +1 -0
  168. package/dist/retry-context.d.ts +2 -0
  169. package/dist/retry-context.js +196 -0
  170. package/dist/retry-context.js.map +1 -0
  171. package/dist/router.d.ts +18 -0
  172. package/dist/router.js +137 -0
  173. package/dist/router.js.map +1 -0
  174. package/dist/run-artifact-types.d.ts +43 -0
  175. package/dist/run-artifact-types.js +2 -0
  176. package/dist/run-artifact-types.js.map +1 -0
  177. package/dist/run-summary.d.ts +14 -0
  178. package/dist/run-summary.js +347 -0
  179. package/dist/run-summary.js.map +1 -0
  180. package/dist/run-sync.d.ts +11 -0
  181. package/dist/run-sync.js +110 -0
  182. package/dist/run-sync.js.map +1 -0
  183. package/dist/run.d.ts +26 -0
  184. package/dist/run.js +150 -0
  185. package/dist/run.js.map +1 -0
  186. package/dist/scope-expansion.d.ts +10 -0
  187. package/dist/scope-expansion.js +117 -0
  188. package/dist/scope-expansion.js.map +1 -0
  189. package/dist/scope.d.ts +4 -0
  190. package/dist/scope.js +37 -0
  191. package/dist/scope.js.map +1 -0
  192. package/dist/scorecard.d.ts +18 -0
  193. package/dist/scorecard.js +128 -0
  194. package/dist/scorecard.js.map +1 -0
  195. package/dist/spec-templates.d.ts +2 -0
  196. package/dist/spec-templates.js +285 -0
  197. package/dist/spec-templates.js.map +1 -0
  198. package/dist/spec-validator.d.ts +8 -0
  199. package/dist/spec-validator.js +144 -0
  200. package/dist/spec-validator.js.map +1 -0
  201. package/dist/status.d.ts +68 -0
  202. package/dist/status.js +261 -0
  203. package/dist/status.js.map +1 -0
  204. package/dist/storage.d.ts +9 -0
  205. package/dist/storage.js +35 -0
  206. package/dist/storage.js.map +1 -0
  207. package/dist/task-executor-completion.d.ts +12 -0
  208. package/dist/task-executor-completion.js +67 -0
  209. package/dist/task-executor-completion.js.map +1 -0
  210. package/dist/task-executor-fallback.d.ts +20 -0
  211. package/dist/task-executor-fallback.js +12 -0
  212. package/dist/task-executor-fallback.js.map +1 -0
  213. package/dist/task-executor.d.ts +34 -0
  214. package/dist/task-executor.js +521 -0
  215. package/dist/task-executor.js.map +1 -0
  216. package/dist/task-graph.d.ts +11 -0
  217. package/dist/task-graph.js +226 -0
  218. package/dist/task-graph.js.map +1 -0
  219. package/dist/task-pipeline-helpers.d.ts +45 -0
  220. package/dist/task-pipeline-helpers.js +160 -0
  221. package/dist/task-pipeline-helpers.js.map +1 -0
  222. package/dist/task-review.d.ts +51 -0
  223. package/dist/task-review.js +410 -0
  224. package/dist/task-review.js.map +1 -0
  225. package/dist/transitions.d.ts +13 -0
  226. package/dist/transitions.js +104 -0
  227. package/dist/transitions.js.map +1 -0
  228. package/dist/types.d.ts +405 -0
  229. package/dist/types.js +101 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/utils.d.ts +1 -0
  232. package/dist/utils.js +23 -0
  233. package/dist/utils.js.map +1 -0
  234. package/dist/validation.d.ts +19 -0
  235. package/dist/validation.js +73 -0
  236. package/dist/validation.js.map +1 -0
  237. package/dist/worker-response.d.ts +12 -0
  238. package/dist/worker-response.js +60 -0
  239. package/dist/worker-response.js.map +1 -0
  240. package/dist/worker-runner.d.ts +19 -0
  241. package/dist/worker-runner.js +347 -0
  242. package/dist/worker-runner.js.map +1 -0
  243. package/dist/worktree-cleanup.d.ts +44 -0
  244. package/dist/worktree-cleanup.js +325 -0
  245. package/dist/worktree-cleanup.js.map +1 -0
  246. package/dist/worktree.d.ts +22 -0
  247. package/dist/worktree.js +213 -0
  248. package/dist/worktree.js.map +1 -0
  249. package/examples/spec.md +58 -0
  250. package/package.json +66 -0
@@ -0,0 +1,325 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readJson, fileExists, runDir, taskDir } from './storage.js';
5
+ import { readTaskStatus } from './status.js';
6
+ import { acquireLock, readLock, releaseLock } from './lock.js';
7
+ const DELETABLE_ARTIFACTS = [
8
+ 'response_raw.txt',
9
+ 'response.json',
10
+ 'diff.patch',
11
+ 'worker_prompt.md',
12
+ 'review.json',
13
+ 'diagnostic.json',
14
+ 'trim-log.json',
15
+ ];
16
+ function getTaskGraph(stwRoot, runId) {
17
+ const graphPath = join(runDir(stwRoot, runId), 'task_graph.json');
18
+ if (!fileExists(graphPath)) {
19
+ return null;
20
+ }
21
+ return readJson(graphPath);
22
+ }
23
+ function getEligibleTaskIds(stwRoot, runId) {
24
+ return getTaskGraph(stwRoot, runId)?.tasks.map((task) => task.id) ?? [];
25
+ }
26
+ function parseTaskIdFromBranch(branch) {
27
+ const match = branch.match(/-(T\d+)$/);
28
+ return match?.[1] ?? null;
29
+ }
30
+ function getPreservedWorktreePaths(status) {
31
+ if (!status)
32
+ return [];
33
+ const paths = new Set();
34
+ if (status.preserved_worktree_path) {
35
+ paths.add(status.preserved_worktree_path);
36
+ }
37
+ for (const value of status.preserved_worktree_paths ?? []) {
38
+ if (value)
39
+ paths.add(value);
40
+ }
41
+ return [...paths];
42
+ }
43
+ function getRunTaskIds(stwRoot, runId) {
44
+ return new Set(getEligibleTaskIds(stwRoot, runId));
45
+ }
46
+ function worktreeBelongsToRun(worktree, runId, taskIds) {
47
+ if (!worktree.branch.includes(runId)) {
48
+ return false;
49
+ }
50
+ const taskId = parseTaskIdFromBranch(worktree.branch);
51
+ return taskId !== null && taskIds.has(taskId);
52
+ }
53
+ function branchBelongsToRun(branch, runId, taskIds) {
54
+ if (!branch.includes(runId)) {
55
+ return false;
56
+ }
57
+ const taskId = parseTaskIdFromBranch(branch);
58
+ return taskId !== null && taskIds.has(taskId);
59
+ }
60
+ export function listOrphanedWorktrees(repoRoot, runId) {
61
+ const opts = { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe' };
62
+ const output = execFileSync('git', ['worktree', 'list'], opts);
63
+ return output
64
+ .trim()
65
+ .split('\n')
66
+ .filter((line) => line.includes('stw-worktree-') || line.includes('stw-T'))
67
+ .map((line) => {
68
+ const parts = line.trim().split(/\s+/);
69
+ return {
70
+ path: parts[0],
71
+ commit: parts[1],
72
+ branch: (parts[2] ?? '').replace(/[[\]]/g, ''),
73
+ };
74
+ })
75
+ .filter((worktree) => {
76
+ if (!runId)
77
+ return true;
78
+ return worktree.branch.includes(runId);
79
+ });
80
+ }
81
+ export function cleanupRunWorktrees(repoRoot, runId, opts) {
82
+ const preservedPaths = new Set(opts?.preservedPaths ?? []);
83
+ const taskIds = new Set(opts?.taskIds ?? []);
84
+ const worktrees = listOrphanedWorktrees(repoRoot, runId)
85
+ .filter((worktree) => (taskIds.size === 0 ? true : worktreeBelongsToRun(worktree, runId, taskIds)))
86
+ .filter((worktree) => !preservedPaths.has(worktree.path));
87
+ if (opts?.dryRun)
88
+ return worktrees;
89
+ const execOpts = { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe' };
90
+ for (const wt of worktrees) {
91
+ try {
92
+ execFileSync('git', ['worktree', 'remove', '--force', wt.path], execOpts);
93
+ }
94
+ catch {
95
+ // worktree may already be gone
96
+ }
97
+ }
98
+ return worktrees;
99
+ }
100
+ export function checkRunEligibility(stwRoot, runId, options) {
101
+ const manifestPath = join(runDir(stwRoot, runId), 'manifest.json');
102
+ if (!fileExists(manifestPath)) {
103
+ return { eligible: false, reason: `No manifest found for run ${runId}` };
104
+ }
105
+ const manifest = readJson(manifestPath);
106
+ const terminalStates = ['complete', 'failed', 'stopped'];
107
+ if (!terminalStates.includes(manifest.status)) {
108
+ return { eligible: false, reason: `Run status is '${manifest.status}', not terminal` };
109
+ }
110
+ const graph = getTaskGraph(stwRoot, runId);
111
+ if (graph) {
112
+ for (const task of graph.tasks) {
113
+ const status = readTaskStatus(stwRoot, runId, task.id);
114
+ if (!status) {
115
+ const statePath = join(taskDir(stwRoot, runId, task.id), 'state.json');
116
+ if (fileExists(statePath)) {
117
+ const state = readJson(statePath);
118
+ if (state.state === 'working' || state.state === 'queued') {
119
+ return { eligible: false, reason: `Task ${task.id} is still ${state.state}` };
120
+ }
121
+ }
122
+ continue;
123
+ }
124
+ if (status.status === 'blocked') {
125
+ return { eligible: false, reason: `Task ${task.id} is blocked` };
126
+ }
127
+ }
128
+ }
129
+ const lock = readLock(stwRoot);
130
+ if (lock && !lock.isStale && lock.data.run_id === runId) {
131
+ return { eligible: false, reason: `Run has an active lock (PID ${lock.data.pid})` };
132
+ }
133
+ if (!options.gitlabAvailable) {
134
+ return options.force
135
+ ? { eligible: true }
136
+ : { eligible: false, reason: 'GitLab unavailable — use --force to skip MR verification' };
137
+ }
138
+ const mrState = checkMRState(stwRoot, runId);
139
+ if (mrState && mrState !== 'merged' && mrState !== 'closed') {
140
+ return { eligible: false, reason: `MR is still '${mrState}'` };
141
+ }
142
+ return { eligible: true };
143
+ }
144
+ function checkMRState(stwRoot, runId) {
145
+ const manifestPath = join(runDir(stwRoot, runId), 'manifest.json');
146
+ const manifest = readJson(manifestPath);
147
+ if (!manifest.pr_number)
148
+ return null;
149
+ try {
150
+ const output = execFileSync('glab', ['mr', 'view', String(manifest.pr_number), '--output', 'json'], {
151
+ encoding: 'utf-8',
152
+ stdio: 'pipe',
153
+ });
154
+ const mr = JSON.parse(output);
155
+ return mr.state ?? null;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ export function isGitlabAvailable() {
162
+ try {
163
+ execFileSync('glab', ['auth', 'status'], { encoding: 'utf-8', stdio: 'pipe' });
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ export function listTaskBranches(repoRoot, runId, taskIds) {
171
+ const execOpts = { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe' };
172
+ let output;
173
+ try {
174
+ output = execFileSync('git', ['branch', '--list'], execOpts);
175
+ }
176
+ catch {
177
+ return [];
178
+ }
179
+ const eligibleTaskIds = new Set(taskIds ?? []);
180
+ return output
181
+ .trim()
182
+ .split('\n')
183
+ .map((line) => line.trim().replace(/^[*+]\s*/, ''))
184
+ .filter((branch) => branch !== '')
185
+ .filter((branch) => branch.includes(`-${runId}-`) || branch.startsWith(`stw/${runId}-`) || branch.startsWith(`ai/${runId}-`))
186
+ .filter((branch) => {
187
+ if (eligibleTaskIds.size === 0) {
188
+ return parseTaskIdFromBranch(branch) !== null;
189
+ }
190
+ return branchBelongsToRun(branch, runId, eligibleTaskIds);
191
+ });
192
+ }
193
+ export function deleteTaskBranches(repoRoot, runId, opts) {
194
+ const branches = listTaskBranches(repoRoot, runId, opts?.taskIds);
195
+ if (opts?.dryRun)
196
+ return branches;
197
+ const execOpts = { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe' };
198
+ for (const branch of branches) {
199
+ try {
200
+ execFileSync('git', ['branch', '-D', branch], execOpts);
201
+ }
202
+ catch {
203
+ // branch may already be gone
204
+ }
205
+ }
206
+ return branches;
207
+ }
208
+ export function cleanupRunArtifacts(stwRoot, runId, opts) {
209
+ const deleted = [];
210
+ const runPath = runDir(stwRoot, runId);
211
+ const runLevelLogs = join(runPath, 'logs.ndjson');
212
+ if (existsSync(runLevelLogs)) {
213
+ deleted.push('logs.ndjson');
214
+ if (!opts?.dryRun)
215
+ unlinkSync(runLevelLogs);
216
+ }
217
+ const tasksPath = join(runPath, 'tasks');
218
+ if (!existsSync(tasksPath))
219
+ return deleted;
220
+ for (const taskId of getEligibleTaskIds(stwRoot, runId)) {
221
+ const taskPath = join(tasksPath, taskId);
222
+ for (const artifact of DELETABLE_ARTIFACTS) {
223
+ const artifactPath = join(taskPath, artifact);
224
+ if (existsSync(artifactPath)) {
225
+ deleted.push(`tasks/${taskId}/${artifact}`);
226
+ if (!opts?.dryRun)
227
+ unlinkSync(artifactPath);
228
+ }
229
+ }
230
+ const checkpointsDir = join(taskPath, 'checkpoints');
231
+ if (existsSync(checkpointsDir)) {
232
+ deleted.push(`tasks/${taskId}/checkpoints/`);
233
+ if (!opts?.dryRun)
234
+ rmSync(checkpointsDir, { recursive: true });
235
+ }
236
+ }
237
+ return deleted;
238
+ }
239
+ export function cleanupRun(stwRoot, repoRoot, runId, opts) {
240
+ const eligibility = checkRunEligibility(stwRoot, runId, {
241
+ gitlabAvailable: opts.gitlabAvailable,
242
+ force: opts.force,
243
+ });
244
+ if (!eligibility.eligible) {
245
+ return {
246
+ runId,
247
+ worktreesPruned: 0,
248
+ branchesDeleted: [],
249
+ artifactsDeleted: [],
250
+ skipped: true,
251
+ skipReason: eligibility.reason,
252
+ };
253
+ }
254
+ if (!opts.dryRun) {
255
+ const lockResult = acquireLock(stwRoot, runId);
256
+ if (!lockResult.acquired) {
257
+ return {
258
+ runId,
259
+ worktreesPruned: 0,
260
+ branchesDeleted: [],
261
+ artifactsDeleted: [],
262
+ skipped: true,
263
+ skipReason: `Could not acquire lock (held by PID ${lockResult.existingLock?.data.pid})`,
264
+ };
265
+ }
266
+ }
267
+ try {
268
+ const taskIds = [...getRunTaskIds(stwRoot, runId)];
269
+ const preservedPaths = new Set();
270
+ for (const taskId of taskIds) {
271
+ const status = readTaskStatus(stwRoot, runId, taskId);
272
+ for (const path of getPreservedWorktreePaths(status)) {
273
+ preservedPaths.add(path);
274
+ }
275
+ }
276
+ const worktrees = cleanupRunWorktrees(repoRoot, runId, {
277
+ dryRun: opts.dryRun,
278
+ preservedPaths: [...preservedPaths],
279
+ taskIds,
280
+ });
281
+ const branches = deleteTaskBranches(repoRoot, runId, { dryRun: opts.dryRun, taskIds });
282
+ const artifacts = opts.artifacts ? cleanupRunArtifacts(stwRoot, runId, { dryRun: opts.dryRun }) : [];
283
+ return {
284
+ runId,
285
+ worktreesPruned: worktrees.length,
286
+ branchesDeleted: branches,
287
+ artifactsDeleted: artifacts,
288
+ skipped: false,
289
+ };
290
+ }
291
+ finally {
292
+ if (!opts.dryRun) {
293
+ releaseLock(stwRoot);
294
+ }
295
+ }
296
+ }
297
+ export function listEligibleRuns(stwRoot) {
298
+ const runsPath = join(stwRoot, 'runs');
299
+ if (!existsSync(runsPath))
300
+ return [];
301
+ return readdirSync(runsPath).filter((runId) => {
302
+ const manifestPath = join(runsPath, runId, 'manifest.json');
303
+ if (!fileExists(manifestPath))
304
+ return false;
305
+ const manifest = readJson(manifestPath);
306
+ return ['complete', 'failed', 'stopped'].includes(manifest.status);
307
+ });
308
+ }
309
+ export function listStaleRuns(stwRoot, maxAgeDays = 7) {
310
+ const runsPath = join(stwRoot, 'runs');
311
+ if (!existsSync(runsPath))
312
+ return [];
313
+ const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
314
+ return readdirSync(runsPath).filter((runId) => {
315
+ const manifestPath = join(runsPath, runId, 'manifest.json');
316
+ if (!fileExists(manifestPath))
317
+ return false;
318
+ const manifest = readJson(manifestPath);
319
+ if (!manifest.created_at)
320
+ return false;
321
+ const createdAt = new Date(manifest.created_at).getTime();
322
+ return createdAt < cutoffMs;
323
+ });
324
+ }
325
+ //# sourceMappingURL=worktree-cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../src/worktree-cleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { SeedFileEntry } from './types.js';
2
+ export interface CreateWorktreeResult {
3
+ worktreePath: string;
4
+ taskBranch: string;
5
+ }
6
+ export interface CreateWorktreeOptions {
7
+ repoRoot: string;
8
+ runBranch: string;
9
+ taskId: string;
10
+ worktreeBase?: string;
11
+ preserveExisting?: boolean;
12
+ emptyWorktree?: boolean;
13
+ seedDir?: string;
14
+ }
15
+ export declare function listSeedFiles(seedDir: string, maxFiles?: number): {
16
+ files: SeedFileEntry[];
17
+ totalCount: number;
18
+ };
19
+ export declare function validateSeedDir(seedDir: string): void;
20
+ export declare function createWorktree(options: CreateWorktreeOptions): CreateWorktreeResult;
21
+ export declare function cleanupWorktree(worktreePath: string, repoRoot?: string): void;
22
+ export declare function isWorktreeDirty(worktreePath: string): boolean;
@@ -0,0 +1,213 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { join, relative, extname, basename } from 'node:path';
3
+ import { readdirSync, statSync, copyFileSync, mkdirSync, existsSync, lstatSync } from 'node:fs';
4
+ const MAX_SEED_DIR_BYTES = 50 * 1024 * 1024; // 50MB
5
+ const TEXT_EXTENSIONS = new Set([
6
+ '.ts',
7
+ '.js',
8
+ '.json',
9
+ '.md',
10
+ '.txt',
11
+ '.csv',
12
+ '.yml',
13
+ '.yaml',
14
+ '.toml',
15
+ '.xml',
16
+ '.html',
17
+ '.css',
18
+ '.py',
19
+ '.go',
20
+ '.rs',
21
+ '.java',
22
+ '.sh',
23
+ '.sql',
24
+ '.env',
25
+ '.cfg',
26
+ '.ini',
27
+ '.jsx',
28
+ '.tsx',
29
+ '.vue',
30
+ '.svelte',
31
+ '.rb',
32
+ '.php',
33
+ '.c',
34
+ '.h',
35
+ '.cpp',
36
+ '.hpp',
37
+ '.swift',
38
+ '.kt',
39
+ '.r',
40
+ '.ipynb',
41
+ '.lock',
42
+ '.gitignore',
43
+ '.editorconfig',
44
+ '.prettierrc',
45
+ '.eslintrc',
46
+ '.dockerfile',
47
+ '.makefile',
48
+ ]);
49
+ function formatSize(bytes) {
50
+ if (bytes < 1024)
51
+ return `${bytes} B`;
52
+ if (bytes < 1024 * 1024)
53
+ return `${(bytes / 1024).toFixed(1)} KB`;
54
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
55
+ }
56
+ function isTextFile(filePath) {
57
+ const ext = extname(filePath).toLowerCase();
58
+ const name = basename(filePath).toLowerCase();
59
+ // Files with no extension but known names
60
+ if (!ext && ['makefile', 'dockerfile', 'license', 'readme', 'changelog'].includes(name))
61
+ return true;
62
+ return TEXT_EXTENSIONS.has(ext);
63
+ }
64
+ export function listSeedFiles(seedDir, maxFiles = 50) {
65
+ const all = [];
66
+ function walk(dir) {
67
+ const items = readdirSync(dir, { withFileTypes: true });
68
+ for (const item of items) {
69
+ if (item.name === '.git')
70
+ continue;
71
+ const fullPath = join(dir, item.name);
72
+ if (item.isDirectory()) {
73
+ walk(fullPath);
74
+ }
75
+ else if (item.isFile()) {
76
+ const stat = statSync(fullPath);
77
+ all.push({
78
+ relativePath: relative(seedDir, fullPath),
79
+ absolutePath: fullPath,
80
+ sizeBytes: stat.size,
81
+ sizeHuman: formatSize(stat.size),
82
+ isBinary: !isTextFile(fullPath),
83
+ });
84
+ }
85
+ }
86
+ }
87
+ walk(seedDir);
88
+ return { files: all.slice(0, maxFiles), totalCount: all.length };
89
+ }
90
+ export function validateSeedDir(seedDir) {
91
+ if (!existsSync(seedDir)) {
92
+ throw new Error(`Seed directory does not exist: ${seedDir}`);
93
+ }
94
+ const stat = lstatSync(seedDir);
95
+ if (!stat.isDirectory()) {
96
+ throw new Error(`--from path must be a directory, not a file: ${seedDir}`);
97
+ }
98
+ const totalBytes = computeDirSize(seedDir);
99
+ if (totalBytes > MAX_SEED_DIR_BYTES) {
100
+ throw new Error(`Seed directory exceeds 50MB limit (${formatSize(totalBytes)}). Reduce the directory size or remove large files.`);
101
+ }
102
+ }
103
+ function computeDirSize(dir) {
104
+ let total = 0;
105
+ const items = readdirSync(dir, { withFileTypes: true });
106
+ for (const item of items) {
107
+ if (item.name === '.git')
108
+ continue;
109
+ const fullPath = join(dir, item.name);
110
+ if (item.isDirectory()) {
111
+ total += computeDirSize(fullPath);
112
+ }
113
+ else if (item.isFile()) {
114
+ total += statSync(fullPath).size;
115
+ }
116
+ }
117
+ return total;
118
+ }
119
+ export function createWorktree(options) {
120
+ const { repoRoot, runBranch, taskId, worktreeBase = '/tmp', preserveExisting = false, emptyWorktree = false, seedDir, } = options;
121
+ const taskBranch = `${runBranch}-${taskId}`;
122
+ const worktreePath = join(worktreeBase, `stw-worktree-${taskId}`);
123
+ if (preserveExisting) {
124
+ const opts = {
125
+ cwd: worktreePath,
126
+ encoding: 'utf-8',
127
+ stdio: 'pipe',
128
+ };
129
+ try {
130
+ const branch = execFileSync('git', ['branch', '--show-current'], opts).trim();
131
+ if (branch === taskBranch) {
132
+ return { worktreePath, taskBranch };
133
+ }
134
+ }
135
+ catch {
136
+ // Fall through to normal worktree creation.
137
+ }
138
+ }
139
+ const runOpts = {
140
+ cwd: repoRoot,
141
+ encoding: 'utf-8',
142
+ stdio: 'pipe',
143
+ };
144
+ execFileSync('git', ['worktree', 'prune'], runOpts);
145
+ try {
146
+ execFileSync('git', ['worktree', 'remove', worktreePath, '--force'], runOpts);
147
+ }
148
+ catch {
149
+ // Worktree doesn't exist or is already gone.
150
+ }
151
+ try {
152
+ execFileSync('git', ['branch', '-D', taskBranch], runOpts);
153
+ }
154
+ catch {
155
+ // Branch doesn't exist - expected on first run.
156
+ }
157
+ execFileSync('git', ['worktree', 'add', worktreePath, '-b', taskBranch, runBranch], runOpts);
158
+ if (emptyWorktree) {
159
+ const worktreeOpts = {
160
+ cwd: worktreePath,
161
+ encoding: 'utf-8',
162
+ stdio: 'pipe',
163
+ };
164
+ execFileSync('git', ['rm', '-rf', '.', '--ignore-unmatch'], worktreeOpts);
165
+ execFileSync('git', ['commit', '--allow-empty', '-m', 'Empty worktree for greenfield scaffolding'], worktreeOpts);
166
+ }
167
+ if (seedDir) {
168
+ validateSeedDir(seedDir);
169
+ seedWorktree(worktreePath, seedDir);
170
+ }
171
+ return { worktreePath, taskBranch };
172
+ }
173
+ function seedWorktree(worktreePath, seedDir) {
174
+ const { files } = listSeedFiles(seedDir, Infinity);
175
+ const worktreeOpts = {
176
+ cwd: worktreePath,
177
+ encoding: 'utf-8',
178
+ stdio: 'pipe',
179
+ };
180
+ for (const file of files) {
181
+ const destPath = join(worktreePath, file.relativePath);
182
+ const destDir = join(destPath, '..');
183
+ if (!existsSync(destDir)) {
184
+ mkdirSync(destDir, { recursive: true });
185
+ }
186
+ copyFileSync(file.absolutePath, destPath);
187
+ }
188
+ execFileSync('git', ['add', '-A'], worktreeOpts);
189
+ execFileSync('git', ['commit', '--allow-empty', '-m', `Seed from ${seedDir}`], worktreeOpts);
190
+ }
191
+ export function cleanupWorktree(worktreePath, repoRoot) {
192
+ const opts = {
193
+ cwd: repoRoot,
194
+ encoding: 'utf-8',
195
+ stdio: 'pipe',
196
+ };
197
+ execFileSync('git', ['worktree', 'remove', worktreePath, '--force'], opts);
198
+ }
199
+ export function isWorktreeDirty(worktreePath) {
200
+ const opts = {
201
+ cwd: worktreePath,
202
+ encoding: 'utf-8',
203
+ stdio: 'pipe',
204
+ };
205
+ try {
206
+ const output = execFileSync('git', ['status', '--porcelain'], opts);
207
+ return output.trim().length > 0;
208
+ }
209
+ catch {
210
+ return false;
211
+ }
212
+ }
213
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../src/worktree.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ # Spec: Add rate limiting to API endpoints
2
+
3
+ ## Objective
4
+
5
+ Add configurable rate limiting to all public API endpoints to prevent abuse
6
+ and ensure fair usage across clients.
7
+
8
+ ## Context
9
+
10
+ The API currently has no rate limiting. Under load testing, a single client
11
+ can monopolize server resources. The team agreed to add in-memory rate
12
+ limiting as a first step before moving to Redis-backed limits later.
13
+
14
+ ## Requirements
15
+
16
+ ### T1: Rate limiter middleware
17
+
18
+ Create a rate limiting middleware in `src/middleware/rate-limit.ts`.
19
+
20
+ - Accept `requestsPerMinute` and `burstSize` as constructor parameters
21
+ - Use a sliding window algorithm (not fixed window)
22
+ - Return HTTP 429 with a `Retry-After` header when the limit is exceeded
23
+ - Track limits per client IP (`req.ip`)
24
+
25
+ ### T2: Apply middleware to routes
26
+
27
+ Wire the rate limiter into `src/routes/api.ts` for all `/api/v1/*` endpoints.
28
+
29
+ - Default: 60 requests/minute, burst of 10
30
+ - Make limits configurable via `RATE_LIMIT_RPM` and `RATE_LIMIT_BURST` env vars
31
+ - Add a `/api/v1/health` endpoint that is exempt from rate limiting
32
+
33
+ ### T3: Unit tests for rate limiter
34
+
35
+ Add tests in `tests/rate-limit.test.ts`:
36
+
37
+ - Allows requests under the limit
38
+ - Blocks requests over the limit with 429
39
+ - Resets the window after the configured interval
40
+ - Respects burst allowance
41
+
42
+ ## Constraints
43
+
44
+ - No external dependencies — use an in-memory store (Map with timestamps)
45
+ - Must not break existing tests (`npm test` must pass)
46
+ - Keep middleware stateless across server restarts (no persistence needed)
47
+
48
+ ## Acceptance Criteria
49
+
50
+ - `npm test` passes with new and existing tests
51
+ - `npx tsc --noEmit` passes
52
+ - Rate limiter returns 429 with correct `Retry-After` header
53
+ - Middleware is applied to all `/api/v1/*` routes except health check
54
+
55
+ ## Risk Assessment
56
+
57
+ - **Low risk**: This is additive — no existing behavior changes
58
+ - T1 and T3 can run in parallel; T2 depends on T1