@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.
- package/dist/commands/cardAdd.d.ts +2 -0
- package/dist/commands/cardAdd.d.ts.map +1 -0
- package/dist/commands/cardAdd.js +65 -0
- package/dist/commands/cardAdd.js.map +1 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +264 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/monitorTick.d.ts +2 -0
- package/dist/commands/monitorTick.d.ts.map +1 -0
- package/dist/commands/monitorTick.js +47 -0
- package/dist/commands/monitorTick.js.map +1 -0
- package/dist/commands/pipelineTick.d.ts +2 -0
- package/dist/commands/pipelineTick.d.ts.map +1 -0
- package/dist/commands/pipelineTick.js +44 -0
- package/dist/commands/pipelineTick.js.map +1 -0
- package/dist/commands/pmCommand.d.ts +2 -0
- package/dist/commands/pmCommand.d.ts.map +1 -0
- package/dist/commands/pmCommand.js +159 -0
- package/dist/commands/pmCommand.js.map +1 -0
- package/dist/commands/projectInit.d.ts +2 -0
- package/dist/commands/projectInit.d.ts.map +1 -0
- package/dist/commands/projectInit.js +75 -0
- package/dist/commands/projectInit.js.map +1 -0
- package/dist/commands/qaTick.d.ts +2 -0
- package/dist/commands/qaTick.d.ts.map +1 -0
- package/dist/commands/qaTick.js +43 -0
- package/dist/commands/qaTick.js.map +1 -0
- package/dist/commands/schedulerTick.d.ts +2 -0
- package/dist/commands/schedulerTick.d.ts.map +1 -0
- package/dist/commands/schedulerTick.js +45 -0
- package/dist/commands/schedulerTick.js.map +1 -0
- package/dist/commands/tick.d.ts +14 -0
- package/dist/commands/tick.d.ts.map +1 -0
- package/dist/commands/tick.js +251 -0
- package/dist/commands/tick.js.map +1 -0
- package/dist/commands/workerLaunch.d.ts +2 -0
- package/dist/commands/workerLaunch.d.ts.map +1 -0
- package/dist/commands/workerLaunch.js +56 -0
- package/dist/commands/workerLaunch.js.map +1 -0
- package/dist/core/config.d.ts +38 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +131 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +28 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/lock.d.ts +14 -0
- package/dist/core/lock.d.ts.map +1 -0
- package/dist/core/lock.js +65 -0
- package/dist/core/lock.js.map +1 -0
- package/dist/core/logger.d.ts +24 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +62 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paths.d.ts +27 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +29 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/queue.d.ts +14 -0
- package/dist/core/queue.d.ts.map +1 -0
- package/dist/core/queue.js +38 -0
- package/dist/core/queue.js.map +1 -0
- package/dist/core/state.d.ts +32 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +52 -0
- package/dist/core/state.js.map +1 -0
- package/dist/engines/CloseoutEngine.d.ts +60 -0
- package/dist/engines/CloseoutEngine.d.ts.map +1 -0
- package/dist/engines/CloseoutEngine.js +596 -0
- package/dist/engines/CloseoutEngine.js.map +1 -0
- package/dist/engines/ExecutionEngine.d.ts +65 -0
- package/dist/engines/ExecutionEngine.d.ts.map +1 -0
- package/dist/engines/ExecutionEngine.js +603 -0
- package/dist/engines/ExecutionEngine.js.map +1 -0
- package/dist/engines/MonitorEngine.d.ts +39 -0
- package/dist/engines/MonitorEngine.d.ts.map +1 -0
- package/dist/engines/MonitorEngine.js +473 -0
- package/dist/engines/MonitorEngine.js.map +1 -0
- package/dist/engines/SchedulerEngine.d.ts +24 -0
- package/dist/engines/SchedulerEngine.d.ts.map +1 -0
- package/dist/engines/SchedulerEngine.js +195 -0
- package/dist/engines/SchedulerEngine.js.map +1 -0
- package/dist/interfaces/HookProvider.d.ts +9 -0
- package/dist/interfaces/HookProvider.d.ts.map +1 -0
- package/dist/interfaces/HookProvider.js +2 -0
- package/dist/interfaces/HookProvider.js.map +1 -0
- package/dist/interfaces/Notifier.d.ts +11 -0
- package/dist/interfaces/Notifier.d.ts.map +1 -0
- package/dist/interfaces/Notifier.js +2 -0
- package/dist/interfaces/Notifier.js.map +1 -0
- package/dist/interfaces/RepoBackend.d.ts +23 -0
- package/dist/interfaces/RepoBackend.d.ts.map +1 -0
- package/dist/interfaces/RepoBackend.js +2 -0
- package/dist/interfaces/RepoBackend.js.map +1 -0
- package/dist/interfaces/TaskBackend.d.ts +24 -0
- package/dist/interfaces/TaskBackend.d.ts.map +1 -0
- package/dist/interfaces/TaskBackend.js +2 -0
- package/dist/interfaces/TaskBackend.js.map +1 -0
- package/dist/interfaces/WorkerProvider.d.ts +23 -0
- package/dist/interfaces/WorkerProvider.d.ts.map +1 -0
- package/dist/interfaces/WorkerProvider.js +2 -0
- package/dist/interfaces/WorkerProvider.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +226 -0
- package/dist/main.js.map +1 -0
- package/dist/models/types.d.ts +68 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +2 -0
- package/dist/models/types.js.map +1 -0
- package/dist/providers/ClaudeWorkerProvider.d.ts +84 -0
- package/dist/providers/ClaudeWorkerProvider.d.ts.map +1 -0
- package/dist/providers/ClaudeWorkerProvider.js +293 -0
- package/dist/providers/ClaudeWorkerProvider.js.map +1 -0
- package/dist/providers/CodexWorkerProvider.d.ts +50 -0
- package/dist/providers/CodexWorkerProvider.d.ts.map +1 -0
- package/dist/providers/CodexWorkerProvider.js +275 -0
- package/dist/providers/CodexWorkerProvider.js.map +1 -0
- package/dist/providers/GitLabRepoBackend.d.ts +42 -0
- package/dist/providers/GitLabRepoBackend.d.ts.map +1 -0
- package/dist/providers/GitLabRepoBackend.js +280 -0
- package/dist/providers/GitLabRepoBackend.js.map +1 -0
- package/dist/providers/MarkdownTaskBackend.d.ts +88 -0
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -0
- package/dist/providers/MarkdownTaskBackend.js +414 -0
- package/dist/providers/MarkdownTaskBackend.js.map +1 -0
- package/dist/providers/MatrixNotifier.d.ts +30 -0
- package/dist/providers/MatrixNotifier.d.ts.map +1 -0
- package/dist/providers/MatrixNotifier.js +82 -0
- package/dist/providers/MatrixNotifier.js.map +1 -0
- package/dist/providers/PlaneTaskBackend.d.ts +86 -0
- package/dist/providers/PlaneTaskBackend.d.ts.map +1 -0
- package/dist/providers/PlaneTaskBackend.js +409 -0
- package/dist/providers/PlaneTaskBackend.js.map +1 -0
- package/dist/providers/TrelloTaskBackend.d.ts +53 -0
- package/dist/providers/TrelloTaskBackend.d.ts.map +1 -0
- package/dist/providers/TrelloTaskBackend.js +300 -0
- package/dist/providers/TrelloTaskBackend.js.map +1 -0
- package/dist/providers/registry.d.ts +10 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +29 -0
- package/dist/providers/registry.js.map +1 -0
- 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
|