steroids-cli 0.10.34 → 0.10.36

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 (69) hide show
  1. package/dist/commands/loop-phases.d.ts +3 -2
  2. package/dist/commands/loop-phases.d.ts.map +1 -1
  3. package/dist/commands/loop-phases.js +146 -50
  4. package/dist/commands/loop-phases.js.map +1 -1
  5. package/dist/commands/loop.d.ts.map +1 -1
  6. package/dist/commands/loop.js +21 -3
  7. package/dist/commands/loop.js.map +1 -1
  8. package/dist/commands/tasks-reset.d.ts.map +1 -1
  9. package/dist/commands/tasks-reset.js +6 -0
  10. package/dist/commands/tasks-reset.js.map +1 -1
  11. package/dist/commands/workspaces.d.ts.map +1 -1
  12. package/dist/commands/workspaces.js +71 -1
  13. package/dist/commands/workspaces.js.map +1 -1
  14. package/dist/database/queries.d.ts +15 -1
  15. package/dist/database/queries.d.ts.map +1 -1
  16. package/dist/database/queries.js +43 -0
  17. package/dist/database/queries.js.map +1 -1
  18. package/dist/database/schema.d.ts +2 -2
  19. package/dist/database/schema.d.ts.map +1 -1
  20. package/dist/database/schema.js +4 -0
  21. package/dist/database/schema.js.map +1 -1
  22. package/dist/providers/codex.d.ts +6 -0
  23. package/dist/providers/codex.d.ts.map +1 -1
  24. package/dist/providers/codex.js +40 -14
  25. package/dist/providers/codex.js.map +1 -1
  26. package/dist/runners/global-db-connection.d.ts.map +1 -1
  27. package/dist/runners/global-db-connection.js +6 -0
  28. package/dist/runners/global-db-connection.js.map +1 -1
  29. package/dist/runners/global-db-schema.d.ts +3 -1
  30. package/dist/runners/global-db-schema.d.ts.map +1 -1
  31. package/dist/runners/global-db-schema.js +39 -2
  32. package/dist/runners/global-db-schema.js.map +1 -1
  33. package/dist/runners/orchestrator-loop.d.ts.map +1 -1
  34. package/dist/runners/orchestrator-loop.js +114 -6
  35. package/dist/runners/orchestrator-loop.js.map +1 -1
  36. package/dist/runners/wakeup.d.ts.map +1 -1
  37. package/dist/runners/wakeup.js +14 -0
  38. package/dist/runners/wakeup.js.map +1 -1
  39. package/dist/workspace/git-helpers.d.ts +54 -0
  40. package/dist/workspace/git-helpers.d.ts.map +1 -0
  41. package/dist/workspace/git-helpers.js +136 -0
  42. package/dist/workspace/git-helpers.js.map +1 -0
  43. package/dist/workspace/git-lifecycle.d.ts +53 -0
  44. package/dist/workspace/git-lifecycle.d.ts.map +1 -0
  45. package/dist/workspace/git-lifecycle.js +287 -0
  46. package/dist/workspace/git-lifecycle.js.map +1 -0
  47. package/dist/workspace/merge-lock.d.ts +25 -0
  48. package/dist/workspace/merge-lock.d.ts.map +1 -0
  49. package/dist/workspace/merge-lock.js +93 -0
  50. package/dist/workspace/merge-lock.js.map +1 -0
  51. package/dist/workspace/merge-pipeline.d.ts +21 -0
  52. package/dist/workspace/merge-pipeline.d.ts.map +1 -0
  53. package/dist/workspace/merge-pipeline.js +55 -0
  54. package/dist/workspace/merge-pipeline.js.map +1 -0
  55. package/dist/workspace/pool.d.ts +59 -0
  56. package/dist/workspace/pool.d.ts.map +1 -0
  57. package/dist/workspace/pool.js +247 -0
  58. package/dist/workspace/pool.js.map +1 -0
  59. package/dist/workspace/reconcile.d.ts +20 -0
  60. package/dist/workspace/reconcile.d.ts.map +1 -0
  61. package/dist/workspace/reconcile.js +50 -0
  62. package/dist/workspace/reconcile.js.map +1 -0
  63. package/dist/workspace/types.d.ts +27 -0
  64. package/dist/workspace/types.d.ts.map +1 -0
  65. package/dist/workspace/types.js +6 -0
  66. package/dist/workspace/types.js.map +1 -0
  67. package/migrations/020_add_workspace_fields.sql +10 -0
  68. package/migrations/manifest.json +9 -1
  69. package/package.json +1 -1
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace pool manager — DB-backed slot claiming, release, and clone management.
4
+ *
5
+ * Pool slots live in the global database (`workspace_pool_slots`).
6
+ * Each slot is a full git clone at `~/.steroids/workspaces/<projectHash>/pool-<index>/`.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.resolveRemoteUrl = resolveRemoteUrl;
10
+ exports.claimSlot = claimSlot;
11
+ exports.finalizeSlotPath = finalizeSlotPath;
12
+ exports.releaseSlot = releaseSlot;
13
+ exports.partialReleaseSlot = partialReleaseSlot;
14
+ exports.updateSlotStatus = updateSlotStatus;
15
+ exports.refreshSlotHeartbeat = refreshSlotHeartbeat;
16
+ exports.getSlot = getSlot;
17
+ exports.listProjectSlots = listProjectSlots;
18
+ exports.ensureSlotClone = ensureSlotClone;
19
+ const node_child_process_1 = require("node:child_process");
20
+ const node_fs_1 = require("node:fs");
21
+ const node_path_1 = require("node:path");
22
+ const git_helpers_js_1 = require("./git-helpers.js");
23
+ const clone_js_1 = require("../parallel/clone.js");
24
+ /**
25
+ * Resolve the remote URL for a project.
26
+ * Returns null for local-only projects (no remote or local filesystem path).
27
+ */
28
+ function resolveRemoteUrl(projectPath) {
29
+ try {
30
+ const url = (0, node_child_process_1.execFileSync)('git', ['remote', 'get-url', 'origin'], {
31
+ cwd: projectPath,
32
+ encoding: 'utf-8',
33
+ stdio: ['pipe', 'pipe', 'pipe'],
34
+ }).trim();
35
+ if (!url)
36
+ return null;
37
+ // If it's a local filesystem path (not https://, ssh://, git@), treat as local-only
38
+ if (url.startsWith('/') || url.startsWith('.') || url.startsWith('~')) {
39
+ return null;
40
+ }
41
+ return url;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Build the slot path for a given project and index.
49
+ */
50
+ function buildSlotPath(projectPath, slotIndex) {
51
+ const workspaceRoot = (0, clone_js_1.getDefaultWorkspaceRoot)();
52
+ const projectHash = (0, clone_js_1.getProjectHash)(projectPath);
53
+ return (0, node_path_1.join)(workspaceRoot, projectHash, `pool-${slotIndex}`);
54
+ }
55
+ /**
56
+ * Claim a pool slot for a task. Creates a new slot if none are idle.
57
+ * Uses BEGIN IMMEDIATE for serialised access.
58
+ */
59
+ function claimSlot(globalDb, projectId, runnerId, taskId) {
60
+ const now = Date.now();
61
+ const claim = globalDb.transaction(() => {
62
+ // Try to find an idle slot
63
+ let slot = globalDb
64
+ .prepare(`SELECT * FROM workspace_pool_slots
65
+ WHERE project_id = ? AND status = 'idle'
66
+ LIMIT 1`)
67
+ .get(projectId);
68
+ if (slot) {
69
+ globalDb
70
+ .prepare(`UPDATE workspace_pool_slots
71
+ SET runner_id = ?, task_id = ?, status = 'coder_active',
72
+ claimed_at = ?, heartbeat_at = ?
73
+ WHERE id = ?`)
74
+ .run(runnerId, taskId, now, now, slot.id);
75
+ return globalDb
76
+ .prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
77
+ .get(slot.id);
78
+ }
79
+ // No idle slot — create a new one
80
+ const maxIndex = globalDb
81
+ .prepare('SELECT MAX(slot_index) as max_idx FROM workspace_pool_slots WHERE project_id = ?')
82
+ .get(projectId);
83
+ const nextIndex = (maxIndex?.max_idx ?? -1) + 1;
84
+ try {
85
+ globalDb
86
+ .prepare(`INSERT INTO workspace_pool_slots
87
+ (project_id, slot_index, slot_path, runner_id, task_id, status, claimed_at, heartbeat_at)
88
+ VALUES (?, ?, ?, ?, ?, 'coder_active', ?, ?)`)
89
+ .run(projectId, nextIndex, '', // slot_path set after we know the project path
90
+ runnerId, taskId, now, now);
91
+ }
92
+ catch (error) {
93
+ if (error.message?.includes('UNIQUE constraint')) {
94
+ // Race condition: another runner created a slot — retry idle search
95
+ slot = globalDb
96
+ .prepare(`SELECT * FROM workspace_pool_slots
97
+ WHERE project_id = ? AND status = 'idle'
98
+ LIMIT 1`)
99
+ .get(projectId);
100
+ if (slot) {
101
+ globalDb
102
+ .prepare(`UPDATE workspace_pool_slots
103
+ SET runner_id = ?, task_id = ?, status = 'coder_active',
104
+ claimed_at = ?, heartbeat_at = ?
105
+ WHERE id = ?`)
106
+ .run(runnerId, taskId, now, now, slot.id);
107
+ return globalDb
108
+ .prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
109
+ .get(slot.id);
110
+ }
111
+ }
112
+ throw error;
113
+ }
114
+ const newSlotId = globalDb
115
+ .prepare(`SELECT id FROM workspace_pool_slots
116
+ WHERE project_id = ? AND slot_index = ?`)
117
+ .get(projectId, nextIndex);
118
+ return globalDb
119
+ .prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
120
+ .get(newSlotId.id);
121
+ });
122
+ return claim.immediate();
123
+ }
124
+ /**
125
+ * Finalize the slot path and remote_url after claiming. Call once after claimSlot()
126
+ * when you know the projectPath.
127
+ */
128
+ function finalizeSlotPath(globalDb, slotId, projectPath, remoteUrl) {
129
+ const slotPath = buildSlotPath(projectPath, getSlot(globalDb, slotId).slot_index);
130
+ globalDb
131
+ .prepare(`UPDATE workspace_pool_slots
132
+ SET slot_path = ?, remote_url = ?
133
+ WHERE id = ?`)
134
+ .run(slotPath, remoteUrl, slotId);
135
+ return globalDb
136
+ .prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
137
+ .get(slotId);
138
+ }
139
+ /**
140
+ * Release a slot back to idle, clearing task fields.
141
+ */
142
+ function releaseSlot(globalDb, slotId) {
143
+ globalDb
144
+ .prepare(`UPDATE workspace_pool_slots
145
+ SET status = 'idle', runner_id = NULL, task_id = NULL,
146
+ task_branch = NULL, starting_sha = NULL,
147
+ claimed_at = NULL, heartbeat_at = NULL
148
+ WHERE id = ?`)
149
+ .run(slotId);
150
+ }
151
+ /**
152
+ * Partially release a slot back to idle, preserving workspace fields
153
+ * (task_branch, base_branch, starting_sha) so the reviewer phase can pick
154
+ * them up in the next loop iteration without needing to re-run prepareForTask.
155
+ *
156
+ * Only clears runner-tracking fields (runner_id, claimed_at, heartbeat_at).
157
+ */
158
+ function partialReleaseSlot(globalDb, slotId) {
159
+ globalDb
160
+ .prepare(`UPDATE workspace_pool_slots
161
+ SET status = 'idle', runner_id = NULL,
162
+ claimed_at = NULL, heartbeat_at = NULL
163
+ WHERE id = ?`)
164
+ .run(slotId);
165
+ }
166
+ /**
167
+ * Update slot status and optional fields.
168
+ */
169
+ function updateSlotStatus(globalDb, slotId, status, fields) {
170
+ const sets = ['status = ?'];
171
+ const params = [status];
172
+ if (fields?.task_id !== undefined) {
173
+ sets.push('task_id = ?');
174
+ params.push(fields.task_id);
175
+ }
176
+ if (fields?.task_branch !== undefined) {
177
+ sets.push('task_branch = ?');
178
+ params.push(fields.task_branch);
179
+ }
180
+ if (fields?.base_branch !== undefined) {
181
+ sets.push('base_branch = ?');
182
+ params.push(fields.base_branch);
183
+ }
184
+ if (fields?.starting_sha !== undefined) {
185
+ sets.push('starting_sha = ?');
186
+ params.push(fields.starting_sha);
187
+ }
188
+ params.push(slotId);
189
+ globalDb
190
+ .prepare(`UPDATE workspace_pool_slots SET ${sets.join(', ')} WHERE id = ?`)
191
+ .run(...params);
192
+ }
193
+ /**
194
+ * Refresh the heartbeat timestamp for a slot.
195
+ */
196
+ function refreshSlotHeartbeat(globalDb, slotId) {
197
+ globalDb
198
+ .prepare('UPDATE workspace_pool_slots SET heartbeat_at = ? WHERE id = ?')
199
+ .run(Date.now(), slotId);
200
+ }
201
+ /**
202
+ * Get a single slot by ID.
203
+ */
204
+ function getSlot(globalDb, slotId) {
205
+ return (globalDb
206
+ .prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
207
+ .get(slotId) ?? null);
208
+ }
209
+ /**
210
+ * List all slots for a project.
211
+ */
212
+ function listProjectSlots(globalDb, projectId) {
213
+ return globalDb
214
+ .prepare('SELECT * FROM workspace_pool_slots WHERE project_id = ? ORDER BY slot_index')
215
+ .all(projectId);
216
+ }
217
+ /**
218
+ * Ensure the slot directory exists as a full clone.
219
+ * - If missing: full clone from remoteUrl (or projectPath for local-only).
220
+ * - If shallow: `git fetch --unshallow`.
221
+ * - Always ensure .steroids symlink.
222
+ */
223
+ function ensureSlotClone(slot, remoteUrl, projectPath) {
224
+ const slotPath = slot.slot_path;
225
+ if (!(0, node_fs_1.existsSync)(slotPath) || !(0, node_fs_1.existsSync)((0, node_path_1.join)(slotPath, '.git'))) {
226
+ // Remove any partial directory
227
+ if ((0, node_fs_1.existsSync)(slotPath)) {
228
+ (0, node_fs_1.rmSync)(slotPath, { recursive: true, force: true });
229
+ }
230
+ // Create parent directories
231
+ (0, node_fs_1.mkdirSync)((0, node_path_1.resolve)(slotPath, '..'), { recursive: true });
232
+ // Full clone — no --depth, no --single-branch
233
+ const cloneSource = remoteUrl ?? projectPath;
234
+ (0, node_child_process_1.execFileSync)('git', ['clone', '--no-tags', cloneSource, slotPath], {
235
+ cwd: process.cwd(),
236
+ stdio: ['pipe', 'pipe', 'pipe'],
237
+ timeout: 300_000, // 5 min for large repos
238
+ });
239
+ }
240
+ // If shallow, unshallow it
241
+ if ((0, git_helpers_js_1.isShallowRepository)(slotPath)) {
242
+ (0, git_helpers_js_1.execGit)(slotPath, ['fetch', '--unshallow'], { timeoutMs: 300_000 });
243
+ }
244
+ // Ensure .steroids symlink points to the source project
245
+ (0, clone_js_1.ensureWorkspaceSteroidsSymlink)(slotPath, projectPath);
246
+ }
247
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/workspace/pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Workspace pool reconciliation — reclaim stale slots and locks.
3
+ *
4
+ * Called during runner wakeup to reset state after crashes.
5
+ * Policy: Stale = reset. No state-dependent recovery.
6
+ */
7
+ import type Database from 'better-sqlite3';
8
+ export interface ReconcileResult {
9
+ resetSlots: number;
10
+ deletedLocks: number;
11
+ taskIds: string[];
12
+ }
13
+ /**
14
+ * Find and reset stale workspace pool slots and merge locks.
15
+ *
16
+ * Step 1: Slots with heartbeat_at < now - 5min AND status != 'idle' → reset to idle
17
+ * Step 2: Merge locks with heartbeat_at < now - 3min → delete
18
+ */
19
+ export declare function reconcileStaleWorkspaces(globalDb: Database.Database): ReconcileResult;
20
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/workspace/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAM3C,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,GAAG,eAAe,CAyCrF"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace pool reconciliation — reclaim stale slots and locks.
4
+ *
5
+ * Called during runner wakeup to reset state after crashes.
6
+ * Policy: Stale = reset. No state-dependent recovery.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.reconcileStaleWorkspaces = reconcileStaleWorkspaces;
10
+ const STALE_SLOT_TTL_MS = 10 * 60 * 1000; // 10 minutes — must exceed clone timeout (5min) + event loop block
11
+ const STALE_LOCK_TTL_MS = 10 * 60 * 1000; // 10 minutes — must exceed worst-case push retry (3×120s = 360s)
12
+ /**
13
+ * Find and reset stale workspace pool slots and merge locks.
14
+ *
15
+ * Step 1: Slots with heartbeat_at < now - 5min AND status != 'idle' → reset to idle
16
+ * Step 2: Merge locks with heartbeat_at < now - 3min → delete
17
+ */
18
+ function reconcileStaleWorkspaces(globalDb) {
19
+ const now = Date.now();
20
+ const slotCutoff = now - STALE_SLOT_TTL_MS;
21
+ const lockCutoff = now - STALE_LOCK_TTL_MS;
22
+ // Step 1: Find stale slots
23
+ const staleSlots = globalDb
24
+ .prepare(`SELECT id, task_id FROM workspace_pool_slots
25
+ WHERE heartbeat_at < ? AND status != 'idle'`)
26
+ .all(slotCutoff);
27
+ const taskIds = [];
28
+ for (const slot of staleSlots) {
29
+ globalDb
30
+ .prepare(`UPDATE workspace_pool_slots
31
+ SET status = 'idle', runner_id = NULL, task_id = NULL,
32
+ task_branch = NULL, starting_sha = NULL,
33
+ claimed_at = NULL, heartbeat_at = NULL
34
+ WHERE id = ?`)
35
+ .run(slot.id);
36
+ if (slot.task_id) {
37
+ taskIds.push(slot.task_id);
38
+ }
39
+ }
40
+ // Step 2: Delete stale merge locks
41
+ const lockResult = globalDb
42
+ .prepare('DELETE FROM workspace_merge_locks WHERE heartbeat_at < ?')
43
+ .run(lockCutoff);
44
+ return {
45
+ resetSlots: staleSlots.length,
46
+ deletedLocks: lockResult.changes,
47
+ taskIds,
48
+ };
49
+ }
50
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/workspace/reconcile.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAoBH,4DAyCC;AAxDD,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,mEAAmE;AAC7G,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,iEAAiE;AAQ3G;;;;;GAKG;AACH,SAAgB,wBAAwB,CAAC,QAA2B;IAClE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,GAAG,GAAG,iBAAiB,CAAC;IAC3C,MAAM,UAAU,GAAG,GAAG,GAAG,iBAAiB,CAAC;IAE3C,2BAA2B;IAC3B,MAAM,UAAU,GAAG,QAAQ;SACxB,OAAO,CACN;mDAC6C,CAC9C;SACA,GAAG,CAAC,UAAU,CAA4C,CAAC;IAE9D,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,QAAQ;aACL,OAAO,CACN;;;;sBAIc,CACf;aACA,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEhB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,mCAAmC;IACnC,MAAM,UAAU,GAAG,QAAQ;SACxB,OAAO,CAAC,0DAA0D,CAAC;SACnE,GAAG,CAAC,UAAU,CAAC,CAAC;IAEnB,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,YAAY,EAAE,UAAU,CAAC,OAAO;QAChC,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Workspace pool types — mirrors the workspace_pool_slots DB table.
3
+ */
4
+ import type Database from 'better-sqlite3';
5
+ export type SlotStatus = 'idle' | 'coder_active' | 'awaiting_review' | 'review_active' | 'merging';
6
+ export interface PoolSlot {
7
+ id: number;
8
+ project_id: string;
9
+ slot_index: number;
10
+ slot_path: string;
11
+ remote_url: string | null;
12
+ runner_id: string | null;
13
+ task_id: string | null;
14
+ base_branch: string | null;
15
+ task_branch: string | null;
16
+ starting_sha: string | null;
17
+ status: SlotStatus;
18
+ claimed_at: number | null;
19
+ heartbeat_at: number | null;
20
+ }
21
+ export interface PoolSlotContext {
22
+ globalDb: Database.Database;
23
+ slot: PoolSlot;
24
+ heartbeatTimer: ReturnType<typeof setInterval> | null;
25
+ localOnly: boolean;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workspace/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,cAAc,GACd,iBAAiB,GACjB,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,cAAc,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;CACpB"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace pool types — mirrors the workspace_pool_slots DB table.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/workspace/types.ts"],"names":[],"mappings":";AAAA;;GAEG"}
@@ -0,0 +1,10 @@
1
+ -- UP
2
+ -- Add workspace pool fields to tasks table
3
+ ALTER TABLE tasks ADD COLUMN conflict_count INTEGER NOT NULL DEFAULT 0;
4
+ ALTER TABLE tasks ADD COLUMN blocked_reason TEXT;
5
+ CREATE INDEX IF NOT EXISTS idx_tasks_conflict_count ON tasks(conflict_count) WHERE conflict_count > 0;
6
+
7
+ -- DOWN
8
+ -- SQLite doesn't support DROP COLUMN in older versions
9
+ -- ALTER TABLE tasks DROP COLUMN conflict_count;
10
+ -- ALTER TABLE tasks DROP COLUMN blocked_reason;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.2.1",
3
- "latestDbVersion": 19,
3
+ "latestDbVersion": 20,
4
4
  "migrations": [
5
5
  {
6
6
  "id": 1,
@@ -153,6 +153,14 @@
153
153
  "description": "Add start_commit_sha to tasks table to support End-State Squashing",
154
154
  "checksum": "",
155
155
  "cliVersion": "0.9.43"
156
+ },
157
+ {
158
+ "id": 20,
159
+ "name": "020_add_workspace_fields",
160
+ "file": "020_add_workspace_fields.sql",
161
+ "description": "Add conflict_count and blocked_reason to tasks for workspace pool lifecycle",
162
+ "checksum": "",
163
+ "cliVersion": "0.10.36"
156
164
  }
157
165
  ]
158
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steroids-cli",
3
- "version": "0.10.34",
3
+ "version": "0.10.36",
4
4
  "description": "Automated task execution system with coder/reviewer loop",
5
5
  "main": "dist/index.js",
6
6
  "bin": {