@coralai/sps-cli 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/commands/cardAdd.d.ts +2 -0
  2. package/dist/commands/cardAdd.d.ts.map +1 -0
  3. package/dist/commands/cardAdd.js +65 -0
  4. package/dist/commands/cardAdd.js.map +1 -0
  5. package/dist/commands/doctor.d.ts +9 -0
  6. package/dist/commands/doctor.d.ts.map +1 -0
  7. package/dist/commands/doctor.js +264 -0
  8. package/dist/commands/doctor.js.map +1 -0
  9. package/dist/commands/monitorTick.d.ts +2 -0
  10. package/dist/commands/monitorTick.d.ts.map +1 -0
  11. package/dist/commands/monitorTick.js +47 -0
  12. package/dist/commands/monitorTick.js.map +1 -0
  13. package/dist/commands/pipelineTick.d.ts +2 -0
  14. package/dist/commands/pipelineTick.d.ts.map +1 -0
  15. package/dist/commands/pipelineTick.js +44 -0
  16. package/dist/commands/pipelineTick.js.map +1 -0
  17. package/dist/commands/pmCommand.d.ts +2 -0
  18. package/dist/commands/pmCommand.d.ts.map +1 -0
  19. package/dist/commands/pmCommand.js +159 -0
  20. package/dist/commands/pmCommand.js.map +1 -0
  21. package/dist/commands/projectInit.d.ts +2 -0
  22. package/dist/commands/projectInit.d.ts.map +1 -0
  23. package/dist/commands/projectInit.js +75 -0
  24. package/dist/commands/projectInit.js.map +1 -0
  25. package/dist/commands/qaTick.d.ts +2 -0
  26. package/dist/commands/qaTick.d.ts.map +1 -0
  27. package/dist/commands/qaTick.js +43 -0
  28. package/dist/commands/qaTick.js.map +1 -0
  29. package/dist/commands/schedulerTick.d.ts +2 -0
  30. package/dist/commands/schedulerTick.d.ts.map +1 -0
  31. package/dist/commands/schedulerTick.js +45 -0
  32. package/dist/commands/schedulerTick.js.map +1 -0
  33. package/dist/commands/tick.d.ts +14 -0
  34. package/dist/commands/tick.d.ts.map +1 -0
  35. package/dist/commands/tick.js +251 -0
  36. package/dist/commands/tick.js.map +1 -0
  37. package/dist/commands/workerLaunch.d.ts +2 -0
  38. package/dist/commands/workerLaunch.d.ts.map +1 -0
  39. package/dist/commands/workerLaunch.js +56 -0
  40. package/dist/commands/workerLaunch.js.map +1 -0
  41. package/dist/core/config.d.ts +38 -0
  42. package/dist/core/config.d.ts.map +1 -0
  43. package/dist/core/config.js +131 -0
  44. package/dist/core/config.js.map +1 -0
  45. package/dist/core/context.d.ts +23 -0
  46. package/dist/core/context.d.ts.map +1 -0
  47. package/dist/core/context.js +28 -0
  48. package/dist/core/context.js.map +1 -0
  49. package/dist/core/lock.d.ts +14 -0
  50. package/dist/core/lock.d.ts.map +1 -0
  51. package/dist/core/lock.js +65 -0
  52. package/dist/core/lock.js.map +1 -0
  53. package/dist/core/logger.d.ts +24 -0
  54. package/dist/core/logger.d.ts.map +1 -0
  55. package/dist/core/logger.js +62 -0
  56. package/dist/core/logger.js.map +1 -0
  57. package/dist/core/paths.d.ts +27 -0
  58. package/dist/core/paths.d.ts.map +1 -0
  59. package/dist/core/paths.js +29 -0
  60. package/dist/core/paths.js.map +1 -0
  61. package/dist/core/queue.d.ts +14 -0
  62. package/dist/core/queue.d.ts.map +1 -0
  63. package/dist/core/queue.js +38 -0
  64. package/dist/core/queue.js.map +1 -0
  65. package/dist/core/state.d.ts +32 -0
  66. package/dist/core/state.d.ts.map +1 -0
  67. package/dist/core/state.js +52 -0
  68. package/dist/core/state.js.map +1 -0
  69. package/dist/engines/CloseoutEngine.d.ts +60 -0
  70. package/dist/engines/CloseoutEngine.d.ts.map +1 -0
  71. package/dist/engines/CloseoutEngine.js +596 -0
  72. package/dist/engines/CloseoutEngine.js.map +1 -0
  73. package/dist/engines/ExecutionEngine.d.ts +65 -0
  74. package/dist/engines/ExecutionEngine.d.ts.map +1 -0
  75. package/dist/engines/ExecutionEngine.js +603 -0
  76. package/dist/engines/ExecutionEngine.js.map +1 -0
  77. package/dist/engines/MonitorEngine.d.ts +39 -0
  78. package/dist/engines/MonitorEngine.d.ts.map +1 -0
  79. package/dist/engines/MonitorEngine.js +473 -0
  80. package/dist/engines/MonitorEngine.js.map +1 -0
  81. package/dist/engines/SchedulerEngine.d.ts +24 -0
  82. package/dist/engines/SchedulerEngine.d.ts.map +1 -0
  83. package/dist/engines/SchedulerEngine.js +195 -0
  84. package/dist/engines/SchedulerEngine.js.map +1 -0
  85. package/dist/interfaces/HookProvider.d.ts +9 -0
  86. package/dist/interfaces/HookProvider.d.ts.map +1 -0
  87. package/dist/interfaces/HookProvider.js +2 -0
  88. package/dist/interfaces/HookProvider.js.map +1 -0
  89. package/dist/interfaces/Notifier.d.ts +11 -0
  90. package/dist/interfaces/Notifier.d.ts.map +1 -0
  91. package/dist/interfaces/Notifier.js +2 -0
  92. package/dist/interfaces/Notifier.js.map +1 -0
  93. package/dist/interfaces/RepoBackend.d.ts +23 -0
  94. package/dist/interfaces/RepoBackend.d.ts.map +1 -0
  95. package/dist/interfaces/RepoBackend.js +2 -0
  96. package/dist/interfaces/RepoBackend.js.map +1 -0
  97. package/dist/interfaces/TaskBackend.d.ts +24 -0
  98. package/dist/interfaces/TaskBackend.d.ts.map +1 -0
  99. package/dist/interfaces/TaskBackend.js +2 -0
  100. package/dist/interfaces/TaskBackend.js.map +1 -0
  101. package/dist/interfaces/WorkerProvider.d.ts +23 -0
  102. package/dist/interfaces/WorkerProvider.d.ts.map +1 -0
  103. package/dist/interfaces/WorkerProvider.js +2 -0
  104. package/dist/interfaces/WorkerProvider.js.map +1 -0
  105. package/dist/main.d.ts +3 -0
  106. package/dist/main.d.ts.map +1 -0
  107. package/dist/main.js +226 -0
  108. package/dist/main.js.map +1 -0
  109. package/dist/models/types.d.ts +68 -0
  110. package/dist/models/types.d.ts.map +1 -0
  111. package/dist/models/types.js +2 -0
  112. package/dist/models/types.js.map +1 -0
  113. package/dist/providers/ClaudeWorkerProvider.d.ts +84 -0
  114. package/dist/providers/ClaudeWorkerProvider.d.ts.map +1 -0
  115. package/dist/providers/ClaudeWorkerProvider.js +293 -0
  116. package/dist/providers/ClaudeWorkerProvider.js.map +1 -0
  117. package/dist/providers/CodexWorkerProvider.d.ts +50 -0
  118. package/dist/providers/CodexWorkerProvider.d.ts.map +1 -0
  119. package/dist/providers/CodexWorkerProvider.js +275 -0
  120. package/dist/providers/CodexWorkerProvider.js.map +1 -0
  121. package/dist/providers/GitLabRepoBackend.d.ts +42 -0
  122. package/dist/providers/GitLabRepoBackend.d.ts.map +1 -0
  123. package/dist/providers/GitLabRepoBackend.js +280 -0
  124. package/dist/providers/GitLabRepoBackend.js.map +1 -0
  125. package/dist/providers/MarkdownTaskBackend.d.ts +88 -0
  126. package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -0
  127. package/dist/providers/MarkdownTaskBackend.js +414 -0
  128. package/dist/providers/MarkdownTaskBackend.js.map +1 -0
  129. package/dist/providers/MatrixNotifier.d.ts +30 -0
  130. package/dist/providers/MatrixNotifier.d.ts.map +1 -0
  131. package/dist/providers/MatrixNotifier.js +82 -0
  132. package/dist/providers/MatrixNotifier.js.map +1 -0
  133. package/dist/providers/PlaneTaskBackend.d.ts +86 -0
  134. package/dist/providers/PlaneTaskBackend.d.ts.map +1 -0
  135. package/dist/providers/PlaneTaskBackend.js +409 -0
  136. package/dist/providers/PlaneTaskBackend.js.map +1 -0
  137. package/dist/providers/TrelloTaskBackend.d.ts +53 -0
  138. package/dist/providers/TrelloTaskBackend.d.ts.map +1 -0
  139. package/dist/providers/TrelloTaskBackend.js +300 -0
  140. package/dist/providers/TrelloTaskBackend.js.map +1 -0
  141. package/dist/providers/registry.d.ts +10 -0
  142. package/dist/providers/registry.d.ts.map +1 -0
  143. package/dist/providers/registry.js +29 -0
  144. package/dist/providers/registry.js.map +1 -0
  145. package/package.json +36 -0
@@ -0,0 +1,65 @@
1
+ import type { ProjectContext } from '../core/context.js';
2
+ import type { TaskBackend } from '../interfaces/TaskBackend.js';
3
+ import type { WorkerProvider } from '../interfaces/WorkerProvider.js';
4
+ import type { RepoBackend } from '../interfaces/RepoBackend.js';
5
+ import type { Notifier } from '../interfaces/Notifier.js';
6
+ import type { CommandResult } from '../models/types.js';
7
+ export declare class ExecutionEngine {
8
+ private ctx;
9
+ private taskBackend;
10
+ private workerProvider;
11
+ private repoBackend;
12
+ private notifier?;
13
+ private log;
14
+ constructor(ctx: ProjectContext, taskBackend: TaskBackend, workerProvider: WorkerProvider, repoBackend: RepoBackend, notifier?: Notifier | undefined);
15
+ tick(opts?: {
16
+ dryRun?: boolean;
17
+ }): Promise<CommandResult>;
18
+ /**
19
+ * Launch a single card (for `sps worker launch <project> <seq>`).
20
+ * Assumes card is in Todo state with branch/worktree already prepared.
21
+ */
22
+ launchSingle(seq: string, opts?: {
23
+ dryRun?: boolean;
24
+ }): Promise<CommandResult>;
25
+ private shouldSkip;
26
+ /**
27
+ * Check an Inprogress card: detect worker completion status and act.
28
+ * This is the critical Inprogress → QA bridge (01 §10.2).
29
+ *
30
+ * Detection chain (12 §2):
31
+ * COMPLETED → move card to QA
32
+ * AUTO_CONFIRM → auto-confirm prompt, continue next tick
33
+ * NEEDS_INPUT → mark WAITING-CONFIRMATION, notify
34
+ * BLOCKED → mark BLOCKED
35
+ * ALIVE → no action (worker still working)
36
+ * DEAD → mark STALE-RUNTIME (handled by MonitorEngine)
37
+ * DEAD_EXCEEDED → mark STALE-RUNTIME, notify
38
+ */
39
+ private checkInprogressCard;
40
+ /**
41
+ * Prepare a Backlog card: create branch, create worktree, move to Todo.
42
+ * Steps 1-3 per 01 §4.3.
43
+ */
44
+ private prepareCard;
45
+ /**
46
+ * Launch a Todo card: claim slot, build context, start worker, move to Inprogress.
47
+ * Steps 4-7 per 01 §4.3.
48
+ */
49
+ private launchCard;
50
+ /**
51
+ * Build branch name from card: feature/<seq>-<slug>
52
+ */
53
+ private buildBranchName;
54
+ /**
55
+ * Write worker rules and task prompt to worktree.
56
+ * Generates both CLAUDE.md (for Claude) and AGENTS.md (for Codex).
57
+ */
58
+ private buildTaskContext;
59
+ /**
60
+ * Release a worker slot, cleanup tmux session, remove card from active cards.
61
+ */
62
+ private releaseSlot;
63
+ private logEvent;
64
+ }
65
+ //# sourceMappingURL=ExecutionEngine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExecutionEngine.d.ts","sourceRoot":"","sources":["../../src/engines/ExecutionEngine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAsC,MAAM,oBAAoB,CAAC;AAO5F,qBAAa,eAAe;IAIxB,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,QAAQ,CAAC;IAPnB,OAAO,CAAC,GAAG,CAAS;gBAGV,GAAG,EAAE,cAAc,EACnB,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,WAAW,EACxB,QAAQ,CAAC,EAAE,QAAQ,YAAA;IAKvB,IAAI,CAAC,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,aAAa,CAAC;IAiFnE;;;OAGG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,aAAa,CAAC;IAuDxF,OAAO,CAAC,UAAU;IAMlB;;;;;;;;;;;;OAYG;YACW,mBAAmB;IAwIjC;;;OAGG;YACW,WAAW;IA6DzB;;;OAGG;YACW,UAAU;IAoJxB;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA+DxB;;OAEG;IACH,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,QAAQ;CASjB"}
@@ -0,0 +1,603 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { readState, writeState } from '../core/state.js';
4
+ import { resolveWorktreePath } from '../core/paths.js';
5
+ import { Logger } from '../core/logger.js';
6
+ const SKIP_LABELS = ['BLOCKED', 'NEEDS-FIX', 'CONFLICT', 'WAITING-CONFIRMATION', 'STALE-RUNTIME'];
7
+ export class ExecutionEngine {
8
+ ctx;
9
+ taskBackend;
10
+ workerProvider;
11
+ repoBackend;
12
+ notifier;
13
+ log;
14
+ constructor(ctx, taskBackend, workerProvider, repoBackend, notifier) {
15
+ this.ctx = ctx;
16
+ this.taskBackend = taskBackend;
17
+ this.workerProvider = workerProvider;
18
+ this.repoBackend = repoBackend;
19
+ this.notifier = notifier;
20
+ this.log = new Logger('pipeline', ctx.projectName, ctx.paths.logsDir);
21
+ }
22
+ async tick(opts = {}) {
23
+ const actions = [];
24
+ const result = {
25
+ project: this.ctx.projectName,
26
+ component: 'pipeline',
27
+ status: 'ok',
28
+ exitCode: 0,
29
+ actions,
30
+ recommendedActions: [],
31
+ details: {},
32
+ };
33
+ let actionsThisTick = 0;
34
+ const maxActions = this.ctx.config.MAX_ACTIONS_PER_TICK;
35
+ try {
36
+ // 1. Process Inprogress cards (detect completion → move to QA)
37
+ // This runs first to free slots before launching new workers.
38
+ const inprogressCards = await this.taskBackend.listByState('Inprogress');
39
+ for (const card of inprogressCards) {
40
+ if (this.shouldSkip(card))
41
+ continue;
42
+ const checkResult = await this.checkInprogressCard(card, opts);
43
+ if (checkResult) {
44
+ actions.push(checkResult);
45
+ if (checkResult.result === 'ok')
46
+ actionsThisTick++;
47
+ }
48
+ }
49
+ // 2. Process Backlog cards (prepare: branch + worktree + move to Todo)
50
+ const backlogCards = await this.taskBackend.listByState('Backlog');
51
+ for (const card of backlogCards) {
52
+ if (actionsThisTick >= maxActions)
53
+ break;
54
+ if (this.shouldSkip(card)) {
55
+ actions.push({ action: 'skip', entity: `seq:${card.seq}`, result: 'skip', message: 'Has auxiliary state label' });
56
+ continue;
57
+ }
58
+ const prepareResult = await this.prepareCard(card, opts);
59
+ actions.push(prepareResult);
60
+ if (prepareResult.result === 'ok')
61
+ actionsThisTick++;
62
+ }
63
+ // 3. Process Todo cards (launch: claim + context + worker + move to Inprogress)
64
+ // Stagger launches to avoid overwhelming tmux/system with simultaneous Claude starts
65
+ const todoCards = await this.taskBackend.listByState('Todo');
66
+ let launchedThisTick = 0;
67
+ const failedSlots = new Set(); // track slots that failed launch this tick
68
+ for (const card of todoCards) {
69
+ if (actionsThisTick >= maxActions)
70
+ break;
71
+ if (this.shouldSkip(card)) {
72
+ actions.push({ action: 'skip', entity: `seq:${card.seq}`, result: 'skip', message: 'Has auxiliary state label' });
73
+ continue;
74
+ }
75
+ // Stagger: wait 10s between worker launches to let each Claude settle
76
+ if (launchedThisTick > 0) {
77
+ this.log.info(`Waiting 10s before next worker launch...`);
78
+ await new Promise((r) => setTimeout(r, 10_000));
79
+ }
80
+ const launchResult = await this.launchCard(card, opts, failedSlots);
81
+ actions.push(launchResult);
82
+ if (launchResult.result === 'ok') {
83
+ actionsThisTick++;
84
+ launchedThisTick++;
85
+ }
86
+ }
87
+ }
88
+ catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ this.log.error(`Pipeline tick failed: ${msg}`);
91
+ result.status = 'fail';
92
+ result.exitCode = 1;
93
+ result.details = { error: msg };
94
+ }
95
+ // Check for any failures
96
+ if (actions.some((a) => a.result === 'fail') && result.status === 'ok') {
97
+ result.status = 'fail';
98
+ result.exitCode = 1;
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Launch a single card (for `sps worker launch <project> <seq>`).
104
+ * Assumes card is in Todo state with branch/worktree already prepared.
105
+ */
106
+ async launchSingle(seq, opts = {}) {
107
+ const result = {
108
+ project: this.ctx.projectName,
109
+ component: 'worker-launch',
110
+ status: 'ok',
111
+ exitCode: 0,
112
+ actions: [],
113
+ recommendedActions: [],
114
+ details: {},
115
+ };
116
+ const card = await this.taskBackend.getBySeq(seq);
117
+ if (!card) {
118
+ result.status = 'fail';
119
+ result.exitCode = 1;
120
+ result.details = { error: `Card seq:${seq} not found` };
121
+ return result;
122
+ }
123
+ // If card is in Backlog, do prepare first
124
+ if (card.state === 'Backlog') {
125
+ const prepareAction = await this.prepareCard(card, opts);
126
+ result.actions.push(prepareAction);
127
+ if (prepareAction.result === 'fail') {
128
+ result.status = 'fail';
129
+ result.exitCode = 1;
130
+ return result;
131
+ }
132
+ // Reload card after prepare
133
+ const updated = await this.taskBackend.getBySeq(seq);
134
+ if (!updated || updated.state !== 'Todo') {
135
+ result.status = 'fail';
136
+ result.exitCode = 1;
137
+ result.details = { error: 'Card not in Todo after prepare' };
138
+ return result;
139
+ }
140
+ }
141
+ if (card.state !== 'Todo' && card.state !== 'Backlog') {
142
+ result.status = 'fail';
143
+ result.exitCode = 2;
144
+ result.details = { error: `Card seq:${seq} is in ${card.state}, expected Backlog or Todo` };
145
+ return result;
146
+ }
147
+ const launchAction = await this.launchCard(card, opts);
148
+ result.actions.push(launchAction);
149
+ if (launchAction.result === 'fail') {
150
+ result.status = 'fail';
151
+ result.exitCode = 1;
152
+ }
153
+ return result;
154
+ }
155
+ shouldSkip(card) {
156
+ return SKIP_LABELS.some((label) => card.labels.includes(label));
157
+ }
158
+ // ─── Inprogress Phase (detect completion → QA) ──────────────────
159
+ /**
160
+ * Check an Inprogress card: detect worker completion status and act.
161
+ * This is the critical Inprogress → QA bridge (01 §10.2).
162
+ *
163
+ * Detection chain (12 §2):
164
+ * COMPLETED → move card to QA
165
+ * AUTO_CONFIRM → auto-confirm prompt, continue next tick
166
+ * NEEDS_INPUT → mark WAITING-CONFIRMATION, notify
167
+ * BLOCKED → mark BLOCKED
168
+ * ALIVE → no action (worker still working)
169
+ * DEAD → mark STALE-RUNTIME (handled by MonitorEngine)
170
+ * DEAD_EXCEEDED → mark STALE-RUNTIME, notify
171
+ */
172
+ async checkInprogressCard(card, opts) {
173
+ const seq = card.seq;
174
+ const state = readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
175
+ // Find this card's worker slot
176
+ const slotEntry = Object.entries(state.workers).find(([, w]) => w.seq === parseInt(seq, 10) && w.status === 'active');
177
+ if (!slotEntry) {
178
+ // No active slot — MonitorEngine handles orphan detection
179
+ return null;
180
+ }
181
+ const [slotName, slotState] = slotEntry;
182
+ const session = slotState.tmuxSession;
183
+ if (!session)
184
+ return null;
185
+ // Determine logDir for completion marker detection
186
+ const logDir = this.ctx.paths.logsDir;
187
+ const branch = slotState.branch || this.buildBranchName(card);
188
+ let workerStatus;
189
+ try {
190
+ workerStatus = await this.workerProvider.detectCompleted(session, logDir, branch);
191
+ }
192
+ catch (err) {
193
+ const msg = err instanceof Error ? err.message : String(err);
194
+ this.log.warn(`detectCompleted failed for seq ${seq}: ${msg}`);
195
+ return null;
196
+ }
197
+ switch (workerStatus) {
198
+ case 'COMPLETED': {
199
+ if (opts.dryRun) {
200
+ this.log.info(`[dry-run] Would move seq ${seq} Inprogress → QA`);
201
+ return { action: 'complete', entity: `seq:${seq}`, result: 'ok', message: 'dry-run: would move to QA' };
202
+ }
203
+ // Check if MR exists before moving to QA — worker may still be creating it
204
+ try {
205
+ const mrStatus = await this.repoBackend.getMrStatus(branch);
206
+ if (!mrStatus.exists) {
207
+ this.log.info(`seq ${seq}: Worker completed but MR not yet created, waiting`);
208
+ return null; // retry next tick
209
+ }
210
+ }
211
+ catch {
212
+ // Can't check MR — proceed anyway, closeout will handle it
213
+ }
214
+ // Move card to QA
215
+ try {
216
+ await this.taskBackend.move(seq, 'QA');
217
+ this.log.ok(`seq ${seq}: Worker completed, moved Inprogress → QA`);
218
+ // Update state.json
219
+ const freshState = readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
220
+ if (freshState.activeCards[seq]) {
221
+ freshState.activeCards[seq].state = 'QA';
222
+ writeState(this.ctx.paths.stateFile, freshState, 'pipeline-complete');
223
+ }
224
+ this.logEvent('complete', seq, 'ok', { worker: slotName });
225
+ if (this.notifier) {
226
+ await this.notifier.sendSuccess(`[${this.ctx.projectName}] seq:${seq} worker completed, moved to QA`).catch(() => { });
227
+ }
228
+ return { action: 'complete', entity: `seq:${seq}`, result: 'ok', message: 'Inprogress → QA (worker completed)' };
229
+ }
230
+ catch (err) {
231
+ const msg = err instanceof Error ? err.message : String(err);
232
+ this.log.error(`Failed to move seq ${seq} to QA: ${msg}`);
233
+ return { action: 'complete', entity: `seq:${seq}`, result: 'fail', message: `Move to QA failed: ${msg}` };
234
+ }
235
+ }
236
+ case 'AUTO_CONFIRM': {
237
+ // Non-destructive confirmation prompt → auto-confirm
238
+ this.log.info(`seq ${seq}: Worker waiting for non-destructive confirmation, auto-confirming`);
239
+ try {
240
+ await this.workerProvider.sendFix(session, 'y');
241
+ this.logEvent('auto-confirm', seq, 'ok');
242
+ if (this.notifier) {
243
+ await this.notifier.send(`[${this.ctx.projectName}] seq:${seq} auto-confirmed`, 'info').catch(() => { });
244
+ }
245
+ }
246
+ catch {
247
+ this.log.warn(`seq ${seq}: Auto-confirm failed`);
248
+ }
249
+ return { action: 'auto-confirm', entity: `seq:${seq}`, result: 'ok', message: 'Auto-confirmed non-destructive prompt' };
250
+ }
251
+ case 'NEEDS_INPUT': {
252
+ // Destructive confirmation → mark WAITING-CONFIRMATION, notify Boss
253
+ this.log.warn(`seq ${seq}: Worker waiting for destructive confirmation`);
254
+ try {
255
+ await this.taskBackend.addLabel(seq, 'WAITING-CONFIRMATION');
256
+ }
257
+ catch { /* best effort */ }
258
+ if (this.notifier) {
259
+ await this.notifier.sendWarning(`[${this.ctx.projectName}] seq:${seq} worker waiting for destructive confirmation`).catch(() => { });
260
+ }
261
+ this.logEvent('waiting-destructive', seq, 'ok');
262
+ return { action: 'mark-waiting', entity: `seq:${seq}`, result: 'ok', message: 'Destructive confirmation — waiting for human' };
263
+ }
264
+ case 'BLOCKED': {
265
+ this.log.warn(`seq ${seq}: Worker appears blocked`);
266
+ try {
267
+ await this.taskBackend.addLabel(seq, 'BLOCKED');
268
+ }
269
+ catch { /* best effort */ }
270
+ this.logEvent('blocked', seq, 'ok');
271
+ return { action: 'mark-blocked', entity: `seq:${seq}`, result: 'ok', message: 'Worker blocked' };
272
+ }
273
+ case 'DEAD':
274
+ case 'DEAD_EXCEEDED': {
275
+ // Worker session died — MonitorEngine handles STALE-RUNTIME in detail
276
+ this.log.warn(`seq ${seq}: Worker session dead (${workerStatus})`);
277
+ return null; // defer to MonitorEngine
278
+ }
279
+ case 'ALIVE':
280
+ default:
281
+ // Worker still running — no action needed
282
+ // Update heartbeat
283
+ try {
284
+ const freshState = readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
285
+ if (freshState.workers[slotName]) {
286
+ freshState.workers[slotName].lastHeartbeat = new Date().toISOString();
287
+ writeState(this.ctx.paths.stateFile, freshState, 'pipeline-heartbeat');
288
+ }
289
+ }
290
+ catch { /* non-fatal */ }
291
+ return null;
292
+ }
293
+ }
294
+ // ─── Prepare Phase (Backlog → Todo) ─────────────────────────────
295
+ /**
296
+ * Prepare a Backlog card: create branch, create worktree, move to Todo.
297
+ * Steps 1-3 per 01 §4.3.
298
+ */
299
+ async prepareCard(card, opts) {
300
+ const seq = card.seq;
301
+ const branchName = this.buildBranchName(card);
302
+ const worktreePath = resolveWorktreePath(this.ctx.projectName, seq);
303
+ if (opts.dryRun) {
304
+ this.log.info(`[dry-run] Would prepare seq ${seq}: branch=${branchName} worktree=${worktreePath}`);
305
+ return { action: 'prepare', entity: `seq:${seq}`, result: 'ok', message: 'dry-run' };
306
+ }
307
+ // Step 1: Create branch
308
+ try {
309
+ await this.repoBackend.ensureBranch(this.ctx.paths.repoDir, branchName, this.ctx.mergeBranch);
310
+ this.log.ok(`Step 1: Branch ${branchName} created for seq ${seq}`);
311
+ }
312
+ catch (err) {
313
+ const msg = err instanceof Error ? err.message : String(err);
314
+ this.log.error(`Step 1 failed (branch) for seq ${seq}: ${msg}`);
315
+ this.logEvent('prepare-branch', seq, 'fail', { error: msg });
316
+ return { action: 'prepare', entity: `seq:${seq}`, result: 'fail', message: `Branch creation failed: ${msg}` };
317
+ }
318
+ // Step 2: Create worktree
319
+ try {
320
+ await this.repoBackend.ensureWorktree(this.ctx.paths.repoDir, branchName, worktreePath);
321
+ this.log.ok(`Step 2: Worktree created for seq ${seq} at ${worktreePath}`);
322
+ }
323
+ catch (err) {
324
+ const msg = err instanceof Error ? err.message : String(err);
325
+ this.log.error(`Step 2 failed (worktree) for seq ${seq}: ${msg}`);
326
+ this.logEvent('prepare-worktree', seq, 'fail', { error: msg });
327
+ // Rollback: cleanup branch (best effort, branch may have existed before)
328
+ return { action: 'prepare', entity: `seq:${seq}`, result: 'fail', message: `Worktree creation failed: ${msg}` };
329
+ }
330
+ // Step 3: Move card to Todo
331
+ try {
332
+ await this.taskBackend.move(seq, 'Todo');
333
+ this.log.ok(`Step 3: Moved seq ${seq} Backlog → Todo`);
334
+ this.logEvent('prepare', seq, 'ok');
335
+ if (this.notifier) {
336
+ await this.notifier.send(`[${this.ctx.projectName}] seq:${seq} environment ready (Backlog → Todo)`, 'info').catch(() => { });
337
+ }
338
+ return { action: 'prepare', entity: `seq:${seq}`, result: 'ok', message: 'Backlog → Todo' };
339
+ }
340
+ catch (err) {
341
+ const msg = err instanceof Error ? err.message : String(err);
342
+ this.log.error(`Step 3 failed (move) for seq ${seq}: ${msg}`);
343
+ this.logEvent('prepare-move', seq, 'fail', { error: msg });
344
+ // Rollback: cleanup branch + worktree would be ideal but risky; log for manual cleanup
345
+ return { action: 'prepare', entity: `seq:${seq}`, result: 'fail', message: `Move to Todo failed: ${msg}` };
346
+ }
347
+ }
348
+ // ─── Launch Phase (Todo → Inprogress) ────────────────────────────
349
+ /**
350
+ * Launch a Todo card: claim slot, build context, start worker, move to Inprogress.
351
+ * Steps 4-7 per 01 §4.3.
352
+ */
353
+ async launchCard(card, opts, failedSlots = new Set()) {
354
+ const seq = card.seq;
355
+ const branchName = this.buildBranchName(card);
356
+ const worktreePath = resolveWorktreePath(this.ctx.projectName, seq);
357
+ if (opts.dryRun) {
358
+ this.log.info(`[dry-run] Would launch seq ${seq}`);
359
+ return { action: 'launch', entity: `seq:${seq}`, result: 'ok', message: 'dry-run' };
360
+ }
361
+ // Step 4: Claim worker slot
362
+ // Exclude slots that failed launch this tick to prevent repeated failures
363
+ const state = readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
364
+ const idleSlots = Object.entries(state.workers)
365
+ .filter(([name, w]) => w.status === 'idle' && !failedSlots.has(name));
366
+ if (idleSlots.length === 0) {
367
+ this.log.warn(`No idle worker slot available for seq ${seq}`);
368
+ return { action: 'launch', entity: `seq:${seq}`, result: 'skip', message: 'No idle worker slot' };
369
+ }
370
+ // Prefer slot with live session (Claude still running → context reuse)
371
+ let slotEntry = idleSlots[0];
372
+ if (this.ctx.config.WORKER_SESSION_REUSE) {
373
+ for (const entry of idleSlots) {
374
+ const [name] = entry;
375
+ const sessionName = `${this.ctx.projectName}-${name}`;
376
+ try {
377
+ const inspection = await this.workerProvider.inspect(sessionName);
378
+ if (inspection.alive) {
379
+ slotEntry = entry;
380
+ this.log.info(`Preferring slot ${name} with live session`);
381
+ break;
382
+ }
383
+ }
384
+ catch { /* ignore */ }
385
+ }
386
+ }
387
+ const [slotName] = slotEntry;
388
+ const sessionName = `${this.ctx.projectName}-${slotName}`;
389
+ // Claim slot in state.json
390
+ state.workers[slotName] = {
391
+ status: 'active',
392
+ seq: parseInt(seq, 10),
393
+ branch: branchName,
394
+ worktree: worktreePath,
395
+ tmuxSession: sessionName,
396
+ claimedAt: new Date().toISOString(),
397
+ lastHeartbeat: new Date().toISOString(),
398
+ };
399
+ // Add to active cards
400
+ const conflictDomains = card.labels
401
+ .filter((l) => l.startsWith('conflict:'))
402
+ .map((l) => l.slice('conflict:'.length));
403
+ state.activeCards[seq] = {
404
+ seq: parseInt(seq, 10),
405
+ state: 'Todo',
406
+ worker: slotName,
407
+ mrUrl: null,
408
+ conflictDomains,
409
+ startedAt: new Date().toISOString(),
410
+ };
411
+ try {
412
+ writeState(this.ctx.paths.stateFile, state, 'pipeline-launch');
413
+ this.log.ok(`Step 4: Claimed slot ${slotName} for seq ${seq}`);
414
+ }
415
+ catch (err) {
416
+ const msg = err instanceof Error ? err.message : String(err);
417
+ this.log.error(`Step 4 failed (claim) for seq ${seq}: ${msg}`);
418
+ return { action: 'launch', entity: `seq:${seq}`, result: 'fail', message: `Claim slot failed: ${msg}` };
419
+ }
420
+ // Also claim in PM backend
421
+ try {
422
+ await this.taskBackend.claim(seq, slotName);
423
+ }
424
+ catch (err) {
425
+ const msg = err instanceof Error ? err.message : String(err);
426
+ this.log.warn(`PM claim for seq ${seq} failed (non-fatal): ${msg}`);
427
+ }
428
+ // Step 5: Build task context (CLAUDE.md + .jarvis_task_prompt.txt)
429
+ try {
430
+ this.buildTaskContext(card, worktreePath);
431
+ this.log.ok(`Step 5: Task context built for seq ${seq}`);
432
+ }
433
+ catch (err) {
434
+ const msg = err instanceof Error ? err.message : String(err);
435
+ this.log.error(`Step 5 failed (context) for seq ${seq}: ${msg}`);
436
+ this.releaseSlot(slotName, seq);
437
+ this.logEvent('launch-context', seq, 'fail', { error: msg });
438
+ return { action: 'launch', entity: `seq:${seq}`, result: 'fail', message: `Context build failed: ${msg}` };
439
+ }
440
+ // Step 6: Launch worker
441
+ try {
442
+ await this.workerProvider.launch(sessionName, worktreePath);
443
+ // Wait for worker to be ready
444
+ const ready = await this.workerProvider.waitReady(sessionName, 90_000);
445
+ if (!ready) {
446
+ throw new Error('Worker did not become ready within timeout');
447
+ }
448
+ // Send task prompt
449
+ const promptFile = resolve(worktreePath, '.jarvis_task_prompt.txt');
450
+ await this.workerProvider.sendTask(sessionName, promptFile);
451
+ this.log.ok(`Step 6: Worker launched in session ${sessionName} for seq ${seq}`);
452
+ if (this.notifier) {
453
+ await this.notifier.sendSuccess(`[${this.ctx.projectName}] seq:${seq} worker started (${slotName})`).catch(() => { });
454
+ }
455
+ }
456
+ catch (err) {
457
+ const msg = err instanceof Error ? err.message : String(err);
458
+ this.log.error(`Step 6 failed (worker launch) for seq ${seq}: ${msg}`);
459
+ failedSlots.add(slotName);
460
+ this.releaseSlot(slotName, 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
+ // Step 7: Move card to Inprogress
465
+ try {
466
+ await this.taskBackend.move(seq, 'Inprogress');
467
+ // Update active card state
468
+ const freshState = readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
469
+ if (freshState.activeCards[seq]) {
470
+ freshState.activeCards[seq].state = 'Inprogress';
471
+ writeState(this.ctx.paths.stateFile, freshState, 'pipeline-launch');
472
+ }
473
+ this.log.ok(`Step 7: Moved seq ${seq} Todo → Inprogress`);
474
+ this.logEvent('launch', seq, 'ok', { worker: slotName, session: sessionName });
475
+ return { action: 'launch', entity: `seq:${seq}`, result: 'ok', message: `Todo → Inprogress (${slotName})` };
476
+ }
477
+ catch (err) {
478
+ const msg = err instanceof Error ? err.message : String(err);
479
+ this.log.error(`Step 7 failed (move) for seq ${seq}: ${msg}`);
480
+ // Rollback: stop worker, release slot
481
+ try {
482
+ await this.workerProvider.stop(sessionName);
483
+ }
484
+ catch { /* best effort */ }
485
+ this.releaseSlot(slotName, seq);
486
+ this.logEvent('launch-move', seq, 'fail', { error: msg });
487
+ return { action: 'launch', entity: `seq:${seq}`, result: 'fail', message: `Move to Inprogress failed: ${msg}` };
488
+ }
489
+ }
490
+ // ─── Helpers ─────────────────────────────────────────────────────
491
+ /**
492
+ * Build branch name from card: feature/<seq>-<slug>
493
+ */
494
+ buildBranchName(card) {
495
+ const slug = card.name
496
+ .toLowerCase()
497
+ .replace(/[^a-z0-9]+/g, '-')
498
+ .replace(/^-|-$/g, '')
499
+ .slice(0, 40);
500
+ return `feature/${card.seq}-${slug}`;
501
+ }
502
+ /**
503
+ * Write worker rules and task prompt to worktree.
504
+ * Generates both CLAUDE.md (for Claude) and AGENTS.md (for Codex).
505
+ */
506
+ buildTaskContext(card, worktreePath) {
507
+ if (!existsSync(worktreePath)) {
508
+ mkdirSync(worktreePath, { recursive: true });
509
+ }
510
+ const branchName = this.buildBranchName(card);
511
+ const workerRules = `# Task Rules (auto-generated, do not edit)
512
+
513
+ ## Scope
514
+ - ONLY work in this directory: ${worktreePath}
515
+ - Do NOT read or modify files outside this directory
516
+ - Do NOT explore the system, home directory, or other projects
517
+
518
+ ## Workflow
519
+ 1. Read the task description below
520
+ 2. Implement the changes in this directory
521
+ 3. Self-test your changes
522
+ 4. git add, commit, and push to branch: ${branchName}
523
+ 5. Create a Merge Request targeting ${this.ctx.mergeBranch}
524
+ 6. Output "done" when finished
525
+
526
+ ## Commit Rules
527
+ - Commit frequently (every meaningful change)
528
+ - Push after each commit
529
+ - Branch: ${branchName}
530
+ - Use conventional commit messages (feat:, fix:, etc.)
531
+
532
+ ## MR Creation
533
+ - Use git push first, then create MR via GitLab API:
534
+ curl -s -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" -H "Content-Type: application/json" \\
535
+ "$GITLAB_URL/api/v4/projects/${this.ctx.config.GITLAB_PROJECT_ID}/merge_requests" \\
536
+ -d '{"source_branch":"${branchName}","target_branch":"${this.ctx.mergeBranch}","title":"feat(${card.seq}): ${card.name}"}'
537
+
538
+ ## Forbidden
539
+ - No PLAN.md, TODO.md, TASKLIST.md, ROADMAP.md, NOTES.md
540
+ - No local planning files of any kind
541
+ - No changes outside task scope
542
+ - Do NOT explore ~/.projects or other system directories
543
+ `;
544
+ // .jarvis_task_prompt.txt — task prompt
545
+ const taskPrompt = `Task ID: ${card.seq}
546
+ Task: ${card.name}
547
+ Branch: ${branchName}
548
+ Card Full ID: ${card.id}
549
+
550
+ Description:
551
+ ${card.desc || '(no description)'}
552
+
553
+ Requirements:
554
+ 1. Implement the changes described above
555
+ 2. Self-test your changes
556
+ 3. git add, commit, and push to branch ${branchName}
557
+ 4. Create a Merge Request targeting ${this.ctx.mergeBranch}
558
+ 5. Say "done" when finished
559
+ `;
560
+ // Write both CLAUDE.md (for Claude) and AGENTS.md (for Codex)
561
+ writeFileSync(resolve(worktreePath, 'CLAUDE.md'), workerRules);
562
+ writeFileSync(resolve(worktreePath, 'AGENTS.md'), workerRules);
563
+ writeFileSync(resolve(worktreePath, '.jarvis_task_prompt.txt'), taskPrompt);
564
+ }
565
+ /**
566
+ * Release a worker slot, cleanup tmux session, remove card from active cards.
567
+ */
568
+ releaseSlot(slotName, seq) {
569
+ try {
570
+ // Kill tmux session if it exists (cleanup from failed launch)
571
+ const sessionName = `${this.ctx.projectName}-${slotName}`;
572
+ this.workerProvider.stop(sessionName).catch(() => { });
573
+ const state = readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
574
+ if (state.workers[slotName]) {
575
+ state.workers[slotName] = {
576
+ status: 'idle',
577
+ seq: null,
578
+ branch: null,
579
+ worktree: null,
580
+ tmuxSession: null,
581
+ claimedAt: null,
582
+ lastHeartbeat: null,
583
+ };
584
+ }
585
+ delete state.activeCards[seq];
586
+ writeState(this.ctx.paths.stateFile, state, 'pipeline-release');
587
+ this.taskBackend.releaseClaim(seq).catch(() => { });
588
+ }
589
+ catch {
590
+ this.log.warn(`Failed to release slot ${slotName} for seq ${seq}`);
591
+ }
592
+ }
593
+ logEvent(action, seq, result, meta) {
594
+ this.log.event({
595
+ component: 'pipeline',
596
+ action,
597
+ entity: `seq:${seq}`,
598
+ result,
599
+ meta,
600
+ });
601
+ }
602
+ }
603
+ //# sourceMappingURL=ExecutionEngine.js.map