@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,648 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { resolveWorkflowTransport, resolveGitlabProjectId } from '../core/config.js';
5
- import { IntegrationQueue } from '../manager/integration-queue.js';
6
- import { buildPhasePrompt, INTEGRATION_PROMPT_FILE } from '../core/taskPrompts.js';
7
- import { branchPushed, branchCommitsAhead } from '../providers/outputParser.js';
8
- import { RuntimeStore } from '../core/runtimeStore.js';
9
- import { resolveWorktreePath } from '../core/paths.js';
10
- import { Logger } from '../core/logger.js';
11
- /**
12
- * CloseoutEngine handles the QA → Done pipeline.
13
- *
14
- * In the worker-owned two-phase model, QA means integration:
15
- * - the worker performs rebase / merge / conflict resolution
16
- * - SPS only checks evidence, starts or resumes the integration worker,
17
- * and finalizes the task after the branch is merged.
18
- */
19
- export class CloseoutEngine {
20
- ctx;
21
- taskBackend;
22
- repoBackend;
23
- workerManager;
24
- pipelineAdapter;
25
- notifier;
26
- log;
27
- runtimeStore;
28
- constructor(ctx, taskBackend, repoBackend, workerManager, pipelineAdapter, notifier) {
29
- this.ctx = ctx;
30
- this.taskBackend = taskBackend;
31
- this.repoBackend = repoBackend;
32
- this.workerManager = workerManager;
33
- this.pipelineAdapter = pipelineAdapter;
34
- this.notifier = notifier;
35
- this.log = new Logger('qa', ctx.projectName, ctx.paths.logsDir);
36
- this.runtimeStore = new RuntimeStore(ctx);
37
- }
38
- async tick() {
39
- const actions = [];
40
- const recommendedActions = [];
41
- const result = {
42
- project: this.ctx.projectName,
43
- component: 'qa',
44
- status: 'ok',
45
- exitCode: 0,
46
- actions,
47
- recommendedActions,
48
- details: {},
49
- };
50
- try {
51
- const qaCards = await this.taskBackend.listByState(this.pipelineAdapter.states.review);
52
- if (qaCards.length === 0) {
53
- this.log.info('No QA cards to process');
54
- result.details = { reason: 'no_qa_cards' };
55
- }
56
- else {
57
- this.log.info(`Processing ${qaCards.length} QA card(s)`);
58
- for (const card of qaCards) {
59
- // Skip cards with BLOCKED label
60
- if (card.labels.includes('BLOCKED')) {
61
- this.log.debug(`Skipping seq ${card.seq}: BLOCKED`);
62
- actions.push({
63
- action: 'skip',
64
- entity: `seq:${card.seq}`,
65
- result: 'skip',
66
- message: 'Card is BLOCKED',
67
- });
68
- continue;
69
- }
70
- try {
71
- await this.processQaCard(card, actions, recommendedActions);
72
- }
73
- catch (err) {
74
- const msg = err instanceof Error ? err.message : String(err);
75
- this.log.error(`Unexpected error processing seq ${card.seq}: ${msg}`);
76
- actions.push({
77
- action: 'closeout',
78
- entity: `seq:${card.seq}`,
79
- result: 'fail',
80
- message: `Unexpected error: ${msg}`,
81
- });
82
- }
83
- }
84
- }
85
- }
86
- catch (err) {
87
- const msg = err instanceof Error ? err.message : String(err);
88
- this.log.error(`Closeout tick failed: ${msg}`);
89
- result.status = 'fail';
90
- result.exitCode = 1;
91
- result.details = { error: msg };
92
- }
93
- // Always run worktree cleanup — independent of QA card processing
94
- await this.cleanupWorktrees(actions);
95
- if (actions.some((a) => a.result === 'fail') && result.status === 'ok') {
96
- result.status = 'degraded';
97
- }
98
- return result;
99
- }
100
- // ─── Core Decision Tree ───────────────────────────────────────
101
- async processQaCard(card, actions, recommendedActions) {
102
- const seq = card.seq;
103
- const state = this.runtimeStore.readState();
104
- const runtime = this.runtimeStore.getTask(seq, state);
105
- const branchName = runtime.lease?.branch ||
106
- runtime.evidence?.branch ||
107
- this.buildBranchName(card);
108
- const worktree = runtime.lease?.worktree ||
109
- runtime.evidence?.worktree ||
110
- resolveWorktreePath(this.ctx.projectName, seq, this.ctx.config.WORKTREE_DIR);
111
- if (!worktree || !existsSync(worktree)) {
112
- await this.markNeedsFix(seq, 'QA task has no usable worktree');
113
- actions.push({
114
- action: 'mark-needs-fix',
115
- entity: `seq:${seq}`,
116
- result: 'ok',
117
- message: 'No usable worktree for QA task',
118
- });
119
- return;
120
- }
121
- if (this.isMergedToBase(worktree, branchName)) {
122
- this.log.info(`seq ${seq}: integration already complete, proceeding to release`);
123
- await this.releaseAndDone(card, actions);
124
- return;
125
- }
126
- const activeStatus = await this.inspectQaWorker(card, runtime.slotName, worktree, branchName, actions);
127
- if (activeStatus === 'active' || activeStatus === 'waiting' || activeStatus === 'done') {
128
- return;
129
- }
130
- if (activeStatus === 'failed') {
131
- recommendedActions.push({
132
- action: `Review QA task seq:${seq}`,
133
- reason: 'Integration worker exited without merging the branch',
134
- severity: 'warning',
135
- autoExecutable: false,
136
- requiresConfirmation: true,
137
- safeToRetry: true,
138
- });
139
- return;
140
- }
141
- await this.startIntegrationWorker(card, runtime.slotName, worktree, branchName, actions);
142
- }
143
- async inspectQaWorker(card, slotName, worktree, branchName, actions) {
144
- if (!slotName)
145
- return 'idle';
146
- const snapshots = this.workerManager.inspect({ taskId: String(card.seq) });
147
- if (snapshots.length === 0)
148
- return 'idle';
149
- const snapshot = snapshots[0];
150
- if (snapshot.state === 'idle')
151
- return 'idle';
152
- if (snapshot.state === 'waiting_input') {
153
- this.log.info(`seq ${card.seq}: integration worker waiting for input — ${snapshot.pendingInput?.prompt || 'input required'}`);
154
- actions.push({
155
- action: 'qa-waiting',
156
- entity: `seq:${card.seq}`,
157
- result: 'skip',
158
- message: 'Integration worker waiting_input',
159
- });
160
- return 'waiting';
161
- }
162
- if (snapshot.state === 'needs_confirmation') {
163
- this.log.warn(`seq ${card.seq}: integration worker needs confirmation — ${snapshot.pendingInput?.prompt || 'confirmation required'}`);
164
- actions.push({
165
- action: 'qa-waiting',
166
- entity: `seq:${card.seq}`,
167
- result: 'skip',
168
- message: 'Integration worker needs_confirmation',
169
- });
170
- return 'waiting';
171
- }
172
- if (snapshot.state === 'starting' || snapshot.state === 'running') {
173
- actions.push({
174
- action: 'qa-running',
175
- entity: `seq:${card.seq}`,
176
- result: 'skip',
177
- message: `Integration worker ${snapshot.state}`,
178
- });
179
- return 'active';
180
- }
181
- if (snapshot.state === 'completed') {
182
- if (this.isMergedToBase(worktree, branchName)) {
183
- await this.releaseAndDone(card, actions);
184
- return 'done';
185
- }
186
- }
187
- // 'failed' or 'completed' without merge
188
- await this.releaseQaSlot(card.seq, slotName);
189
- await this.markNeedsFix(card.seq, `Integration worker ${snapshot.state} before merge completed`);
190
- actions.push({
191
- action: 'mark-needs-fix',
192
- entity: `seq:${card.seq}`,
193
- result: 'ok',
194
- message: `Integration worker ${snapshot.state} before merge completed`,
195
- });
196
- return 'failed';
197
- }
198
- async startIntegrationWorker(card, _preferredSlot, worktree, branchName, actions) {
199
- const seq = card.seq;
200
- // Generate integration prompt in-memory (no disk file dependency)
201
- const prompt = this.buildIntegrationPrompt(card, worktree, branchName);
202
- const workflowTransport = resolveWorkflowTransport(this.ctx.config);
203
- const logsDir = this.ctx.paths.logsDir;
204
- const runRequest = {
205
- taskId: String(card.seq),
206
- cardId: String(card.seq),
207
- project: this.ctx.projectName,
208
- phase: 'integration',
209
- prompt,
210
- cwd: worktree,
211
- branch: branchName,
212
- targetBranch: this.ctx.mergeBranch,
213
- tool: (this.pipelineAdapter.integrateStage?.agent || this.ctx.config.ACP_AGENT || this.ctx.config.WORKER_TOOL),
214
- transport: 'acp-sdk',
215
- outputFile: resolve(logsDir, `${this.ctx.projectName}-integration-${card.seq}-${Date.now()}.jsonl`),
216
- completionStrategy: this.pipelineAdapter.integrateStage?.completion,
217
- };
218
- const response = await this.workerManager.run(runRequest);
219
- if (!response.accepted) {
220
- this.log.info(`seq ${seq}: WM rejected integration run: ${response.rejectReason ?? 'unknown'}`);
221
- actions.push({
222
- action: 'qa-launch',
223
- entity: `seq:${seq}`,
224
- result: 'skip',
225
- message: `WM rejected: ${response.rejectReason ?? 'unknown'}`,
226
- });
227
- return;
228
- }
229
- // Queued in IntegrationQueue — no slot yet, will be spawned when active finishes
230
- if (response.queued) {
231
- this.log.info(`seq ${seq}: Queued for integration (position=${response.queuePosition})`);
232
- // Update lease to merging even though no slot is assigned yet.
233
- // This prevents Monitor from misinterpreting the card as stale.
234
- this.runtimeStore.updateState('closeout-queue-integration', (draft) => {
235
- if (draft.leases[seq]) {
236
- draft.leases[seq].phase = 'merging';
237
- draft.leases[seq].lastTransitionAt = new Date().toISOString();
238
- }
239
- });
240
- actions.push({
241
- action: 'qa-launch',
242
- entity: `seq:${seq}`,
243
- result: 'ok',
244
- message: `Queued for integration (position=${response.queuePosition})`,
245
- });
246
- return;
247
- }
248
- // PM claim (best-effort, non-blocking)
249
- const slotName = response.slot;
250
- try {
251
- await this.taskBackend.claim(seq, slotName);
252
- }
253
- catch (err) {
254
- this.log.warn(`seq ${seq}: PM claim for QA worker failed: ${err instanceof Error ? err.message : err}`);
255
- }
256
- // Update local runtime projections with the slot WM allocated
257
- this.runtimeStore.updateState('closeout-launch-integration', (draft) => {
258
- draft.activeCards[seq] = {
259
- seq: parseInt(seq, 10),
260
- state: this.pipelineAdapter.states.review,
261
- worker: slotName,
262
- mrUrl: draft.activeCards[seq]?.mrUrl || null,
263
- conflictDomains: draft.activeCards[seq]?.conflictDomains || [],
264
- startedAt: draft.activeCards[seq]?.startedAt || new Date().toISOString(),
265
- retryCount: draft.activeCards[seq]?.retryCount ?? draft.leases[seq]?.retryCount ?? 0,
266
- };
267
- draft.leases[seq] = {
268
- seq: parseInt(seq, 10),
269
- pmStateObserved: this.pipelineAdapter.states.review,
270
- phase: 'merging',
271
- slot: slotName,
272
- branch: branchName,
273
- worktree,
274
- sessionId: response.sessionId || null,
275
- runId: null,
276
- claimedAt: new Date().toISOString(),
277
- retryCount: draft.leases[seq]?.retryCount ?? 0,
278
- lastTransitionAt: new Date().toISOString(),
279
- };
280
- });
281
- actions.push({
282
- action: 'qa-launch',
283
- entity: `seq:${seq}`,
284
- result: 'ok',
285
- message: `Started integration worker on ${slotName}`,
286
- });
287
- this.logEvent('qa-launch', seq, 'ok', { worker: slotName });
288
- }
289
- // ─── Resource Release (01 §10.3.3) ─────────────────────────────
290
- /**
291
- * Release resources after successful merge. Each step failure MUST NOT
292
- * block subsequent steps — log and continue.
293
- *
294
- * Order:
295
- * 1. Move card to Done
296
- * 2. Release claim in PM
297
- * 3. Release worker slot in state.json (→ idle)
298
- * 4. Stop worker session
299
- * 5. Mark worktree for cleanup
300
- */
301
- async releaseAndDone(card, actions) {
302
- const seq = card.seq;
303
- const errors = [];
304
- // Step 0: Clean auxiliary labels (NEEDS-FIX, STALE-RUNTIME, etc.)
305
- // These may have been set by EventHandler during transient failures
306
- // but the card is now successfully merged — labels should not persist.
307
- for (const label of this.pipelineAdapter.auxiliaryLabels) {
308
- if (card.labels.includes(label)) {
309
- try {
310
- await this.taskBackend.removeLabel(seq, label);
311
- this.log.ok(`seq ${seq}: Removed residual label "${label}"`);
312
- }
313
- catch { /* best effort */ }
314
- }
315
- }
316
- // Step 1: Move card to Done
317
- try {
318
- await this.taskBackend.move(seq, this.pipelineAdapter.states.done);
319
- this.log.ok(`seq ${seq}: Moved to ${this.pipelineAdapter.states.done}`);
320
- }
321
- catch (err) {
322
- const msg = err instanceof Error ? err.message : String(err);
323
- this.log.error(`seq ${seq}: Failed to move to Done: ${msg}`);
324
- errors.push(`move-done: ${msg}`);
325
- }
326
- // Step 2: Release claim
327
- try {
328
- await this.taskBackend.releaseClaim(seq);
329
- this.log.ok(`seq ${seq}: Claim released`);
330
- }
331
- catch (err) {
332
- const msg = err instanceof Error ? err.message : String(err);
333
- this.log.error(`seq ${seq}: Failed to release claim: ${msg}`);
334
- errors.push(`release-claim: ${msg}`);
335
- }
336
- // Step 3: Release worker slot in state.json
337
- const state = this.runtimeStore.readState();
338
- const runtime = this.runtimeStore.getTask(seq, state);
339
- const slotEntry = runtime.slotName && runtime.slot ? [runtime.slotName, runtime.slot] : null;
340
- if (slotEntry) {
341
- const [slotName] = slotEntry;
342
- try {
343
- this.runtimeStore.updateState('closeout-release', (draft) => {
344
- this.runtimeStore.releaseTaskProjection(draft, seq, { dropLease: true });
345
- });
346
- this.log.ok(`seq ${seq}: Worker slot ${slotName} released`);
347
- }
348
- catch (err) {
349
- const msg = err instanceof Error ? err.message : String(err);
350
- this.log.error(`seq ${seq}: Failed to release slot: ${msg}`);
351
- errors.push(`release-slot: ${msg}`);
352
- }
353
- }
354
- else {
355
- // No active slot found — already released (idempotency)
356
- // Still clean up activeCards entry if present
357
- if (state.activeCards[seq] || state.leases[seq]) {
358
- try {
359
- this.runtimeStore.updateState('closeout-release', (draft) => {
360
- this.runtimeStore.releaseTaskProjection(draft, seq, { dropLease: true });
361
- });
362
- }
363
- catch {
364
- // non-fatal
365
- }
366
- }
367
- this.log.debug(`seq ${seq}: No active worker slot found (already released)`);
368
- }
369
- // Step 4: Silently stop the worker process if still running.
370
- // DO NOT use workerManager.cancel() — it emits run.failed events which
371
- // trigger NEEDS-FIX labels and error notifications. The task is already
372
- // Done; we just need to kill the orphan process without side effects.
373
- try {
374
- const snapshots = this.workerManager.inspect({ taskId: seq });
375
- for (const snap of snapshots) {
376
- if (snap.pid && snap.pid > 0) {
377
- try {
378
- process.kill(snap.pid, 'SIGTERM');
379
- }
380
- catch { /* already dead */ }
381
- }
382
- }
383
- this.log.ok(`seq ${seq}: Worker process cleaned up`);
384
- }
385
- catch (err) {
386
- const msg = err instanceof Error ? err.message : String(err);
387
- this.log.debug(`seq ${seq}: Worker cleanup: ${msg}`);
388
- }
389
- // Step 4b: Clear completed task from IntegrationQueue to unblock waiting tasks.
390
- // The WM cancel above only cleans up spawned workers. If this task was the
391
- // active entry in the queue (detected as already-merged before WM spawned),
392
- // the queue is never advanced. Dequeue explicitly so waiting tasks proceed.
393
- try {
394
- const iq = new IntegrationQueue(this.ctx.paths.stateFile, this.ctx.config.MAX_CONCURRENT_WORKERS);
395
- const active = iq.getActive(this.ctx.projectName, this.ctx.mergeBranch);
396
- if (active && active.taskId === seq) {
397
- iq.dequeueNext(this.ctx.projectName, this.ctx.mergeBranch);
398
- this.log.ok(`seq ${seq}: Integration queue advanced`);
399
- }
400
- }
401
- catch (err) {
402
- const msg = err instanceof Error ? err.message : String(err);
403
- this.log.warn(`seq ${seq}: Failed to advance integration queue: ${msg}`);
404
- }
405
- // Step 5: Mark worktree for cleanup (actual removal runs at end of tick)
406
- try {
407
- const freshState = this.runtimeStore.readState();
408
- const branchName = this.buildBranchName(card);
409
- const worktreePath = runtime.lease?.worktree ||
410
- runtime.evidence?.worktree ||
411
- resolveWorktreePath(this.ctx.projectName, seq, this.ctx.config.WORKTREE_DIR);
412
- const cleanup = freshState.worktreeCleanup ?? [];
413
- const alreadyMarked = cleanup.some((e) => e.branch === branchName);
414
- if (!alreadyMarked) {
415
- cleanup.push({ branch: branchName, worktreePath, markedAt: new Date().toISOString() });
416
- this.runtimeStore.updateState('closeout-worktree-mark', (draft) => {
417
- draft.worktreeCleanup = cleanup;
418
- });
419
- }
420
- this.log.ok(`seq ${seq}: Worktree marked for cleanup`);
421
- }
422
- catch (err) {
423
- const msg = err instanceof Error ? err.message : String(err);
424
- this.log.error(`seq ${seq}: Failed to mark worktree for cleanup: ${msg}`);
425
- errors.push(`worktree-mark: ${msg}`);
426
- }
427
- // Notify
428
- if (errors.length === 0) {
429
- await this.notifySafe(`✅ [${this.ctx.projectName}] seq:${seq} merged and released successfully`);
430
- }
431
- else {
432
- await this.notifySafe(`⚠️ [${this.ctx.projectName}] seq:${seq} merged but release had errors: ${errors.join('; ')}`);
433
- }
434
- // Record action
435
- const actionResult = errors.length === 0 ? 'ok' : 'fail';
436
- actions.push({
437
- action: 'closeout',
438
- entity: `seq:${seq}`,
439
- result: actionResult,
440
- message: errors.length === 0
441
- ? 'Merged → Done, resources released'
442
- : `Merged → Done with errors: ${errors.join('; ')}`,
443
- });
444
- this.logEvent('closeout', seq, actionResult, errors.length > 0 ? { errors } : undefined);
445
- }
446
- // ─── Worktree Cleanup ──────────────────────────────────────────
447
- /**
448
- * Process the worktreeCleanup queue: remove worktree directories and
449
- * delete local branches that have been merged.
450
- *
451
- * Each entry is processed independently — one failure does not block others.
452
- */
453
- async cleanupWorktrees(actions) {
454
- const state = this.runtimeStore.readState();
455
- const queue = state.worktreeCleanup ?? [];
456
- if (queue.length === 0)
457
- return;
458
- // Only clean up entries marked at least 30 seconds ago.
459
- // Entries marked in the current tick may still have worker processes
460
- // shutting down (SIGTERM grace period). Defer to next tick.
461
- const now = Date.now();
462
- const ready = queue.filter(e => now - new Date(e.markedAt).getTime() >= 30_000);
463
- const deferred = queue.filter(e => now - new Date(e.markedAt).getTime() < 30_000);
464
- if (ready.length === 0) {
465
- if (deferred.length > 0) {
466
- this.log.debug(`${deferred.length} worktree(s) deferred — waiting for worker shutdown`);
467
- }
468
- return;
469
- }
470
- this.log.info(`Cleaning up ${ready.length} worktree(s)`);
471
- const remaining = [...deferred];
472
- for (const entry of ready) {
473
- try {
474
- await this.repoBackend.removeWorktree(this.ctx.paths.repoDir, entry.worktreePath, entry.branch);
475
- this.log.ok(`Cleaned up worktree: ${entry.branch}`);
476
- actions.push({
477
- action: 'worktree-cleanup',
478
- entity: entry.branch,
479
- result: 'ok',
480
- message: `Removed worktree ${entry.worktreePath}`,
481
- });
482
- this.logEvent('worktree-cleanup', entry.branch, 'ok');
483
- }
484
- catch (err) {
485
- const msg = err instanceof Error ? err.message : String(err);
486
- this.log.warn(`Failed to clean up worktree ${entry.branch}: ${msg}`);
487
- remaining.push(entry); // retry next tick
488
- actions.push({
489
- action: 'worktree-cleanup',
490
- entity: entry.branch,
491
- result: 'fail',
492
- message: `Cleanup failed: ${msg}`,
493
- });
494
- }
495
- }
496
- // Update state with remaining entries
497
- this.runtimeStore.updateState('closeout-worktree-cleanup', (freshState) => {
498
- freshState.worktreeCleanup = remaining;
499
- });
500
- }
501
- // ─── Helpers ───────────────────────────────────────────────────
502
- buildBranchName(card) {
503
- const slug = card.name
504
- .toLowerCase()
505
- .replace(/[^a-z0-9]+/g, '-')
506
- .replace(/^-|-$/g, '')
507
- .slice(0, 40);
508
- return `feature/${card.seq}-${slug}`;
509
- }
510
- async markNeedsFix(seq, reason) {
511
- await this.addLabelSafe(seq, 'NEEDS-FIX');
512
- await this.commentSafe(seq, `NEEDS-FIX: ${reason}`);
513
- await this.notifySafe(`⚠️ [${this.ctx.projectName}] seq:${seq} marked NEEDS-FIX: ${reason}`);
514
- }
515
- async addLabelSafe(seq, label) {
516
- try {
517
- await this.taskBackend.addLabel(seq, label);
518
- }
519
- catch (err) {
520
- const msg = err instanceof Error ? err.message : String(err);
521
- this.log.error(`Failed to add label ${label} to seq ${seq}: ${msg}`);
522
- }
523
- }
524
- async commentSafe(seq, text) {
525
- try {
526
- await this.taskBackend.comment(seq, text);
527
- }
528
- catch (err) {
529
- const msg = err instanceof Error ? err.message : String(err);
530
- this.log.error(`Failed to comment on seq ${seq}: ${msg}`);
531
- }
532
- }
533
- async notifySafe(message) {
534
- if (!this.notifier)
535
- return;
536
- try {
537
- await this.notifier.send(message);
538
- }
539
- catch {
540
- // Notification failures are never fatal
541
- }
542
- }
543
- logEvent(action, seq, result, meta) {
544
- this.log.event({
545
- component: 'qa',
546
- action,
547
- entity: `seq:${seq}`,
548
- result,
549
- meta,
550
- });
551
- }
552
- /**
553
- * Generate integration prompt in-memory. Archive to .sps/ for debugging.
554
- * Independent of ExecutionEngine — each phase generates its own prompt.
555
- */
556
- buildIntegrationPrompt(card, worktree, branchName) {
557
- // Load project rules from worktree
558
- let projectRules = '';
559
- const claudeMdPath = resolve(worktree, 'CLAUDE.md');
560
- const agentsMdPath = resolve(worktree, 'AGENTS.md');
561
- if (existsSync(claudeMdPath)) {
562
- projectRules = readFileSync(claudeMdPath, 'utf-8').trim();
563
- }
564
- if (existsSync(agentsMdPath)) {
565
- const agentsRules = readFileSync(agentsMdPath, 'utf-8').trim();
566
- projectRules = projectRules ? `${projectRules}\n\n${agentsRules}` : agentsRules;
567
- }
568
- const prompt = buildPhasePrompt({
569
- taskSeq: card.seq,
570
- taskTitle: card.name,
571
- taskDescription: card.desc || '(no description)',
572
- cardId: card.id,
573
- worktreePath: worktree,
574
- branchName,
575
- targetBranch: this.ctx.mergeBranch,
576
- mergeMode: this.ctx.mrMode,
577
- gitlabProjectId: resolveGitlabProjectId(this.ctx.config),
578
- projectRules: projectRules || undefined,
579
- phase: 'integration',
580
- });
581
- // Archive to .sps/ for debugging (non-blocking)
582
- try {
583
- const spsDir = resolve(worktree, '.sps');
584
- if (!existsSync(spsDir))
585
- mkdirSync(spsDir, { recursive: true });
586
- writeFileSync(resolve(spsDir, INTEGRATION_PROMPT_FILE), prompt);
587
- }
588
- catch { /* archive failure should never block integration */ }
589
- return prompt;
590
- }
591
- /**
592
- * Check if the feature branch has been merged into the base branch.
593
- *
594
- * Guard against false positives: a freshly created branch with no work
595
- * is trivially an ancestor of origin/base. Only return true if:
596
- * - Branch was pushed to remote, OR
597
- * - Branch has local commits ahead of base
598
- * This matches CompletionJudge's artifact check logic.
599
- */
600
- isMergedToBase(worktree, branchName) {
601
- try {
602
- execFileSync('git', ['-C', worktree, 'fetch', 'origin', this.ctx.mergeBranch], { stdio: 'ignore' });
603
- }
604
- catch {
605
- // Best effort. A stale fetch is still usable for local containment checks.
606
- }
607
- try {
608
- execFileSync('git', ['-C', worktree, 'merge-base', '--is-ancestor', branchName, `origin/${this.ctx.mergeBranch}`], { stdio: 'ignore' });
609
- }
610
- catch {
611
- return false;
612
- }
613
- // is-ancestor passed — but is it a real merge or an empty branch?
614
- const pushed = branchPushed(worktree, branchName);
615
- const localAhead = branchCommitsAhead(worktree, branchName, this.ctx.mergeBranch);
616
- if (pushed || localAhead > 0) {
617
- return true;
618
- }
619
- // Empty branch sitting at base commit — not a real merge
620
- this.log.debug(`seq branch ${branchName}: ancestor of ${this.ctx.mergeBranch} but no artifacts — not a real merge`);
621
- return false;
622
- }
623
- async releaseQaSlot(seq, slotName) {
624
- try {
625
- await this.workerManager.cancel({ taskId: seq, project: this.ctx.projectName, reason: 'anomaly' });
626
- }
627
- catch (err) {
628
- const msg = err instanceof Error ? err.message : String(err);
629
- this.log.warn(`seq ${seq}: WM cancel in releaseQaSlot failed: ${msg}`);
630
- }
631
- this.runtimeStore.updateState('closeout-release-qa-slot', (draft) => {
632
- this.runtimeStore.releaseTaskProjection(draft, seq, {
633
- dropLease: false,
634
- phase: 'merging',
635
- keepWorktree: true,
636
- pmStateObserved: this.pipelineAdapter.states.review,
637
- });
638
- });
639
- try {
640
- await this.taskBackend.releaseClaim(seq);
641
- }
642
- catch (err) {
643
- const msg = err instanceof Error ? err.message : String(err);
644
- this.log.warn(`seq ${seq}: Failed to release QA claim for ${slotName}: ${msg}`);
645
- }
646
- }
647
- }
648
- //# sourceMappingURL=CloseoutEngine.js.map