@coralai/sps-cli 0.23.12 → 0.23.13
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/README.md +3 -3
- package/dist/core/runtimeStore.d.ts +44 -0
- package/dist/core/runtimeStore.d.ts.map +1 -0
- package/dist/core/runtimeStore.js +104 -0
- package/dist/core/runtimeStore.js.map +1 -0
- package/dist/engines/CloseoutEngine.d.ts +1 -0
- package/dist/engines/CloseoutEngine.d.ts.map +1 -1
- package/dist/engines/CloseoutEngine.js +110 -104
- package/dist/engines/CloseoutEngine.js.map +1 -1
- package/dist/engines/MonitorEngine.d.ts +1 -0
- package/dist/engines/MonitorEngine.d.ts.map +1 -1
- package/dist/engines/MonitorEngine.js +118 -123
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> **中文文档**: See `README-CN.md` in the source repository for Chinese documentation.
|
|
6
6
|
|
|
7
|
-
**v0.23.
|
|
7
|
+
**v0.23.13**
|
|
8
8
|
|
|
9
9
|
SPS (Smart Pipeline System) is a fully automated development pipeline CLI tool driven by AI Agents. From task card creation to code merging, the entire process runs unattended.
|
|
10
10
|
|
|
@@ -121,9 +121,9 @@ Planning -> Backlog -> Todo -> Inprogress -> Done
|
|
|
121
121
|
| Todo -> Inprogress | ExecutionEngine | Assign Worker slot, build task context, launch AI Worker |
|
|
122
122
|
| Inprogress -> Done | PostActions + MergeMutex | Detect Worker completion, serialize merge to target branch, release resources, clean up worktree |
|
|
123
123
|
|
|
124
|
-
The Worker no longer executes `.sps/merge.sh` as the normal path. In `MR_MODE=none`, the Worker commits and pushes the feature branch, then SPS closeout performs a serialized merge. Final integration now runs inside a temporary detached merge worktree, which avoids `main already used by worktree` failures and keeps the user's main checkout untouched. `.sps/merge.sh` remains only as a manual fallback. See `docs/design/10-acp-worker-runtime-design.md` for the persistent Agent transport model, the full worker state breakdown, and the local same-user OAuth reuse boundary. See `docs/design/11-runtime-state-authority-and-recovery-redesign.md` for the
|
|
124
|
+
The Worker no longer executes `.sps/merge.sh` as the normal path. In `MR_MODE=none`, the Worker commits and pushes the feature branch, then SPS closeout performs a serialized merge. Final integration now runs inside a temporary detached merge worktree, which avoids `main already used by worktree` failures and keeps the user's main checkout untouched. `.sps/merge.sh` remains only as a manual fallback. See `docs/design/10-acp-worker-runtime-design.md` for the persistent Agent transport model, the full worker state breakdown, and the local same-user OAuth reuse boundary. See `docs/design/11-runtime-state-authority-and-recovery-redesign.md` for the redesign that demotes `state.json` / `acp-state.json` to projections and re-centers recovery around PM state plus worktree/git evidence. See `docs/design/12-unified-runtime-state-machine.md` for the next-step module cleanup plan that converges `Execution`, `Recovery`, `Monitor`, `Closeout`, and `PostActions` into one state machine authority plus a single runtime coordinator/write path.
|
|
125
125
|
|
|
126
|
-
PTY transport now extends the same session/run model into the default CLI-backed worker path. When `WORKER_TRANSPORT=pty` or `WORKER_TRANSPORT=acp`, `sps tick` launches work through `sessionId/runId`, persists that state into `runtime/state.json` plus `runtime/acp-state.json`, and lets recovery/status/dashboard inspect the live session/run state. `PostActions` retry/conflict flows and `CloseoutEngine` autofix/conflict flows now resume the same session when possible and automatically rebuild a fresh persistent session when the original one is gone before a retry or merge-conflict follow-up run. v0.23.5 upgrades `node-pty` to a macOS-safe build, auto-restores missing execute permissions on `spawn-helper`, and auto-skips the benign Codex update notice during PTY boot so `ensureSession()` can reach `ready` on fresh launches instead of failing with `posix_spawnp failed`. v0.23.6 further fixes PTY run lifecycle tracking so newly submitted conflict-resolution runs no longer get marked `completed` during the first inspect cycle before the CLI actually leaves the prompt. v0.23.7 hardens `sps worker dashboard` itself by switching PTY/TUI panels from raw pane dumps to structured worker summaries, preventing Codex/Claude full-screen redraw artifacts from corrupting the dashboard layout while also counting PTY-backed active sessions correctly in the summary bar. v0.23.9 now validates persisted PTY sessions by PID before treating them as alive, which keeps `sps status`, `sps worker dashboard`, and `sps card dashboard` from showing dead workers as active after `sps tick` has already stopped. v0.23.10 closes the remaining state-drift loop without taking ordering authority away from Plane/Trello/Markdown: the PM backend remains the first principle for task order and human drag/drop edits, while SPS only reasserts `Todo` / `Inprogress` for cards still locally owned by a non-idle runtime slot. `MonitorEngine` now includes runtime-owned in-progress cards even when PM has drifted, and `sps card dashboard` fetches all cards in a single backend round-trip while sharing the same `active/merging/stale` worker buckets as `sps status` and `sps worker dashboard`, reducing self-inflicted Plane `429` spikes. v0.23.11 begins landing the state-authority redesign itself: `RuntimeCoordinator` can now build a PM + worktree + session-first runtime projection without persisting it, tick startup persists that projection before recovery, and all read-only views now consume a shared snapshot path instead of mutating `state.json` / `acp-state.json` during display refreshes. v0.23.12 then moves the execution path onto the same model: `Recovery` now restores tasks from `TaskLease + WorktreeEvidence` instead of old slot snapshots, and `ExecutionEngine` / `PostActions` / `MonitorEngine` now treat `lease.phase` and `lease.retryCount` as the authoritative runtime metadata while `activeCards` remains a derived projection for compatibility. The state file writers now also use unique temporary filenames so overlapping writes do not collide on a shared `.tmp` path. Codex has been verified on launch, recovery, direct merge, same-session resume, PTY conflict fallback, spawn-helper self-heal, immediate post-launch run-state inspection, summary-style dashboard rendering, cold-state liveness validation, runtime-owned PM reconciliation, read-only snapshot rendering, and lease-first recovery/execution flow; Claude still depends on host-side `claude auth login` before reaching `ready`.
|
|
126
|
+
PTY transport now extends the same session/run model into the default CLI-backed worker path. When `WORKER_TRANSPORT=pty` or `WORKER_TRANSPORT=acp`, `sps tick` launches work through `sessionId/runId`, persists that state into `runtime/state.json` plus `runtime/acp-state.json`, and lets recovery/status/dashboard inspect the live session/run state. `PostActions` retry/conflict flows and `CloseoutEngine` autofix/conflict flows now resume the same session when possible and automatically rebuild a fresh persistent session when the original one is gone before a retry or merge-conflict follow-up run. v0.23.5 upgrades `node-pty` to a macOS-safe build, auto-restores missing execute permissions on `spawn-helper`, and auto-skips the benign Codex update notice during PTY boot so `ensureSession()` can reach `ready` on fresh launches instead of failing with `posix_spawnp failed`. v0.23.6 further fixes PTY run lifecycle tracking so newly submitted conflict-resolution runs no longer get marked `completed` during the first inspect cycle before the CLI actually leaves the prompt. v0.23.7 hardens `sps worker dashboard` itself by switching PTY/TUI panels from raw pane dumps to structured worker summaries, preventing Codex/Claude full-screen redraw artifacts from corrupting the dashboard layout while also counting PTY-backed active sessions correctly in the summary bar. v0.23.9 now validates persisted PTY sessions by PID before treating them as alive, which keeps `sps status`, `sps worker dashboard`, and `sps card dashboard` from showing dead workers as active after `sps tick` has already stopped. v0.23.10 closes the remaining state-drift loop without taking ordering authority away from Plane/Trello/Markdown: the PM backend remains the first principle for task order and human drag/drop edits, while SPS only reasserts `Todo` / `Inprogress` for cards still locally owned by a non-idle runtime slot. `MonitorEngine` now includes runtime-owned in-progress cards even when PM has drifted, and `sps card dashboard` fetches all cards in a single backend round-trip while sharing the same `active/merging/stale` worker buckets as `sps status` and `sps worker dashboard`, reducing self-inflicted Plane `429` spikes. v0.23.11 begins landing the state-authority redesign itself: `RuntimeCoordinator` can now build a PM + worktree + session-first runtime projection without persisting it, tick startup persists that projection before recovery, and all read-only views now consume a shared snapshot path instead of mutating `state.json` / `acp-state.json` during display refreshes. v0.23.12 then moves the execution path onto the same model: `Recovery` now restores tasks from `TaskLease + WorktreeEvidence` instead of old slot snapshots, and `ExecutionEngine` / `PostActions` / `MonitorEngine` now treat `lease.phase` and `lease.retryCount` as the authoritative runtime metadata while `activeCards` remains a derived projection for compatibility. v0.23.13 extends that cleanup into the remaining control loops: `MonitorEngine` now walks live leases instead of active slots for health, timeout, stale, waiting, and alignment checks; `CloseoutEngine` now releases completed tasks through `RuntimeStore.releaseTaskProjection()`; and `RuntimeStore` becomes the shared mutation surface for monitor/closeout cleanup instead of direct `state.json` rewrites. The state file writers now also use unique temporary filenames so overlapping writes do not collide on a shared `.tmp` path. Codex has been verified on launch, recovery, direct merge, same-session resume, PTY conflict fallback, spawn-helper self-heal, immediate post-launch run-state inspection, summary-style dashboard rendering, cold-state liveness validation, runtime-owned PM reconciliation, read-only snapshot rendering, and lease-first recovery/execution/monitor/closeout flow; Claude still depends on host-side `claude auth login` before reaching `ready`.
|
|
127
127
|
|
|
128
128
|
### MR_MODE=create (Optional)
|
|
129
129
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ProjectContext } from './context.js';
|
|
2
|
+
import { type ActiveCardState, type RuntimeState, type TaskLease, type WorkerSlotState, type WorktreeEvidence } from './state.js';
|
|
3
|
+
import type { ACPState, ACPSessionRecord } from '../models/acp.js';
|
|
4
|
+
export interface TaskRuntimeView {
|
|
5
|
+
seq: string;
|
|
6
|
+
lease: TaskLease | null;
|
|
7
|
+
evidence: WorktreeEvidence | null;
|
|
8
|
+
activeCard: ActiveCardState | null;
|
|
9
|
+
slotName: string | null;
|
|
10
|
+
slot: WorkerSlotState | null;
|
|
11
|
+
session: ACPSessionRecord | null;
|
|
12
|
+
}
|
|
13
|
+
type RuntimePaths = Pick<ProjectContext, 'paths' | 'maxWorkers'>;
|
|
14
|
+
export declare class RuntimeStore {
|
|
15
|
+
private readonly ctx;
|
|
16
|
+
constructor(ctx: RuntimePaths);
|
|
17
|
+
readState(): RuntimeState;
|
|
18
|
+
readACPState(): ACPState;
|
|
19
|
+
read(): {
|
|
20
|
+
state: RuntimeState;
|
|
21
|
+
acpState: ACPState;
|
|
22
|
+
};
|
|
23
|
+
updateState(updatedBy: string, mutator: (state: RuntimeState) => void): RuntimeState;
|
|
24
|
+
updateACPState(updatedBy: string, mutator: (acpState: ACPState) => void): ACPState;
|
|
25
|
+
updateRuntime(updatedBy: string, mutator: (state: RuntimeState, acpState: ACPState) => void): {
|
|
26
|
+
state: RuntimeState;
|
|
27
|
+
acpState: ACPState;
|
|
28
|
+
};
|
|
29
|
+
getTask(seq: string, state?: RuntimeState, acpState?: ACPState): TaskRuntimeView;
|
|
30
|
+
findSlotForTask(seq: string, state: RuntimeState): string | null;
|
|
31
|
+
findAvailableSlot(state: RuntimeState, options?: {
|
|
32
|
+
preferred?: string | null;
|
|
33
|
+
exclude?: Set<string>;
|
|
34
|
+
}): string | null;
|
|
35
|
+
clearWorkerSlot(state: RuntimeState, slotName: string): void;
|
|
36
|
+
releaseTaskProjection(state: RuntimeState, seq: string, options?: {
|
|
37
|
+
dropLease?: boolean;
|
|
38
|
+
phase?: TaskLease['phase'];
|
|
39
|
+
keepWorktree?: boolean;
|
|
40
|
+
pmStateObserved?: TaskLease['pmStateObserved'];
|
|
41
|
+
}): void;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=runtimeStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtimeStore.d.ts","sourceRoot":"","sources":["../../src/core/runtimeStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAClC;AAED,KAAK,YAAY,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,YAAY,CAAC,CAAC;AAEjE,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,YAAY;IAE9C,SAAS,IAAI,YAAY;IAIzB,YAAY,IAAI,QAAQ;IAIxB,IAAI,IAAI;QAAE,KAAK,EAAE,YAAY,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE;IAOnD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,YAAY;IAOpF,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,GAAG,QAAQ;IAOlF,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,KAAK,IAAI,GACzD;QAAE,KAAK,EAAE,YAAY,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE;IAS9C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,eAAe;IAuBhF,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI;IAShE,iBAAiB,CACf,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;KAAO,GACjE,MAAM,GAAG,IAAI;IAgBhB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI5D,qBAAqB,CACnB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;QACP,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,KAAK,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;QAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,eAAe,CAAC,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC;KAC3C,GACL,IAAI;CA2BR"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readACPState, writeACPState } from './acpState.js';
|
|
2
|
+
import { createIdleWorkerSlot, readState, writeState, } from './state.js';
|
|
3
|
+
export class RuntimeStore {
|
|
4
|
+
ctx;
|
|
5
|
+
constructor(ctx) {
|
|
6
|
+
this.ctx = ctx;
|
|
7
|
+
}
|
|
8
|
+
readState() {
|
|
9
|
+
return readState(this.ctx.paths.stateFile, this.ctx.maxWorkers);
|
|
10
|
+
}
|
|
11
|
+
readACPState() {
|
|
12
|
+
return readACPState(this.ctx.paths.acpStateFile);
|
|
13
|
+
}
|
|
14
|
+
read() {
|
|
15
|
+
return {
|
|
16
|
+
state: this.readState(),
|
|
17
|
+
acpState: this.readACPState(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
updateState(updatedBy, mutator) {
|
|
21
|
+
const state = this.readState();
|
|
22
|
+
mutator(state);
|
|
23
|
+
writeState(this.ctx.paths.stateFile, state, updatedBy);
|
|
24
|
+
return state;
|
|
25
|
+
}
|
|
26
|
+
updateACPState(updatedBy, mutator) {
|
|
27
|
+
const acpState = this.readACPState();
|
|
28
|
+
mutator(acpState);
|
|
29
|
+
writeACPState(this.ctx.paths.acpStateFile, acpState, updatedBy);
|
|
30
|
+
return acpState;
|
|
31
|
+
}
|
|
32
|
+
updateRuntime(updatedBy, mutator) {
|
|
33
|
+
const state = this.readState();
|
|
34
|
+
const acpState = this.readACPState();
|
|
35
|
+
mutator(state, acpState);
|
|
36
|
+
writeState(this.ctx.paths.stateFile, state, updatedBy);
|
|
37
|
+
writeACPState(this.ctx.paths.acpStateFile, acpState, updatedBy);
|
|
38
|
+
return { state, acpState };
|
|
39
|
+
}
|
|
40
|
+
getTask(seq, state, acpState) {
|
|
41
|
+
const runtimeState = state ?? this.readState();
|
|
42
|
+
const runtimeACP = acpState ?? this.readACPState();
|
|
43
|
+
const key = String(seq);
|
|
44
|
+
const lease = runtimeState.leases[key] || null;
|
|
45
|
+
const slotName = lease?.slot ||
|
|
46
|
+
Object.entries(runtimeState.workers).find(([, worker]) => worker.seq === parseInt(key, 10))?.[0] ||
|
|
47
|
+
null;
|
|
48
|
+
const slot = slotName ? runtimeState.workers[slotName] || null : null;
|
|
49
|
+
const session = slotName ? runtimeACP.sessions[slotName] || null : null;
|
|
50
|
+
return {
|
|
51
|
+
seq: key,
|
|
52
|
+
lease,
|
|
53
|
+
evidence: runtimeState.worktreeEvidence[key] || null,
|
|
54
|
+
activeCard: runtimeState.activeCards[key] || null,
|
|
55
|
+
slotName,
|
|
56
|
+
slot,
|
|
57
|
+
session,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
findSlotForTask(seq, state) {
|
|
61
|
+
const key = String(seq);
|
|
62
|
+
return (state.leases[key]?.slot ||
|
|
63
|
+
Object.entries(state.workers).find(([, worker]) => worker.seq === parseInt(key, 10))?.[0] ||
|
|
64
|
+
null);
|
|
65
|
+
}
|
|
66
|
+
findAvailableSlot(state, options = {}) {
|
|
67
|
+
const exclude = options.exclude ?? new Set();
|
|
68
|
+
if (options.preferred) {
|
|
69
|
+
const preferred = state.workers[options.preferred];
|
|
70
|
+
if (preferred && preferred.status === 'idle' && !exclude.has(options.preferred)) {
|
|
71
|
+
return options.preferred;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return (Object.entries(state.workers).find(([slotName, worker]) => worker.status === 'idle' && !exclude.has(slotName))?.[0] || null);
|
|
75
|
+
}
|
|
76
|
+
clearWorkerSlot(state, slotName) {
|
|
77
|
+
state.workers[slotName] = createIdleWorkerSlot();
|
|
78
|
+
}
|
|
79
|
+
releaseTaskProjection(state, seq, options = {}) {
|
|
80
|
+
const slotName = this.findSlotForTask(seq, state);
|
|
81
|
+
if (slotName) {
|
|
82
|
+
this.clearWorkerSlot(state, slotName);
|
|
83
|
+
}
|
|
84
|
+
delete state.activeCards[seq];
|
|
85
|
+
if (options.dropLease) {
|
|
86
|
+
delete state.leases[seq];
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const lease = state.leases[seq];
|
|
90
|
+
if (!lease)
|
|
91
|
+
return;
|
|
92
|
+
lease.slot = null;
|
|
93
|
+
lease.sessionId = null;
|
|
94
|
+
lease.runId = null;
|
|
95
|
+
lease.phase = options.phase ?? 'suspended';
|
|
96
|
+
lease.pmStateObserved = options.pmStateObserved ?? lease.pmStateObserved;
|
|
97
|
+
if (!options.keepWorktree) {
|
|
98
|
+
lease.worktree = null;
|
|
99
|
+
lease.branch = null;
|
|
100
|
+
}
|
|
101
|
+
lease.lastTransitionAt = new Date().toISOString();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=runtimeStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtimeStore.js","sourceRoot":"","sources":["../../src/core/runtimeStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,7 @@ export declare class CloseoutEngine {
|
|
|
27
27
|
private notifier?;
|
|
28
28
|
private agentRuntime;
|
|
29
29
|
private log;
|
|
30
|
+
private runtimeStore;
|
|
30
31
|
constructor(ctx: ProjectContext, taskBackend: TaskBackend, repoBackend: RepoBackend, workerProvider: WorkerProvider, notifier?: Notifier | undefined, agentRuntime?: AgentRuntime | null);
|
|
31
32
|
tick(): Promise<CommandResult>;
|
|
32
33
|
private processQaCard;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CloseoutEngine.d.ts","sourceRoot":"","sources":["../../src/engines/CloseoutEngine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,aAAa,EAAyC,MAAM,oBAAoB,CAAC;AAK/F;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAc;
|
|
1
|
+
{"version":3,"file":"CloseoutEngine.d.ts","sourceRoot":"","sources":["../../src/engines/CloseoutEngine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,aAAa,EAAyC,MAAM,oBAAoB,CAAC;AAK/F;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAc;IAKvB,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,QAAQ,CAAC;IACjB,OAAO,CAAC,YAAY;IATtB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,YAAY,CAAe;gBAGzB,GAAG,EAAE,cAAc,EACnB,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,cAAc,EAC9B,QAAQ,CAAC,EAAE,QAAQ,YAAA,EACnB,YAAY,GAAE,YAAY,GAAG,IAAW;IAM5C,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;YAoEtB,aAAa;YAwEb,aAAa;YAqHb,eAAe;YAkIf,YAAY;IAyC1B;;;;;;;;;;OAUG;YACW,cAAc;IAsH5B;;;;;OAKG;YACW,eAAe;IA0I7B;;;;;OAKG;YACW,gBAAgB;IA4C9B,OAAO,CAAC,eAAe;YAST,YAAY;YAMZ,YAAY;YASZ,WAAW;YASX,UAAU;YAYV,eAAe;IAyD7B,OAAO,CAAC,QAAQ;CAcjB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RuntimeStore } from '../core/runtimeStore.js';
|
|
2
2
|
import { resolveWorktreePath } from '../core/paths.js';
|
|
3
3
|
import { Logger } from '../core/logger.js';
|
|
4
4
|
/**
|
|
@@ -23,6 +23,7 @@ export class CloseoutEngine {
|
|
|
23
23
|
notifier;
|
|
24
24
|
agentRuntime;
|
|
25
25
|
log;
|
|
26
|
+
runtimeStore;
|
|
26
27
|
constructor(ctx, taskBackend, repoBackend, workerProvider, notifier, agentRuntime = null) {
|
|
27
28
|
this.ctx = ctx;
|
|
28
29
|
this.taskBackend = taskBackend;
|
|
@@ -31,6 +32,7 @@ export class CloseoutEngine {
|
|
|
31
32
|
this.notifier = notifier;
|
|
32
33
|
this.agentRuntime = agentRuntime;
|
|
33
34
|
this.log = new Logger('qa', ctx.projectName, ctx.paths.logsDir);
|
|
35
|
+
this.runtimeStore = new RuntimeStore(ctx);
|
|
34
36
|
}
|
|
35
37
|
async tick() {
|
|
36
38
|
const actions = [];
|
|
@@ -274,37 +276,41 @@ export class CloseoutEngine {
|
|
|
274
276
|
}
|
|
275
277
|
const autofixAttempts = typeof meta.autofixAttempts === 'number' ? meta.autofixAttempts : 0;
|
|
276
278
|
if (autofixAttempts < maxAttempts) {
|
|
277
|
-
// Try self-repair:
|
|
278
|
-
const state =
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
279
|
+
// Try self-repair: prefer lease/worktree evidence, then reuse or rebuild a worker slot.
|
|
280
|
+
const state = this.runtimeStore.readState();
|
|
281
|
+
const runtime = this.runtimeStore.getTask(seq, state);
|
|
282
|
+
const slotName = runtime.slotName ||
|
|
283
|
+
this.runtimeStore.findAvailableSlot(state);
|
|
284
|
+
const slotState = slotName ? state.workers[slotName] || null : null;
|
|
285
|
+
const session = slotState?.tmuxSession || null;
|
|
286
|
+
const worktree = runtime.lease?.worktree || runtime.evidence?.worktree || slotState?.worktree || '';
|
|
287
|
+
const isPrintMode = slotState?.mode === 'print';
|
|
288
|
+
const isAcpMode = !!slotState &&
|
|
289
|
+
(slotState.transport === 'acp' ||
|
|
285
290
|
slotState.transport === 'pty' ||
|
|
286
291
|
slotState.mode === 'acp' ||
|
|
287
|
-
slotState.mode === 'pty';
|
|
292
|
+
slotState.mode === 'pty');
|
|
293
|
+
if (slotName && (session || (isAcpMode && this.agentRuntime && worktree))) {
|
|
288
294
|
try {
|
|
289
295
|
const fixPrompt = `CI pipeline has failed. Please review the CI logs, fix the issues, commit, and push. This is autofix attempt ${autofixAttempts + 1} of ${maxAttempts}.`;
|
|
290
296
|
if (isAcpMode && this.agentRuntime) {
|
|
291
|
-
await this.resumeAcpWorker(slotName, seq,
|
|
297
|
+
await this.resumeAcpWorker(slotName, seq, worktree, branchName, fixPrompt, 'active', 'closeout-autofix-resume');
|
|
292
298
|
}
|
|
293
299
|
else if (isPrintMode) {
|
|
294
300
|
// Print mode: spawn new process with --resume (process already exited)
|
|
295
|
-
const resumeResult = await this.workerProvider.sendFix(session, fixPrompt, slotState
|
|
301
|
+
const resumeResult = await this.workerProvider.sendFix(session, fixPrompt, slotState?.sessionId || undefined);
|
|
296
302
|
// Update state with new process info
|
|
297
303
|
if (resumeResult && typeof resumeResult === 'object' && 'pid' in resumeResult) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
+
this.runtimeStore.updateState('closeout-autofix-resume', (freshState) => {
|
|
305
|
+
if (freshState.workers[slotName]) {
|
|
306
|
+
freshState.workers[slotName].pid = resumeResult.pid;
|
|
307
|
+
freshState.workers[slotName].outputFile = resumeResult.outputFile;
|
|
308
|
+
if (resumeResult.sessionId) {
|
|
309
|
+
freshState.workers[slotName].sessionId = resumeResult.sessionId;
|
|
310
|
+
}
|
|
311
|
+
freshState.workers[slotName].exitCode = null;
|
|
304
312
|
}
|
|
305
|
-
|
|
306
|
-
writeState(this.ctx.paths.stateFile, freshState, 'closeout-autofix-resume');
|
|
307
|
-
}
|
|
313
|
+
});
|
|
308
314
|
}
|
|
309
315
|
}
|
|
310
316
|
else {
|
|
@@ -427,36 +433,17 @@ export class CloseoutEngine {
|
|
|
427
433
|
errors.push(`release-claim: ${msg}`);
|
|
428
434
|
}
|
|
429
435
|
// Step 3: Release worker slot in state.json
|
|
430
|
-
const state =
|
|
431
|
-
const
|
|
436
|
+
const state = this.runtimeStore.readState();
|
|
437
|
+
const runtime = this.runtimeStore.getTask(seq, state);
|
|
438
|
+
const slotEntry = runtime.slotName && runtime.slot ? [runtime.slotName, runtime.slot] : null;
|
|
432
439
|
let sessionName = null;
|
|
433
440
|
if (slotEntry) {
|
|
434
441
|
const [slotName, slotState] = slotEntry;
|
|
435
442
|
sessionName = slotState.tmuxSession;
|
|
436
443
|
try {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
branch: null,
|
|
441
|
-
worktree: null,
|
|
442
|
-
tmuxSession: null,
|
|
443
|
-
claimedAt: null,
|
|
444
|
-
lastHeartbeat: null,
|
|
445
|
-
mode: null,
|
|
446
|
-
transport: null,
|
|
447
|
-
agent: null,
|
|
448
|
-
sessionId: null,
|
|
449
|
-
runId: null,
|
|
450
|
-
sessionState: null,
|
|
451
|
-
remoteStatus: null,
|
|
452
|
-
lastEventAt: null,
|
|
453
|
-
pid: null,
|
|
454
|
-
outputFile: null,
|
|
455
|
-
exitCode: null,
|
|
456
|
-
};
|
|
457
|
-
delete state.activeCards[seq];
|
|
458
|
-
delete state.leases[seq];
|
|
459
|
-
writeState(this.ctx.paths.stateFile, state, 'closeout-release');
|
|
444
|
+
this.runtimeStore.updateState('closeout-release', (draft) => {
|
|
445
|
+
this.runtimeStore.releaseTaskProjection(draft, seq, { dropLease: true });
|
|
446
|
+
});
|
|
460
447
|
this.log.ok(`seq ${seq}: Worker slot ${slotName} released`);
|
|
461
448
|
}
|
|
462
449
|
catch (err) {
|
|
@@ -468,11 +455,11 @@ export class CloseoutEngine {
|
|
|
468
455
|
else {
|
|
469
456
|
// No active slot found — already released (idempotency)
|
|
470
457
|
// Still clean up activeCards entry if present
|
|
471
|
-
if (state.activeCards[seq]) {
|
|
472
|
-
delete state.activeCards[seq];
|
|
473
|
-
delete state.leases[seq];
|
|
458
|
+
if (state.activeCards[seq] || state.leases[seq]) {
|
|
474
459
|
try {
|
|
475
|
-
|
|
460
|
+
this.runtimeStore.updateState('closeout-release', (draft) => {
|
|
461
|
+
this.runtimeStore.releaseTaskProjection(draft, seq, { dropLease: true });
|
|
462
|
+
});
|
|
476
463
|
}
|
|
477
464
|
catch {
|
|
478
465
|
// non-fatal
|
|
@@ -494,15 +481,18 @@ export class CloseoutEngine {
|
|
|
494
481
|
}
|
|
495
482
|
// Step 5: Mark worktree for cleanup (actual removal runs at end of tick)
|
|
496
483
|
try {
|
|
497
|
-
const freshState =
|
|
484
|
+
const freshState = this.runtimeStore.readState();
|
|
498
485
|
const branchName = this.buildBranchName(card);
|
|
499
|
-
const worktreePath =
|
|
486
|
+
const worktreePath = runtime.lease?.worktree ||
|
|
487
|
+
runtime.evidence?.worktree ||
|
|
488
|
+
resolveWorktreePath(this.ctx.projectName, seq, this.ctx.config.WORKTREE_DIR);
|
|
500
489
|
const cleanup = freshState.worktreeCleanup ?? [];
|
|
501
490
|
const alreadyMarked = cleanup.some((e) => e.branch === branchName);
|
|
502
491
|
if (!alreadyMarked) {
|
|
503
492
|
cleanup.push({ branch: branchName, worktreePath, markedAt: new Date().toISOString() });
|
|
504
|
-
|
|
505
|
-
|
|
493
|
+
this.runtimeStore.updateState('closeout-worktree-mark', (draft) => {
|
|
494
|
+
draft.worktreeCleanup = cleanup;
|
|
495
|
+
});
|
|
506
496
|
}
|
|
507
497
|
this.log.ok(`seq ${seq}: Worktree marked for cleanup`);
|
|
508
498
|
}
|
|
@@ -540,11 +530,11 @@ export class CloseoutEngine {
|
|
|
540
530
|
async resolveConflict(card, actions) {
|
|
541
531
|
const seq = card.seq;
|
|
542
532
|
const branchName = this.buildBranchName(card);
|
|
543
|
-
const state =
|
|
533
|
+
const state = this.runtimeStore.readState();
|
|
534
|
+
const runtime = this.runtimeStore.getTask(seq, state);
|
|
544
535
|
const baseBranch = this.ctx.mergeBranch;
|
|
545
536
|
// Find worktree for this card
|
|
546
|
-
const
|
|
547
|
-
const worktree = slotEntry?.[1]?.worktree;
|
|
537
|
+
const worktree = runtime.lease?.worktree || runtime.evidence?.worktree || runtime.slot?.worktree;
|
|
548
538
|
if (!worktree) {
|
|
549
539
|
this.log.warn(`seq ${seq}: No worktree found for conflict resolution`);
|
|
550
540
|
return false;
|
|
@@ -576,19 +566,17 @@ export class CloseoutEngine {
|
|
|
576
566
|
this.log.warn(`seq ${seq}: L1 rebase threw: ${msg}`);
|
|
577
567
|
}
|
|
578
568
|
// ── L2: Send to original worker ──────────────────────────────
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const [slotName, slotState] = slotEntry;
|
|
582
|
-
const session = slotState.tmuxSession;
|
|
583
|
-
if (!session) {
|
|
584
|
-
this.log.warn(`seq ${seq}: No worker session for L2 conflict resolution`);
|
|
569
|
+
const slotName = runtime.slotName || this.runtimeStore.findAvailableSlot(state);
|
|
570
|
+
if (!slotName)
|
|
585
571
|
return false;
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
slotState.
|
|
591
|
-
|
|
572
|
+
const slotState = state.workers[slotName];
|
|
573
|
+
const session = slotState?.tmuxSession || null;
|
|
574
|
+
const isPrintMode = slotState?.mode === 'print';
|
|
575
|
+
const isAcpMode = !!slotState &&
|
|
576
|
+
(slotState.transport === 'acp' ||
|
|
577
|
+
slotState.transport === 'pty' ||
|
|
578
|
+
slotState.mode === 'acp' ||
|
|
579
|
+
slotState.mode === 'pty');
|
|
592
580
|
try {
|
|
593
581
|
if (isAcpMode && this.agentRuntime) {
|
|
594
582
|
await this.resumeAcpWorker(slotName, seq, worktree, branchName, [
|
|
@@ -598,24 +586,32 @@ export class CloseoutEngine {
|
|
|
598
586
|
].join('\n'), 'resolving', 'closeout-conflict-resume');
|
|
599
587
|
}
|
|
600
588
|
else if (isPrintMode) {
|
|
589
|
+
if (!session) {
|
|
590
|
+
this.log.warn(`seq ${seq}: No worker session for L2 conflict resolution`);
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
601
593
|
// Print mode: spawn new process with --resume
|
|
602
594
|
this.log.info(`seq ${seq}: L2 — spawning conflict resolution via --resume`);
|
|
603
|
-
const resumeResult = await this.workerProvider.resolveConflict(session, worktree, branchName, slotState
|
|
595
|
+
const resumeResult = await this.workerProvider.resolveConflict(session, worktree, branchName, slotState?.sessionId || undefined);
|
|
604
596
|
// Update state with new process info
|
|
605
597
|
if (resumeResult && typeof resumeResult === 'object' && 'pid' in resumeResult) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
598
|
+
this.runtimeStore.updateState('closeout-conflict-resume', (freshState) => {
|
|
599
|
+
if (freshState.workers[slotName]) {
|
|
600
|
+
freshState.workers[slotName].pid = resumeResult.pid;
|
|
601
|
+
freshState.workers[slotName].outputFile = resumeResult.outputFile;
|
|
602
|
+
if (resumeResult.sessionId) {
|
|
603
|
+
freshState.workers[slotName].sessionId = resumeResult.sessionId;
|
|
604
|
+
}
|
|
605
|
+
freshState.workers[slotName].exitCode = null;
|
|
612
606
|
}
|
|
613
|
-
|
|
614
|
-
writeState(this.ctx.paths.stateFile, freshState, 'closeout-conflict-resume');
|
|
615
|
-
}
|
|
607
|
+
});
|
|
616
608
|
}
|
|
617
609
|
}
|
|
618
610
|
else {
|
|
611
|
+
if (!session) {
|
|
612
|
+
this.log.warn(`seq ${seq}: No live worker session for interactive L2 conflict resolution`);
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
619
615
|
// Interactive mode: send to live tmux session
|
|
620
616
|
const inspection = await this.workerProvider.inspect(session);
|
|
621
617
|
if (!inspection.alive) {
|
|
@@ -649,7 +645,7 @@ export class CloseoutEngine {
|
|
|
649
645
|
* Each entry is processed independently — one failure does not block others.
|
|
650
646
|
*/
|
|
651
647
|
async cleanupWorktrees(actions) {
|
|
652
|
-
const state =
|
|
648
|
+
const state = this.runtimeStore.readState();
|
|
653
649
|
const queue = state.worktreeCleanup ?? [];
|
|
654
650
|
if (queue.length === 0)
|
|
655
651
|
return;
|
|
@@ -680,9 +676,9 @@ export class CloseoutEngine {
|
|
|
680
676
|
}
|
|
681
677
|
}
|
|
682
678
|
// Update state with remaining entries
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
679
|
+
this.runtimeStore.updateState('closeout-worktree-cleanup', (freshState) => {
|
|
680
|
+
freshState.worktreeCleanup = remaining;
|
|
681
|
+
});
|
|
686
682
|
}
|
|
687
683
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
688
684
|
buildBranchName(card) {
|
|
@@ -740,27 +736,37 @@ export class CloseoutEngine {
|
|
|
740
736
|
await this.agentRuntime.ensureSession(slotName, undefined, worktree);
|
|
741
737
|
session = await this.agentRuntime.startRun(slotName, prompt, undefined, worktree);
|
|
742
738
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
739
|
+
this.runtimeStore.updateState(updatedBy, (state) => {
|
|
740
|
+
const slot = state.workers[slotName];
|
|
741
|
+
if (slot) {
|
|
742
|
+
slot.status = slotStatus;
|
|
743
|
+
slot.mode = this.ctx.config.WORKER_TRANSPORT === 'pty' ? 'pty' : 'acp';
|
|
744
|
+
slot.transport = this.ctx.config.WORKER_TRANSPORT === 'pty' ? 'pty' : 'acp';
|
|
745
|
+
slot.agent = session.tool;
|
|
746
|
+
slot.tmuxSession = session.sessionName;
|
|
747
|
+
slot.sessionId = session.sessionId;
|
|
748
|
+
slot.runId = session.currentRun?.runId || null;
|
|
749
|
+
slot.sessionState = session.sessionState;
|
|
750
|
+
slot.remoteStatus = session.currentRun?.status || null;
|
|
751
|
+
slot.lastEventAt = session.lastSeenAt;
|
|
752
|
+
slot.lastHeartbeat = new Date().toISOString();
|
|
753
|
+
slot.branch = slot.branch || branchName;
|
|
754
|
+
slot.worktree = slot.worktree || worktree;
|
|
755
|
+
slot.seq = parseInt(seq, 10);
|
|
756
|
+
slot.pid = null;
|
|
757
|
+
slot.outputFile = null;
|
|
758
|
+
slot.exitCode = null;
|
|
759
|
+
}
|
|
760
|
+
if (state.leases[seq]) {
|
|
761
|
+
state.leases[seq].slot = slotName;
|
|
762
|
+
state.leases[seq].branch = branchName;
|
|
763
|
+
state.leases[seq].worktree = worktree;
|
|
764
|
+
state.leases[seq].sessionId = session.sessionId;
|
|
765
|
+
state.leases[seq].runId = session.currentRun?.runId || null;
|
|
766
|
+
state.leases[seq].phase = slotStatus === 'resolving' ? 'resolving_conflict' : 'coding';
|
|
767
|
+
state.leases[seq].lastTransitionAt = new Date().toISOString();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
764
770
|
}
|
|
765
771
|
logEvent(action, seq, result, meta) {
|
|
766
772
|
this.log.event({
|