@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.
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/dist/agentic-fallback.d.ts +3 -0
- package/dist/agentic-fallback.js +32 -0
- package/dist/agentic-fallback.js.map +1 -0
- package/dist/agentic-prompt.d.ts +2 -0
- package/dist/agentic-prompt.js +68 -0
- package/dist/agentic-prompt.js.map +1 -0
- package/dist/agentic-runtime.d.ts +48 -0
- package/dist/agentic-runtime.js +149 -0
- package/dist/agentic-runtime.js.map +1 -0
- package/dist/agentic-types.d.ts +37 -0
- package/dist/agentic-types.js +2 -0
- package/dist/agentic-types.js.map +1 -0
- package/dist/agents.d.ts +7 -0
- package/dist/agents.js +2 -0
- package/dist/agents.js.map +1 -0
- package/dist/assignments.d.ts +7 -0
- package/dist/assignments.js +125 -0
- package/dist/assignments.js.map +1 -0
- package/dist/checkpoint.d.ts +35 -0
- package/dist/checkpoint.js +78 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/circuit-breaker.d.ts +17 -0
- package/dist/circuit-breaker.js +65 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/claim.d.ts +6 -0
- package/dist/claim.js +135 -0
- package/dist/claim.js.map +1 -0
- package/dist/clarity-gate.d.ts +12 -0
- package/dist/clarity-gate.js +83 -0
- package/dist/clarity-gate.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +38 -0
- package/dist/cli.js.map +1 -0
- package/dist/command-dispatch.d.ts +45 -0
- package/dist/command-dispatch.js +206 -0
- package/dist/command-dispatch.js.map +1 -0
- package/dist/command-parser.d.ts +11 -0
- package/dist/command-parser.js +101 -0
- package/dist/command-parser.js.map +1 -0
- package/dist/commands/clean.d.ts +10 -0
- package/dist/commands/clean.js +133 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/execution.d.ts +2 -0
- package/dist/commands/execution.js +327 -0
- package/dist/commands/execution.js.map +1 -0
- package/dist/commands/go.d.ts +2 -0
- package/dist/commands/go.js +197 -0
- package/dist/commands/go.js.map +1 -0
- package/dist/commands/helpers.d.ts +44 -0
- package/dist/commands/helpers.js +231 -0
- package/dist/commands/helpers.js.map +1 -0
- package/dist/commands/idea.d.ts +2 -0
- package/dist/commands/idea.js +89 -0
- package/dist/commands/idea.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/integration.d.ts +7 -0
- package/dist/commands/integration.js +139 -0
- package/dist/commands/integration.js.map +1 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +301 -0
- package/dist/commands/maintenance.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +356 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +198 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +35 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/stats.d.ts +2 -0
- package/dist/commands/stats.js +80 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/task-ops.d.ts +2 -0
- package/dist/commands/task-ops.js +406 -0
- package/dist/commands/task-ops.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +338 -0
- package/dist/config.js.map +1 -0
- package/dist/cost.d.ts +30 -0
- package/dist/cost.js +167 -0
- package/dist/cost.js.map +1 -0
- package/dist/crash-recovery.d.ts +9 -0
- package/dist/crash-recovery.js +42 -0
- package/dist/crash-recovery.js.map +1 -0
- package/dist/diagnostic.d.ts +48 -0
- package/dist/diagnostic.js +328 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/doctor.d.ts +31 -0
- package/dist/doctor.js +225 -0
- package/dist/doctor.js.map +1 -0
- package/dist/drift.d.ts +11 -0
- package/dist/drift.js +57 -0
- package/dist/drift.js.map +1 -0
- package/dist/git-utils.d.ts +20 -0
- package/dist/git-utils.js +206 -0
- package/dist/git-utils.js.map +1 -0
- package/dist/gitlab.d.ts +54 -0
- package/dist/gitlab.js +101 -0
- package/dist/gitlab.js.map +1 -0
- package/dist/idea.d.ts +35 -0
- package/dist/idea.js +251 -0
- package/dist/idea.js.map +1 -0
- package/dist/import-resolution.d.ts +13 -0
- package/dist/import-resolution.js +111 -0
- package/dist/import-resolution.js.map +1 -0
- package/dist/inbox-renderer.d.ts +2 -0
- package/dist/inbox-renderer.js +67 -0
- package/dist/inbox-renderer.js.map +1 -0
- package/dist/init.d.ts +105 -0
- package/dist/init.js +235 -0
- package/dist/init.js.map +1 -0
- package/dist/llm-reviewer.d.ts +14 -0
- package/dist/llm-reviewer.js +109 -0
- package/dist/llm-reviewer.js.map +1 -0
- package/dist/lock.d.ts +26 -0
- package/dist/lock.js +76 -0
- package/dist/lock.js.map +1 -0
- package/dist/logger.d.ts +24 -0
- package/dist/logger.js +40 -0
- package/dist/logger.js.map +1 -0
- package/dist/math-utils.d.ts +2 -0
- package/dist/math-utils.js +7 -0
- package/dist/math-utils.js.map +1 -0
- package/dist/mechanical-review.d.ts +30 -0
- package/dist/mechanical-review.js +76 -0
- package/dist/mechanical-review.js.map +1 -0
- package/dist/merge.d.ts +83 -0
- package/dist/merge.js +363 -0
- package/dist/merge.js.map +1 -0
- package/dist/parallel.d.ts +35 -0
- package/dist/parallel.js +214 -0
- package/dist/parallel.js.map +1 -0
- package/dist/plan-validation.d.ts +19 -0
- package/dist/plan-validation.js +253 -0
- package/dist/plan-validation.js.map +1 -0
- package/dist/planner-prompt.d.ts +33 -0
- package/dist/planner-prompt.js +244 -0
- package/dist/planner-prompt.js.map +1 -0
- package/dist/planner.d.ts +29 -0
- package/dist/planner.js +511 -0
- package/dist/planner.js.map +1 -0
- package/dist/poller.d.ts +34 -0
- package/dist/poller.js +91 -0
- package/dist/poller.js.map +1 -0
- package/dist/progress.d.ts +34 -0
- package/dist/progress.js +122 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-builder.d.ts +51 -0
- package/dist/prompt-builder.js +481 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +278 -0
- package/dist/provider.js.map +1 -0
- package/dist/question-handler.d.ts +18 -0
- package/dist/question-handler.js +154 -0
- package/dist/question-handler.js.map +1 -0
- package/dist/question-triage.d.ts +31 -0
- package/dist/question-triage.js +175 -0
- package/dist/question-triage.js.map +1 -0
- package/dist/repo-detection.d.ts +8 -0
- package/dist/repo-detection.js +18 -0
- package/dist/repo-detection.js.map +1 -0
- package/dist/retry-context.d.ts +2 -0
- package/dist/retry-context.js +196 -0
- package/dist/retry-context.js.map +1 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.js +137 -0
- package/dist/router.js.map +1 -0
- package/dist/run-artifact-types.d.ts +43 -0
- package/dist/run-artifact-types.js +2 -0
- package/dist/run-artifact-types.js.map +1 -0
- package/dist/run-summary.d.ts +14 -0
- package/dist/run-summary.js +347 -0
- package/dist/run-summary.js.map +1 -0
- package/dist/run-sync.d.ts +11 -0
- package/dist/run-sync.js +110 -0
- package/dist/run-sync.js.map +1 -0
- package/dist/run.d.ts +26 -0
- package/dist/run.js +150 -0
- package/dist/run.js.map +1 -0
- package/dist/scope-expansion.d.ts +10 -0
- package/dist/scope-expansion.js +117 -0
- package/dist/scope-expansion.js.map +1 -0
- package/dist/scope.d.ts +4 -0
- package/dist/scope.js +37 -0
- package/dist/scope.js.map +1 -0
- package/dist/scorecard.d.ts +18 -0
- package/dist/scorecard.js +128 -0
- package/dist/scorecard.js.map +1 -0
- package/dist/spec-templates.d.ts +2 -0
- package/dist/spec-templates.js +285 -0
- package/dist/spec-templates.js.map +1 -0
- package/dist/spec-validator.d.ts +8 -0
- package/dist/spec-validator.js +144 -0
- package/dist/spec-validator.js.map +1 -0
- package/dist/status.d.ts +68 -0
- package/dist/status.js +261 -0
- package/dist/status.js.map +1 -0
- package/dist/storage.d.ts +9 -0
- package/dist/storage.js +35 -0
- package/dist/storage.js.map +1 -0
- package/dist/task-executor-completion.d.ts +12 -0
- package/dist/task-executor-completion.js +67 -0
- package/dist/task-executor-completion.js.map +1 -0
- package/dist/task-executor-fallback.d.ts +20 -0
- package/dist/task-executor-fallback.js +12 -0
- package/dist/task-executor-fallback.js.map +1 -0
- package/dist/task-executor.d.ts +34 -0
- package/dist/task-executor.js +521 -0
- package/dist/task-executor.js.map +1 -0
- package/dist/task-graph.d.ts +11 -0
- package/dist/task-graph.js +226 -0
- package/dist/task-graph.js.map +1 -0
- package/dist/task-pipeline-helpers.d.ts +45 -0
- package/dist/task-pipeline-helpers.js +160 -0
- package/dist/task-pipeline-helpers.js.map +1 -0
- package/dist/task-review.d.ts +51 -0
- package/dist/task-review.js +410 -0
- package/dist/task-review.js.map +1 -0
- package/dist/transitions.d.ts +13 -0
- package/dist/transitions.js +104 -0
- package/dist/transitions.js.map +1 -0
- package/dist/types.d.ts +405 -0
- package/dist/types.js +101 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +23 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +19 -0
- package/dist/validation.js +73 -0
- package/dist/validation.js.map +1 -0
- package/dist/worker-response.d.ts +12 -0
- package/dist/worker-response.js +60 -0
- package/dist/worker-response.js.map +1 -0
- package/dist/worker-runner.d.ts +19 -0
- package/dist/worker-runner.js +347 -0
- package/dist/worker-runner.js.map +1 -0
- package/dist/worktree-cleanup.d.ts +44 -0
- package/dist/worktree-cleanup.js +325 -0
- package/dist/worktree-cleanup.js.map +1 -0
- package/dist/worktree.d.ts +22 -0
- package/dist/worktree.js +213 -0
- package/dist/worktree.js.map +1 -0
- package/examples/spec.md +58 -0
- 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;
|
package/dist/worktree.js
ADDED
|
@@ -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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"}
|
package/examples/spec.md
ADDED
|
@@ -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
|