@coralai/sps-cli 0.49.0 → 0.49.1

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 (155) hide show
  1. package/dist/console-assets/assets/{index-C7skV1yt.js → index-Co0H5e5_.js} +14 -14
  2. package/dist/console-assets/index.html +1 -1
  3. package/package.json +1 -1
  4. package/dist/commands/cardMarkComplete.test.d.ts +0 -2
  5. package/dist/commands/cardMarkComplete.test.d.ts.map +0 -1
  6. package/dist/commands/cardMarkComplete.test.js +0 -142
  7. package/dist/commands/cardMarkComplete.test.js.map +0 -1
  8. package/dist/commands/cardMarkStarted.test.d.ts +0 -2
  9. package/dist/commands/cardMarkStarted.test.d.ts.map +0 -1
  10. package/dist/commands/cardMarkStarted.test.js +0 -98
  11. package/dist/commands/cardMarkStarted.test.js.map +0 -1
  12. package/dist/commands/projectInit.test.d.ts +0 -2
  13. package/dist/commands/projectInit.test.d.ts.map +0 -1
  14. package/dist/commands/projectInit.test.js +0 -126
  15. package/dist/commands/projectInit.test.js.map +0 -1
  16. package/dist/commands/tick-heartbeat.test.d.ts +0 -2
  17. package/dist/commands/tick-heartbeat.test.d.ts.map +0 -1
  18. package/dist/commands/tick-heartbeat.test.js +0 -89
  19. package/dist/commands/tick-heartbeat.test.js.map +0 -1
  20. package/dist/console-server/routes/chat.test.d.ts +0 -2
  21. package/dist/console-server/routes/chat.test.d.ts.map +0 -1
  22. package/dist/console-server/routes/chat.test.js +0 -131
  23. package/dist/console-server/routes/chat.test.js.map +0 -1
  24. package/dist/console-server/routes/projects.test.d.ts +0 -2
  25. package/dist/console-server/routes/projects.test.d.ts.map +0 -1
  26. package/dist/console-server/routes/projects.test.js +0 -150
  27. package/dist/console-server/routes/projects.test.js.map +0 -1
  28. package/dist/console-server/routes/system.test.d.ts +0 -2
  29. package/dist/console-server/routes/system.test.d.ts.map +0 -1
  30. package/dist/console-server/routes/system.test.js +0 -107
  31. package/dist/console-server/routes/system.test.js.map +0 -1
  32. package/dist/core/checklist.test.d.ts +0 -2
  33. package/dist/core/checklist.test.d.ts.map +0 -1
  34. package/dist/core/checklist.test.js +0 -74
  35. package/dist/core/checklist.test.js.map +0 -1
  36. package/dist/core/config.test.d.ts +0 -2
  37. package/dist/core/config.test.d.ts.map +0 -1
  38. package/dist/core/config.test.js +0 -352
  39. package/dist/core/config.test.js.map +0 -1
  40. package/dist/core/lock.test.d.ts +0 -2
  41. package/dist/core/lock.test.d.ts.map +0 -1
  42. package/dist/core/lock.test.js +0 -118
  43. package/dist/core/lock.test.js.map +0 -1
  44. package/dist/core/markerFile.test.d.ts +0 -2
  45. package/dist/core/markerFile.test.d.ts.map +0 -1
  46. package/dist/core/markerFile.test.js +0 -111
  47. package/dist/core/markerFile.test.js.map +0 -1
  48. package/dist/core/queue.test.d.ts +0 -2
  49. package/dist/core/queue.test.d.ts.map +0 -1
  50. package/dist/core/queue.test.js +0 -114
  51. package/dist/core/queue.test.js.map +0 -1
  52. package/dist/core/sessionCleanup.test.d.ts +0 -2
  53. package/dist/core/sessionCleanup.test.d.ts.map +0 -1
  54. package/dist/core/sessionCleanup.test.js +0 -158
  55. package/dist/core/sessionCleanup.test.js.map +0 -1
  56. package/dist/core/shellEnv.test.d.ts +0 -2
  57. package/dist/core/shellEnv.test.d.ts.map +0 -1
  58. package/dist/core/shellEnv.test.js +0 -116
  59. package/dist/core/shellEnv.test.js.map +0 -1
  60. package/dist/core/skillStore.test.d.ts +0 -2
  61. package/dist/core/skillStore.test.d.ts.map +0 -1
  62. package/dist/core/skillStore.test.js +0 -203
  63. package/dist/core/skillStore.test.js.map +0 -1
  64. package/dist/core/state.test.d.ts +0 -2
  65. package/dist/core/state.test.d.ts.map +0 -1
  66. package/dist/core/state.test.js +0 -336
  67. package/dist/core/state.test.js.map +0 -1
  68. package/dist/engines/CloseoutEngine.d.ts +0 -72
  69. package/dist/engines/CloseoutEngine.d.ts.map +0 -1
  70. package/dist/engines/CloseoutEngine.js +0 -648
  71. package/dist/engines/CloseoutEngine.js.map +0 -1
  72. package/dist/engines/EventHandler.test.d.ts +0 -2
  73. package/dist/engines/EventHandler.test.d.ts.map +0 -1
  74. package/dist/engines/EventHandler.test.js +0 -169
  75. package/dist/engines/EventHandler.test.js.map +0 -1
  76. package/dist/engines/ExecutionEngine.d.ts +0 -125
  77. package/dist/engines/ExecutionEngine.d.ts.map +0 -1
  78. package/dist/engines/ExecutionEngine.js +0 -766
  79. package/dist/engines/ExecutionEngine.js.map +0 -1
  80. package/dist/engines/MonitorEngine.test.d.ts +0 -2
  81. package/dist/engines/MonitorEngine.test.d.ts.map +0 -1
  82. package/dist/engines/MonitorEngine.test.js +0 -355
  83. package/dist/engines/MonitorEngine.test.js.map +0 -1
  84. package/dist/engines/engine-pipeline-adapter.test.d.ts +0 -17
  85. package/dist/engines/engine-pipeline-adapter.test.d.ts.map +0 -1
  86. package/dist/engines/engine-pipeline-adapter.test.js +0 -707
  87. package/dist/engines/engine-pipeline-adapter.test.js.map +0 -1
  88. package/dist/interfaces/HookProvider.d.ts +0 -9
  89. package/dist/interfaces/HookProvider.d.ts.map +0 -1
  90. package/dist/interfaces/HookProvider.js +0 -2
  91. package/dist/interfaces/HookProvider.js.map +0 -1
  92. package/dist/manager/completion-judge.test.d.ts +0 -17
  93. package/dist/manager/completion-judge.test.d.ts.map +0 -1
  94. package/dist/manager/completion-judge.test.js +0 -233
  95. package/dist/manager/completion-judge.test.js.map +0 -1
  96. package/dist/manager/integration-queue.d.ts +0 -71
  97. package/dist/manager/integration-queue.d.ts.map +0 -1
  98. package/dist/manager/integration-queue.js +0 -137
  99. package/dist/manager/integration-queue.js.map +0 -1
  100. package/dist/manager/integration-queue.test.d.ts +0 -17
  101. package/dist/manager/integration-queue.test.d.ts.map +0 -1
  102. package/dist/manager/integration-queue.test.js +0 -210
  103. package/dist/manager/integration-queue.test.js.map +0 -1
  104. package/dist/manager/pm-client.d.ts +0 -10
  105. package/dist/manager/pm-client.d.ts.map +0 -1
  106. package/dist/manager/pm-client.js +0 -260
  107. package/dist/manager/pm-client.js.map +0 -1
  108. package/dist/manager/resource-limiter.d.ts +0 -56
  109. package/dist/manager/resource-limiter.d.ts.map +0 -1
  110. package/dist/manager/resource-limiter.js +0 -116
  111. package/dist/manager/resource-limiter.js.map +0 -1
  112. package/dist/manager/resource-limiter.test.d.ts +0 -17
  113. package/dist/manager/resource-limiter.test.d.ts.map +0 -1
  114. package/dist/manager/resource-limiter.test.js +0 -118
  115. package/dist/manager/resource-limiter.test.js.map +0 -1
  116. package/dist/manager/supervisor.test.d.ts +0 -17
  117. package/dist/manager/supervisor.test.d.ts.map +0 -1
  118. package/dist/manager/supervisor.test.js +0 -216
  119. package/dist/manager/supervisor.test.js.map +0 -1
  120. package/dist/manager/worker-manager-impl.test.d.ts +0 -17
  121. package/dist/manager/worker-manager-impl.test.d.ts.map +0 -1
  122. package/dist/manager/worker-manager-impl.test.js +0 -446
  123. package/dist/manager/worker-manager-impl.test.js.map +0 -1
  124. package/dist/providers/PlaneTaskBackend.d.ts +0 -83
  125. package/dist/providers/PlaneTaskBackend.d.ts.map +0 -1
  126. package/dist/providers/PlaneTaskBackend.js +0 -461
  127. package/dist/providers/PlaneTaskBackend.js.map +0 -1
  128. package/dist/providers/TrelloTaskBackend.d.ts +0 -64
  129. package/dist/providers/TrelloTaskBackend.d.ts.map +0 -1
  130. package/dist/providers/TrelloTaskBackend.js +0 -298
  131. package/dist/providers/TrelloTaskBackend.js.map +0 -1
  132. package/dist/providers/adapters/acp-fs-handlers.test.d.ts +0 -2
  133. package/dist/providers/adapters/acp-fs-handlers.test.d.ts.map +0 -1
  134. package/dist/providers/adapters/acp-fs-handlers.test.js +0 -80
  135. package/dist/providers/adapters/acp-fs-handlers.test.js.map +0 -1
  136. package/dist/providers/adapters/acp-permissions.test.d.ts +0 -2
  137. package/dist/providers/adapters/acp-permissions.test.d.ts.map +0 -1
  138. package/dist/providers/adapters/acp-permissions.test.js +0 -103
  139. package/dist/providers/adapters/acp-permissions.test.js.map +0 -1
  140. package/dist/providers/adapters/acp-session-accumulator.test.d.ts +0 -2
  141. package/dist/providers/adapters/acp-session-accumulator.test.d.ts.map +0 -1
  142. package/dist/providers/adapters/acp-session-accumulator.test.js +0 -88
  143. package/dist/providers/adapters/acp-session-accumulator.test.js.map +0 -1
  144. package/dist/providers/adapters/acp-terminal-manager.test.d.ts +0 -2
  145. package/dist/providers/adapters/acp-terminal-manager.test.d.ts.map +0 -1
  146. package/dist/providers/adapters/acp-terminal-manager.test.js +0 -86
  147. package/dist/providers/adapters/acp-terminal-manager.test.js.map +0 -1
  148. package/dist/providers/outputParser.test.d.ts +0 -2
  149. package/dist/providers/outputParser.test.d.ts.map +0 -1
  150. package/dist/providers/outputParser.test.js +0 -185
  151. package/dist/providers/outputParser.test.js.map +0 -1
  152. package/dist/test-setup.d.ts +0 -2
  153. package/dist/test-setup.d.ts.map +0 -1
  154. package/dist/test-setup.js +0 -27
  155. package/dist/test-setup.js.map +0 -1
@@ -1,766 +0,0 @@
1
- import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { RuntimeStore } from '../core/runtimeStore.js';
4
- import { resolveGitlabProjectId, resolveWorkflowTransport } from '../core/config.js';
5
- import { resolveWorktreePath } from '../core/paths.js';
6
- import { readQueue } from '../core/queue.js';
7
- import { buildPhasePrompt, DEVELOPMENT_PROMPT_FILE, } from '../core/taskPrompts.js';
8
- import { Logger } from '../core/logger.js';
9
- const SKIP_LABELS = ['BLOCKED', 'NEEDS-FIX', 'CONFLICT', 'WAITING-CONFIRMATION', 'STALE-RUNTIME'];
10
- /** All labels that should be cleaned when a card re-enters the pipeline */
11
- const CLEANUP_LABELS = [...SKIP_LABELS, 'CLAIMED'];
12
- export class ExecutionEngine {
13
- ctx;
14
- taskBackend;
15
- repoBackend;
16
- workerManager;
17
- pipelineAdapter;
18
- notifier;
19
- log;
20
- runtimeStore;
21
- constructor(ctx, taskBackend, repoBackend, workerManager, pipelineAdapter, notifier) {
22
- this.ctx = ctx;
23
- this.taskBackend = taskBackend;
24
- this.repoBackend = repoBackend;
25
- this.workerManager = workerManager;
26
- this.pipelineAdapter = pipelineAdapter;
27
- this.notifier = notifier;
28
- this.log = new Logger('pipeline', ctx.projectName, ctx.paths.logsDir);
29
- this.runtimeStore = new RuntimeStore(ctx);
30
- }
31
- async tick(opts = {}) {
32
- const actions = [];
33
- const result = {
34
- project: this.ctx.projectName,
35
- component: 'pipeline',
36
- status: 'ok',
37
- exitCode: 0,
38
- actions,
39
- recommendedActions: [],
40
- details: {},
41
- };
42
- let actionsThisTick = 0;
43
- const maxActions = this.ctx.config.MAX_ACTIONS_PER_TICK;
44
- try {
45
- actions.push(...await this.reconcilePmStatesWithRuntime());
46
- // 1. Process Inprogress cards (detect completion → move to QA)
47
- // This runs first to free slots before launching new workers.
48
- // Completion detection does NOT consume action quota — it's a
49
- // prerequisite for freeing slots, not a new forward action.
50
- const inprogressCards = await this.listRuntimeAwareInprogressCards();
51
- for (const card of inprogressCards) {
52
- if (this.shouldSkip(card))
53
- continue;
54
- const checkResult = await this.checkInprogressCard(card, opts);
55
- if (checkResult) {
56
- actions.push(checkResult);
57
- // NOTE: intentionally not incrementing actionsThisTick here.
58
- // Completion detection frees slots for new launches and should
59
- // never block subsequent prepare/launch steps in the same tick.
60
- }
61
- }
62
- // 2. Process Backlog cards (prepare: branch + worktree + move to Todo)
63
- // Prepare does NOT consume launch quota — it only sets up the
64
- // environment. This allows prepare + launch to happen in a single tick.
65
- // However, we limit prepares to available capacity: only prepare as
66
- // many cards as there are idle slots + remaining launch quota. This
67
- // prevents cards piling up in Todo when workers can't launch.
68
- const backlogCards = await this.taskBackend.listByState(this.pipelineAdapter.states.backlog);
69
- const currentState = this.runtimeStore.readState();
70
- const idleSlots = Object.values(currentState.workers).filter(w => w.status === 'idle').length;
71
- const todoCards0 = await this.taskBackend.listByState(this.pipelineAdapter.states.ready);
72
- const todoCount = todoCards0.filter(c => !this.shouldSkip(c)).length;
73
- const prepareLimit = Math.max(0, idleSlots - todoCount);
74
- let preparedThisTick = 0;
75
- for (const card of backlogCards) {
76
- if (preparedThisTick >= prepareLimit)
77
- break;
78
- // Auto-clean auxiliary labels on Backlog cards — if a card was manually
79
- // moved back to Planning/Backlog, stale labels should not block it.
80
- await this.cleanAuxiliaryLabels(card);
81
- if (this.shouldSkip(card)) {
82
- actions.push({ action: 'skip', entity: `seq:${card.seq}`, result: 'skip', message: 'Has auxiliary state label' });
83
- continue;
84
- }
85
- const prepareResult = await this.prepareCard(card, opts);
86
- actions.push(prepareResult);
87
- if (prepareResult.result === 'ok')
88
- preparedThisTick++;
89
- }
90
- // 3. Process Todo cards (launch: claim + context + worker + move to Inprogress)
91
- // This is the only step that consumes action quota — it starts
92
- // resource-intensive AI workers that need system capacity.
93
- // Sort by pipeline_order to respect card priority (#5 skip bug fix).
94
- let todoCards = await this.taskBackend.listByState(this.pipelineAdapter.states.ready);
95
- const pipelineOrder = readQueue(this.ctx.paths.pipelineOrderFile);
96
- if (pipelineOrder.length > 0) {
97
- todoCards = todoCards.sort((a, b) => {
98
- const aIdx = pipelineOrder.indexOf(parseInt(a.seq, 10));
99
- const bIdx = pipelineOrder.indexOf(parseInt(b.seq, 10));
100
- // Cards in pipeline_order come first, in order; others after
101
- if (aIdx >= 0 && bIdx >= 0)
102
- return aIdx - bIdx;
103
- if (aIdx >= 0)
104
- return -1;
105
- if (bIdx >= 0)
106
- return 1;
107
- return parseInt(a.seq, 10) - parseInt(b.seq, 10);
108
- });
109
- }
110
- let launchedThisTick = 0;
111
- const failedSlots = new Set(); // track slots that failed launch this tick
112
- for (const card of todoCards) {
113
- if (actionsThisTick >= maxActions)
114
- break;
115
- if (this.shouldSkip(card)) {
116
- actions.push({ action: 'skip', entity: `seq:${card.seq}`, result: 'skip', message: 'Has auxiliary state label' });
117
- continue;
118
- }
119
- // Stagger is handled by ResourceLimiter.enforceStagger() inside launchCard
120
- const launchResult = await this.launchCard(card, opts, failedSlots);
121
- actions.push(launchResult);
122
- if (launchResult.result === 'ok') {
123
- actionsThisTick++;
124
- launchedThisTick++;
125
- }
126
- }
127
- }
128
- catch (err) {
129
- const msg = err instanceof Error ? err.message : String(err);
130
- this.log.error(`Pipeline tick failed: ${msg}`);
131
- result.status = 'fail';
132
- result.exitCode = 1;
133
- result.details = { error: msg };
134
- }
135
- // Check for any failures
136
- if (actions.some((a) => a.result === 'fail') && result.status === 'ok') {
137
- result.status = 'fail';
138
- result.exitCode = 1;
139
- }
140
- return result;
141
- }
142
- /**
143
- * Launch a single card (for `sps worker launch <project> <seq>`).
144
- * Assumes card is in Todo state with branch/worktree already prepared.
145
- */
146
- async launchSingle(seq, opts = {}) {
147
- const result = {
148
- project: this.ctx.projectName,
149
- component: 'worker-launch',
150
- status: 'ok',
151
- exitCode: 0,
152
- actions: [],
153
- recommendedActions: [],
154
- details: {},
155
- };
156
- const card = await this.taskBackend.getBySeq(seq);
157
- if (!card) {
158
- result.status = 'fail';
159
- result.exitCode = 1;
160
- result.details = { error: `Card seq:${seq} not found` };
161
- return result;
162
- }
163
- // If card is in Backlog, do prepare first
164
- if (card.state === this.pipelineAdapter.states.backlog) {
165
- const prepareAction = await this.prepareCard(card, opts);
166
- result.actions.push(prepareAction);
167
- if (prepareAction.result === 'fail') {
168
- result.status = 'fail';
169
- result.exitCode = 1;
170
- return result;
171
- }
172
- // Reload card after prepare
173
- const updated = await this.taskBackend.getBySeq(seq);
174
- if (!updated || updated.state !== this.pipelineAdapter.states.ready) {
175
- result.status = 'fail';
176
- result.exitCode = 1;
177
- result.details = { error: `Card not in ${this.pipelineAdapter.states.ready} after prepare` };
178
- return result;
179
- }
180
- }
181
- if (card.state !== this.pipelineAdapter.states.ready && card.state !== this.pipelineAdapter.states.backlog) {
182
- result.status = 'fail';
183
- result.exitCode = 2;
184
- result.details = { error: `Card seq:${seq} is in ${card.state}, expected ${this.pipelineAdapter.states.backlog} or ${this.pipelineAdapter.states.ready}` };
185
- return result;
186
- }
187
- const launchAction = await this.launchCard(card, opts);
188
- result.actions.push(launchAction);
189
- if (launchAction.result === 'fail') {
190
- result.status = 'fail';
191
- result.exitCode = 1;
192
- }
193
- return result;
194
- }
195
- shouldSkip(card) {
196
- return SKIP_LABELS.some((label) => card.labels.includes(label));
197
- }
198
- async listRuntimeAwareInprogressCards() {
199
- const cards = await this.taskBackend.listByState(this.pipelineAdapter.states.active);
200
- const bySeq = new Map(cards.map(card => [card.seq, card]));
201
- const state = this.runtimeStore.readState();
202
- for (const [seq, lease] of Object.entries(state.leases)) {
203
- const slot = lease.slot ? state.workers[lease.slot] || null : null;
204
- if (this.derivePmStateFromLease(lease, slot) !== this.pipelineAdapter.states.active || bySeq.has(seq))
205
- continue;
206
- const card = await this.taskBackend.getBySeq(seq);
207
- if (card)
208
- bySeq.set(seq, card);
209
- }
210
- return Array.from(bySeq.values()).sort((a, b) => parseInt(a.seq, 10) - parseInt(b.seq, 10));
211
- }
212
- async reconcilePmStatesWithRuntime() {
213
- const state = this.runtimeStore.readState();
214
- const actions = [];
215
- for (const [seq, lease] of Object.entries(state.leases)) {
216
- const slot = lease.slot ? state.workers[lease.slot] || null : null;
217
- const targetState = this.derivePmStateFromLease(lease, slot);
218
- if (!targetState)
219
- continue;
220
- const card = await this.taskBackend.getBySeq(seq);
221
- if (!card || card.state === targetState)
222
- continue;
223
- try {
224
- await this.taskBackend.move(seq, targetState);
225
- this.log.info(`Reconciled seq ${seq} ${card.state} → ${targetState} to match runtime state`);
226
- actions.push({
227
- action: 'pm-reconcile',
228
- entity: `seq:${seq}`,
229
- result: 'ok',
230
- message: `${card.state} → ${targetState}`,
231
- });
232
- }
233
- catch (err) {
234
- const msg = err instanceof Error ? err.message : String(err);
235
- this.log.warn(`Failed to reconcile PM state for seq ${seq}: ${msg}`);
236
- actions.push({
237
- action: 'pm-reconcile',
238
- entity: `seq:${seq}`,
239
- result: 'skip',
240
- message: msg,
241
- });
242
- }
243
- }
244
- return actions;
245
- }
246
- derivePmStateFromLease(lease, slot) {
247
- const s = this.pipelineAdapter.states;
248
- if (lease.phase === 'queued' || lease.phase === 'preparing') {
249
- return s.ready;
250
- }
251
- if (lease.phase === 'coding') {
252
- return s.active;
253
- }
254
- if (lease.phase === 'waiting_confirmation'
255
- && lease.pmStateObserved !== s.review
256
- && slot?.status !== 'merging'
257
- && slot?.status !== 'resolving') {
258
- return s.active;
259
- }
260
- if (['merging', 'resolving_conflict', 'closing'].includes(lease.phase)) {
261
- return s.review;
262
- }
263
- if (lease.phase === 'waiting_confirmation' && lease.pmStateObserved === s.review) {
264
- return s.review;
265
- }
266
- return null;
267
- }
268
- isRuntimeOwnedSlot(slot) {
269
- return !!slot && slot.status !== 'idle';
270
- }
271
- /**
272
- * Remove auxiliary state labels (STALE-RUNTIME, NEEDS-FIX, etc.) from a card.
273
- * Called when a card re-enters Backlog — indicates human intent to retry,
274
- * so stale labels from previous runs should not block it.
275
- */
276
- async cleanAuxiliaryLabels(card) {
277
- for (const label of CLEANUP_LABELS) {
278
- if (card.labels.includes(label)) {
279
- try {
280
- await this.taskBackend.removeLabel(card.seq, label);
281
- card.labels = card.labels.filter(l => l !== label);
282
- this.log.ok(`Removed stale label "${label}" from seq ${card.seq}`);
283
- }
284
- catch {
285
- this.log.warn(`Failed to remove label "${label}" from seq ${card.seq}`);
286
- }
287
- }
288
- }
289
- }
290
- // ─── Inprogress Phase (detect completion → Done) ────────────────
291
- /**
292
- * Check an Inprogress card: verify worker is still running or handled by exit callback.
293
- *
294
- * The Supervisor exit callback triggers CompletionJudge → PostActions automatically,
295
- * so this method only needs to:
296
- * - Update heartbeat if worker is still running
297
- * - Confirm completion if PostActions already processed it
298
- */
299
- async checkInprogressCard(card, opts) {
300
- const seq = card.seq;
301
- const state = this.runtimeStore.readState();
302
- const lease = state.leases[seq] || null;
303
- const slotName = this.findRuntimeSlotName(state, seq, lease);
304
- if (!slotName) {
305
- // Slot already released (PostActions handled it via exit callback)
306
- return null;
307
- }
308
- // Use WorkerManager.inspect() to check worker state
309
- const snapshots = this.workerManager.inspect({ project: this.ctx.projectName, taskId: seq });
310
- const snapshot = snapshots[0];
311
- if (snapshot && (snapshot.state === 'running' || snapshot.state === 'starting')) {
312
- // Worker still running — update heartbeat
313
- try {
314
- this.runtimeStore.updateState('pipeline-heartbeat', (freshState) => {
315
- if (freshState.workers[slotName]) {
316
- freshState.workers[slotName].lastHeartbeat = new Date().toISOString();
317
- }
318
- });
319
- }
320
- catch { /* non-fatal */ }
321
- return null;
322
- }
323
- if (snapshot && (snapshot.state === 'waiting_input' || snapshot.state === 'needs_confirmation')) {
324
- // Worker waiting for input — log and wait
325
- this.log.info(`seq ${seq}: worker in state ${snapshot.state}`);
326
- return null;
327
- }
328
- if (snapshot && snapshot.state === 'completed') {
329
- // WM exit callback handled completion
330
- this.log.ok(`seq ${seq}: Completed (handled by WM exit callback)`);
331
- return { action: 'complete', entity: `seq:${seq}`, result: 'ok', message: 'Completed via WM exit callback' };
332
- }
333
- if (snapshot && snapshot.state === 'failed') {
334
- // WM exit callback handled failure
335
- this.log.info(`seq ${seq}: Failed (handled by WM exit callback)`);
336
- return { action: 'complete', entity: `seq:${seq}`, result: 'fail', message: 'Failed via WM exit callback' };
337
- }
338
- // No snapshot found — WM already processed and released the slot
339
- const freshState = this.runtimeStore.readState();
340
- if (!freshState.workers[slotName] || freshState.workers[slotName].status === 'idle') {
341
- this.log.ok(`seq ${seq}: Completed (WM already processed)`);
342
- return { action: 'complete', entity: `seq:${seq}`, result: 'ok', message: 'Completed (WM processed)' };
343
- }
344
- // Still active in state but no snapshot — MonitorEngine/Recovery handles
345
- return null;
346
- }
347
- // ─── Prepare Phase (Backlog → Todo) ─────────────────────────────
348
- /**
349
- * Prepare a Backlog card: create branch, create worktree, move to Todo.
350
- * Steps 1-3 per 01 §4.3.
351
- */
352
- async prepareCard(card, opts) {
353
- const seq = card.seq;
354
- const branchName = this.buildBranchName(card);
355
- const worktreePath = resolveWorktreePath(this.ctx.projectName, seq, this.ctx.config.WORKTREE_DIR);
356
- if (opts.dryRun) {
357
- this.log.info(`[dry-run] Would prepare seq ${seq}: branch=${branchName} worktree=${worktreePath}`);
358
- return { action: 'prepare', entity: `seq:${seq}`, result: 'ok', message: 'dry-run' };
359
- }
360
- // Step 1: Create branch
361
- try {
362
- await this.repoBackend.ensureBranch(this.ctx.paths.repoDir, branchName, this.ctx.mergeBranch);
363
- this.log.ok(`Step 1: Branch ${branchName} created for seq ${seq}`);
364
- }
365
- catch (err) {
366
- const msg = err instanceof Error ? err.message : String(err);
367
- this.log.error(`Step 1 failed (branch) for seq ${seq}: ${msg}`);
368
- this.logEvent('prepare-branch', seq, 'fail', { error: msg });
369
- return { action: 'prepare', entity: `seq:${seq}`, result: 'fail', message: `Branch creation failed: ${msg}` };
370
- }
371
- // Step 2: Create worktree
372
- try {
373
- await this.repoBackend.ensureWorktree(this.ctx.paths.repoDir, branchName, worktreePath);
374
- this.log.ok(`Step 2: Worktree created for seq ${seq} at ${worktreePath}`);
375
- }
376
- catch (err) {
377
- const msg = err instanceof Error ? err.message : String(err);
378
- this.log.error(`Step 2 failed (worktree) for seq ${seq}: ${msg}`);
379
- this.logEvent('prepare-worktree', seq, 'fail', { error: msg });
380
- // Rollback: cleanup branch (best effort, branch may have existed before)
381
- return { action: 'prepare', entity: `seq:${seq}`, result: 'fail', message: `Worktree creation failed: ${msg}` };
382
- }
383
- // Step 3: Move card to Todo
384
- try {
385
- await this.taskBackend.move(seq, this.pipelineAdapter.states.ready);
386
- this.log.ok(`Step 3: Moved seq ${seq} ${this.pipelineAdapter.states.backlog} → ${this.pipelineAdapter.states.ready}`);
387
- this.logEvent('prepare', seq, 'ok');
388
- if (this.notifier) {
389
- await this.notifier.send(`ℹ️ [${this.ctx.projectName}] seq:${seq} environment ready (${this.pipelineAdapter.states.backlog} → ${this.pipelineAdapter.states.ready})`).catch(() => { });
390
- }
391
- return { action: 'prepare', entity: `seq:${seq}`, result: 'ok', message: `${this.pipelineAdapter.states.backlog} → ${this.pipelineAdapter.states.ready}` };
392
- }
393
- catch (err) {
394
- const msg = err instanceof Error ? err.message : String(err);
395
- this.log.error(`Step 3 failed (move) for seq ${seq}: ${msg}`);
396
- this.logEvent('prepare-move', seq, 'fail', { error: msg });
397
- // Rollback: cleanup branch + worktree would be ideal but risky; log for manual cleanup
398
- return { action: 'prepare', entity: `seq:${seq}`, result: 'fail', message: `Move to ${this.pipelineAdapter.states.ready} failed: ${msg}` };
399
- }
400
- }
401
- // ─── Launch Phase (Todo → Inprogress) ────────────────────────────
402
- /**
403
- * Launch a Todo card: claim slot, build context, start worker, move to Inprogress.
404
- * Steps 4-7 per 01 §4.3.
405
- */
406
- async launchCard(card, opts, failedSlots = new Set()) {
407
- const seq = card.seq;
408
- const branchName = this.buildBranchName(card);
409
- const worktreePath = resolveWorktreePath(this.ctx.projectName, seq, this.ctx.config.WORKTREE_DIR);
410
- const workflowTransport = resolveWorkflowTransport(this.ctx.config);
411
- if (opts.dryRun) {
412
- this.log.info(`[dry-run] Would launch seq ${seq}`);
413
- return { action: 'launch', entity: `seq:${seq}`, result: 'ok', message: 'dry-run' };
414
- }
415
- // Step 5: PM claim (kept in Engine — PM backend awareness)
416
- try {
417
- await this.taskBackend.claim(seq, `pending-wm`);
418
- }
419
- catch (err) {
420
- const msg = err instanceof Error ? err.message : String(err);
421
- this.log.warn(`PM claim for seq ${seq} failed (non-fatal): ${msg}`);
422
- }
423
- // Step 5b: Build development prompt (in-memory, archive to .sps/)
424
- let prompt;
425
- try {
426
- prompt = this.buildDevelopmentPrompt(card, worktreePath);
427
- this.log.ok(`Step 5: Task context built for seq ${seq}`);
428
- }
429
- catch (err) {
430
- const msg = err instanceof Error ? err.message : String(err);
431
- this.log.error(`Step 5 failed (context) for seq ${seq}: ${msg}`);
432
- this.logEvent('launch-context', seq, 'fail', { error: msg });
433
- return { action: 'launch', entity: `seq:${seq}`, result: 'fail', message: `Context build failed: ${msg}` };
434
- }
435
- // Step 6: Launch worker via WorkerManager.run()
436
- const logsDir = this.ctx.config.raw.LOGS_DIR || `/tmp/sps-${this.ctx.projectName}`;
437
- const runRequest = {
438
- taskId: String(card.seq),
439
- cardId: String(card.seq),
440
- project: this.ctx.projectName,
441
- phase: 'development',
442
- prompt,
443
- cwd: worktreePath,
444
- branch: branchName,
445
- targetBranch: this.ctx.mergeBranch,
446
- tool: (this.pipelineAdapter.developStage.agent || this.ctx.config.WORKER_TOOL),
447
- transport: 'acp-sdk',
448
- outputFile: resolve(logsDir, `${this.ctx.projectName}-worker-${card.seq}-${Date.now()}.jsonl`),
449
- timeoutSec: this.ctx.config.WORKER_LAUNCH_TIMEOUT_S,
450
- maxRetries: this.ctx.config.WORKER_RESTART_LIMIT,
451
- completionStrategy: this.pipelineAdapter.developStage.completion,
452
- };
453
- let response;
454
- try {
455
- response = await this.workerManager.run(runRequest);
456
- }
457
- catch (err) {
458
- const msg = err instanceof Error ? err.message : String(err);
459
- this.log.error(`Step 6 failed (WM.run) for seq ${seq}: ${msg}`);
460
- failedSlots.add(`wm-error-${seq}`);
461
- this.logEvent('launch-worker', seq, 'fail', { error: msg });
462
- return { action: 'launch', entity: `seq:${seq}`, result: 'fail', message: `Worker launch failed: ${msg}` };
463
- }
464
- if (!response.accepted) {
465
- this.log.warn(`WM rejected seq ${seq}: ${response.rejectReason}`);
466
- return {
467
- action: 'launch',
468
- entity: `seq:${seq}`,
469
- result: response.rejectReason === 'resource_exhausted' ? 'skip' : 'fail',
470
- message: `WM rejected: ${response.rejectReason}`,
471
- };
472
- }
473
- const slotName = response.slot;
474
- this.log.ok(`Step 6: WM launched worker for seq ${seq} (slot=${slotName}, pid=${response.pid ?? 'n/a'})`);
475
- if (this.notifier) {
476
- await this.notifier.send(`▶️ [${this.ctx.projectName}] seq:${seq} worker started (${slotName})`).catch(() => { });
477
- }
478
- // Step 7: Move card to Inprogress
479
- try {
480
- await this.taskBackend.move(seq, this.pipelineAdapter.states.active);
481
- // Update active card state
482
- this.runtimeStore.updateState('pipeline-launch', (freshState) => {
483
- if (freshState.activeCards[seq]) {
484
- freshState.activeCards[seq].state = this.pipelineAdapter.states.active;
485
- if (freshState.leases[seq]) {
486
- freshState.leases[seq].pmStateObserved = this.pipelineAdapter.states.active;
487
- if (freshState.leases[seq].phase === 'preparing' || freshState.leases[seq].phase === 'queued') {
488
- freshState.leases[seq].phase = 'coding';
489
- }
490
- freshState.leases[seq].lastTransitionAt = new Date().toISOString();
491
- }
492
- }
493
- });
494
- this.log.ok(`Step 7: Moved seq ${seq} ${this.pipelineAdapter.states.ready} → ${this.pipelineAdapter.states.active}`);
495
- this.logEvent('launch', seq, 'ok', { worker: slotName });
496
- return { action: 'launch', entity: `seq:${seq}`, result: 'ok', message: `${this.pipelineAdapter.states.ready} → ${this.pipelineAdapter.states.active} (${slotName})` };
497
- }
498
- catch (err) {
499
- const msg = err instanceof Error ? err.message : String(err);
500
- this.log.error(`Step 7 failed (move) for seq ${seq}: ${msg}`);
501
- // Rollback: cancel worker via WM (handles kill + resource release)
502
- try {
503
- await this.workerManager.cancel({ taskId: String(card.seq), project: this.ctx.projectName, reason: 'anomaly' });
504
- }
505
- catch { /* best effort */ }
506
- this.releaseSlot(slotName, seq);
507
- this.logEvent('launch-move', seq, 'fail', { error: msg });
508
- return { action: 'launch', entity: `seq:${seq}`, result: 'fail', message: `Move to ${this.pipelineAdapter.states.active} failed: ${msg}` };
509
- }
510
- }
511
- /**
512
- * @deprecated Phase 1 transitional — WM's internal exit callback handles ACP inspection.
513
- * Kept for edge-case fallback; will be removed when WM fully owns ACP lifecycle.
514
- */
515
- async checkAcpInprogressCard(card, slotName) {
516
- const seq = card.seq;
517
- const state = this.runtimeStore.readState();
518
- const slot = state.workers[slotName];
519
- if (!slot)
520
- return null;
521
- // Use WorkerManager.inspect() for normalized worker state
522
- const snapshots = this.workerManager.inspect({ project: this.ctx.projectName, taskId: seq });
523
- const snapshot = snapshots[0];
524
- if (snapshot && (snapshot.state === 'running' || snapshot.state === 'starting')) {
525
- // Worker still active — update heartbeat
526
- this.runtimeStore.updateState('pipeline-acp-heartbeat', (freshState) => {
527
- const freshSlot = freshState.workers[slotName];
528
- if (freshSlot) {
529
- freshSlot.lastHeartbeat = new Date().toISOString();
530
- }
531
- });
532
- return null;
533
- }
534
- if (snapshot && snapshot.state === 'waiting_input') {
535
- this.log.info(`seq ${seq}: worker waiting for input`);
536
- return null;
537
- }
538
- if (snapshot && snapshot.state === 'needs_confirmation') {
539
- this.log.warn(`seq ${seq}: worker needs confirmation`);
540
- return null;
541
- }
542
- if (snapshot && snapshot.state === 'completed') {
543
- this.log.ok(`seq ${seq}: ACP run completed (via WM)`);
544
- return {
545
- action: 'complete',
546
- entity: `seq:${seq}`,
547
- result: 'ok',
548
- message: `${resolveWorkflowTransport(this.ctx.config).toUpperCase()} run completed`,
549
- };
550
- }
551
- if (snapshot && snapshot.state === 'failed') {
552
- this.log.info(`seq ${seq}: ACP run failed (via WM)`);
553
- return {
554
- action: 'complete',
555
- entity: `seq:${seq}`,
556
- result: 'fail',
557
- message: `${resolveWorkflowTransport(this.ctx.config).toUpperCase()} run failed`,
558
- };
559
- }
560
- // No snapshot — session lost or already cleaned up
561
- this.runtimeStore.updateState('pipeline-acp-lost', (freshState) => {
562
- const lostSlot = freshState.workers[slotName];
563
- if (lostSlot) {
564
- lostSlot.sessionState = 'offline';
565
- lostSlot.remoteStatus = 'lost';
566
- lostSlot.lastEventAt = new Date().toISOString();
567
- lostSlot.lastHeartbeat = new Date().toISOString();
568
- }
569
- });
570
- this.log.warn(`seq ${seq}: ACP session lost — no WM snapshot found`);
571
- return {
572
- action: 'complete',
573
- entity: `seq:${seq}`,
574
- result: 'fail',
575
- message: 'ACP session lost',
576
- };
577
- }
578
- findRuntimeSlotName(state, seq, lease) {
579
- if (lease?.slot && state.workers[lease.slot])
580
- return lease.slot;
581
- const slotEntry = Object.entries(state.workers).find(([, worker]) => worker.seq === parseInt(seq, 10) && worker.status !== 'idle');
582
- return slotEntry?.[0] || null;
583
- }
584
- getRetryCount(state, seq) {
585
- return state.leases[seq]?.retryCount ?? state.activeCards[seq]?.retryCount ?? 0;
586
- }
587
- // ─── Helpers ─────────────────────────────────────────────────────
588
- /**
589
- * Build branch name from card: feature/<seq>-<slug>
590
- */
591
- buildBranchName(card) {
592
- const slug = card.name
593
- .toLowerCase()
594
- .replace(/[^a-z0-9]+/g, '-')
595
- .replace(/^-|-$/g, '')
596
- .slice(0, 40);
597
- return `feature/${card.seq}-${slug}`;
598
- }
599
- /**
600
- * Write task-specific prompt to worktree.
601
- *
602
- * CLAUDE.md and AGENTS.md are managed by `sps doctor --fix` and committed
603
- * to the repo — worktrees inherit them automatically via git.
604
- *
605
- * The prompt file includes the project rules (from CLAUDE.md) followed by
606
- * the task-specific details. This ensures that when a session is reused
607
- * (WORKER_SESSION_REUSE=true), the worker always receives the latest
608
- * project rules via the prompt — even though /clear + cd does not
609
- * trigger Claude/Codex to re-read CLAUDE.md from disk.
610
- */
611
- /**
612
- * Build the shared prompt context used by both development and integration phases.
613
- * Loads skill profiles, project rules, and knowledge from the worktree.
614
- */
615
- buildPromptContext(card, worktreePath) {
616
- const branchName = this.buildBranchName(card);
617
- const skillContent = this.loadSkillProfiles(card);
618
- const claudeMdPath = resolve(worktreePath, 'CLAUDE.md');
619
- const agentsMdPath = resolve(worktreePath, 'AGENTS.md');
620
- const workerTool = this.pipelineAdapter.developStage.agent || this.ctx.config.WORKER_TOOL;
621
- let projectRules = '';
622
- if (existsSync(claudeMdPath)) {
623
- projectRules = readFileSync(claudeMdPath, 'utf-8').trim();
624
- }
625
- if (existsSync(agentsMdPath)) {
626
- const agentsRules = readFileSync(agentsMdPath, 'utf-8').trim();
627
- projectRules = projectRules ? `${projectRules}\n\n${agentsRules}` : agentsRules;
628
- }
629
- if (!projectRules) {
630
- const expectedFile = workerTool === 'codex' ? 'AGENTS.md' : 'CLAUDE.md';
631
- this.log.warn(`${expectedFile} not found in worktree — run: sps doctor ${this.ctx.projectName} --fix`);
632
- }
633
- const knowledge = this.loadProjectKnowledge(worktreePath);
634
- return {
635
- taskSeq: card.seq,
636
- taskTitle: card.name,
637
- taskDescription: card.desc || '(no description)',
638
- cardId: card.id,
639
- worktreePath,
640
- branchName,
641
- targetBranch: this.ctx.mergeBranch,
642
- mergeMode: this.ctx.mrMode,
643
- gitlabProjectId: resolveGitlabProjectId(this.ctx.config),
644
- skillContent,
645
- projectRules,
646
- knowledge,
647
- };
648
- }
649
- /**
650
- * Generate development prompt and archive to .sps/ for debugging.
651
- * Returns the prompt string directly — no disk file dependency.
652
- */
653
- buildDevelopmentPrompt(card, worktreePath) {
654
- const ctx = this.buildPromptContext(card, worktreePath);
655
- const prompt = buildPhasePrompt({ ...ctx, phase: 'development' });
656
- // Archive to .sps/ for debugging (non-blocking)
657
- try {
658
- const spsDir = resolve(worktreePath, '.sps');
659
- if (!existsSync(spsDir))
660
- mkdirSync(spsDir, { recursive: true });
661
- writeFileSync(resolve(spsDir, DEVELOPMENT_PROMPT_FILE), prompt);
662
- }
663
- catch { /* archive failure should never block worker launch */ }
664
- return prompt;
665
- }
666
- /**
667
- * Release a worker slot and remove card from active cards.
668
- * Used for launch failure rollback.
669
- */
670
- releaseSlot(slotName, seq) {
671
- try {
672
- this.runtimeStore.updateState('pipeline-release', (state) => {
673
- this.runtimeStore.releaseTaskProjection(state, seq, { dropLease: true });
674
- });
675
- this.taskBackend.releaseClaim(seq).catch(() => { });
676
- }
677
- catch {
678
- this.log.warn(`Failed to release slot ${slotName} for seq ${seq}`);
679
- }
680
- }
681
- // ─── Skill Profile Loading (label-driven) ─────────────────────
682
- /**
683
- * Load skill profiles based on card labels (skill:xxx) or project default.
684
- * Returns combined profile content for prompt injection.
685
- */
686
- loadSkillProfiles(card) {
687
- // 1. Extract skill:xxx labels from card
688
- let skills = card.labels
689
- .filter(l => l.startsWith('skill:'))
690
- .map(l => l.slice('skill:'.length));
691
- // 2. Stage-level profile from pipeline YAML (overrides project default)
692
- if (skills.length === 0 && this.pipelineAdapter.developStage.profile) {
693
- skills = this.pipelineAdapter.developStage.profile.split(',').map(s => s.trim()).filter(Boolean);
694
- }
695
- // 3. Fallback to project default
696
- if (skills.length === 0) {
697
- const defaultSkills = this.ctx.config.raw.DEFAULT_WORKER_SKILLS;
698
- if (defaultSkills) {
699
- skills = defaultSkills.split(',').map(s => s.trim()).filter(Boolean);
700
- }
701
- }
702
- if (skills.length === 0)
703
- return '';
704
- // Don't inject full profile content — agent loads skills on demand
705
- // via ~/.claude/skills/ or ~/.codex/skills/ (symlinked from ~/.coral/skills/).
706
- // Just tell the agent which skills to activate.
707
- this.log.ok(`Skill labels: ${skills.join(', ')}`);
708
- return `# Required Skills\n\nThis task requires the following skills: ${skills.join(', ')}.\nLoad the dev-worker skill and read the corresponding references.`;
709
- }
710
- // ─── Project Knowledge Loading (truncated) ────────────────────
711
- /**
712
- * Load recent project knowledge from docs/DECISIONS.md and docs/CHANGELOG.md.
713
- * Truncates to recent entries to keep prompt size manageable.
714
- */
715
- loadProjectKnowledge(worktreePath) {
716
- const sections = ['# Project Knowledge (from previous tasks)'];
717
- let hasContent = false;
718
- // Recent decisions (last 10 sections)
719
- const decisionsPath = resolve(worktreePath, 'docs', 'DECISIONS.md');
720
- if (existsSync(decisionsPath)) {
721
- const content = readFileSync(decisionsPath, 'utf-8');
722
- const recent = this.extractRecentSections(content, 10);
723
- if (recent) {
724
- sections.push('## Recent Decisions\n' + recent);
725
- hasContent = true;
726
- }
727
- }
728
- // Recent changelog (last 5 sections)
729
- const changelogPath = resolve(worktreePath, 'docs', 'CHANGELOG.md');
730
- if (existsSync(changelogPath)) {
731
- const content = readFileSync(changelogPath, 'utf-8');
732
- const recent = this.extractRecentSections(content, 5);
733
- if (recent) {
734
- sections.push('## Recent Changes\n' + recent);
735
- hasContent = true;
736
- }
737
- }
738
- return hasContent ? sections.join('\n\n') : '';
739
- }
740
- /**
741
- * Extract the last N ## sections from a markdown file.
742
- */
743
- extractRecentSections(content, maxSections) {
744
- const lines = content.split('\n');
745
- const sectionStarts = [];
746
- for (let i = 0; i < lines.length; i++) {
747
- if (lines[i].startsWith('## ')) {
748
- sectionStarts.push(i);
749
- }
750
- }
751
- if (sectionStarts.length === 0)
752
- return content.trim();
753
- const start = sectionStarts[Math.max(0, sectionStarts.length - maxSections)];
754
- return lines.slice(start).join('\n').trim();
755
- }
756
- logEvent(action, seq, result, meta) {
757
- this.log.event({
758
- component: 'pipeline',
759
- action,
760
- entity: `seq:${seq}`,
761
- result,
762
- meta,
763
- });
764
- }
765
- }
766
- //# sourceMappingURL=ExecutionEngine.js.map