@wrongstack/core 0.313.0 → 0.313.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/project-server.js +84 -4868
- package/dist/chunk-2BUUIMQB.js +225 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2FQL4PDC.js +292 -0
- package/dist/chunk-2KKODIQU.js +2256 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3E244UDA.js +356 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-4LJJYBAU.js +388 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-4XV6IR37.js +187 -0
- package/dist/chunk-4ZHTKJHM.js +183 -0
- package/dist/chunk-52RTVH4M.js +361 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5DSMEVA4.js +931 -0
- package/dist/chunk-5RP3QTRL.js +1223 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-62V2AFVG.js +422 -0
- package/dist/chunk-65RDIOR2.js +2470 -0
- package/dist/chunk-6AKOTGLM.js +597 -0
- package/dist/chunk-7ZHZE5EO.js +28 -0
- package/dist/chunk-A455ATZB.js +140 -0
- package/dist/chunk-AD7ZFZCS.js +46 -0
- package/dist/chunk-AI4LLQ2U.js +856 -0
- package/dist/chunk-ASX6AW36.js +789 -0
- package/dist/chunk-AT4V4XGI.js +3522 -0
- package/dist/chunk-AWNQ6ED6.js +311 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-BAKVJLEV.js +417 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-CB2HFKTP.js +9 -0
- package/dist/chunk-CE4ZOCOI.js +1640 -0
- package/dist/chunk-CQQL4ZOQ.js +151 -0
- package/dist/chunk-DEIQMBD2.js +65 -0
- package/dist/chunk-E6I5RXVK.js +58 -0
- package/dist/chunk-EFEHDQGY.js +1589 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-EPC3GZD4.js +308 -0
- package/dist/chunk-FAYRX2AD.js +13 -0
- package/dist/chunk-FFSX2QDQ.js +362 -0
- package/dist/chunk-FHAMQS7Q.js +5856 -0
- package/dist/chunk-FIV4IJL7.js +2263 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FSWCWSTS.js +395 -0
- package/dist/chunk-FW5LDDMJ.js +16464 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5AAKEEZ.js +228 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GAZEWQEZ.js +231 -0
- package/dist/chunk-GFJOMSHG.js +125 -0
- package/dist/chunk-GFXZTHNI.js +158 -0
- package/dist/chunk-GM54GJBU.js +960 -0
- package/dist/chunk-GPQ3NJPR.js +900 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-H5E77KUJ.js +15 -0
- package/dist/chunk-H72H4XDU.js +1023 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-I45KAW2N.js +5020 -0
- package/dist/chunk-IQ35HMQL.js +1396 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-JJPMWPB6.js +109 -0
- package/dist/chunk-JTHKOYWU.js +663 -0
- package/dist/chunk-JVGPR3TS.js +365 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K3G6JBUC.js +5232 -0
- package/dist/chunk-K4YUNKU7.js +589 -0
- package/dist/chunk-KBZGE6LN.js +710 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KI4L2DUL.js +560 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KSJ5UTP7.js +154 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LZDDLNCZ.js +279 -0
- package/dist/chunk-M2WF6AXG.js +115 -0
- package/dist/chunk-M7BS2K6U.js +1 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NFAYHPRH.js +323 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-O74MJHXU.js +187 -0
- package/dist/chunk-OJZEX73F.js +112 -0
- package/dist/chunk-PAAWLLZX.js +74 -0
- package/dist/chunk-PFXFQQRT.js +339 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-QRTG4XTI.js +188 -0
- package/dist/chunk-QVOVMXJR.js +1351 -0
- package/dist/chunk-RCQYT2I4.js +757 -0
- package/dist/chunk-RD6DEE36.js +1178 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RPOA7IAE.js +150 -0
- package/dist/chunk-SN2DQRL6.js +1172 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SQP64RCS.js +5391 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-T3KGZBU5.js +3171 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TYW2GI5I.js +1077 -0
- package/dist/chunk-UIWONWVG.js +4959 -0
- package/dist/chunk-VB3H5WWB.js +151 -0
- package/dist/chunk-W4A6QA3D.js +258 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WEBMSQP2.js +123 -0
- package/dist/chunk-WOEZV3Q4.js +333 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-WZO23AWO.js +2779 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-XQ2XGIBF.js +2086 -0
- package/dist/chunk-Y2GE5TSI.js +355 -0
- package/dist/chunk-YBUJCZ57.js +351 -0
- package/dist/chunk-YNCVSFGF.js +149 -0
- package/dist/chunk-YUPMXOGP.js +282 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZEHUCJHY.js +129 -0
- package/dist/chunk-ZYU2YOJ6.js +1496 -0
- package/dist/chunk-ZZ4V3WIZ.js +163 -0
- package/dist/coordination/agents/index.js +145 -6260
- package/dist/coordination/index.js +365 -34324
- package/dist/coordination/mailbox-project-server.js +48 -3276
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12480
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/design/index.js +50 -1087
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1599 -21888
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +112 -5139
- package/dist/hq/protocol.js +21 -668
- package/dist/index.js +1410 -92656
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- package/dist/kernel/index.js +14 -992
- package/dist/models/index.js +40 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/index.js +17 -279
- package/dist/plugin/index.js +584 -14835
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry-R23UBE7H.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/review-finding-store-DKAV3AS6.js +21 -0
- package/dist/review-report-store-OWY3EC3N.js +19 -0
- package/dist/security/index.js +114 -2202
- package/dist/session-catalog/index.js +36 -2103
- package/dist/session-catalog/project-server.js +42 -1435
- package/dist/skills/index.js +35 -1578
- package/dist/storage/index.js +182 -15600
- package/dist/tasking/index.js +10 -676
- package/dist/tools/index.js +52 -9590
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1342
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/session-markers.js +8 -144
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +269 -5685
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/wiring/proxy-rewrite.d.ts +18 -0
- package/dist/wiring/proxy-rewrite.js +18 -121
- package/dist/worktree/index.js +6 -887
- package/package.json +4 -4
|
@@ -0,0 +1,2086 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultTaskStore,
|
|
3
|
+
TaskTracker
|
|
4
|
+
} from "./chunk-K4YUNKU7.js";
|
|
5
|
+
import {
|
|
6
|
+
noOpLogger
|
|
7
|
+
} from "./chunk-WBPVKEEE.js";
|
|
8
|
+
import {
|
|
9
|
+
readBundledInstructionText,
|
|
10
|
+
renderInstructionTemplate
|
|
11
|
+
} from "./chunk-5S437PRC.js";
|
|
12
|
+
import {
|
|
13
|
+
atomicWrite,
|
|
14
|
+
withFileLock
|
|
15
|
+
} from "./chunk-7ZHZE5EO.js";
|
|
16
|
+
import {
|
|
17
|
+
toErrorMessage
|
|
18
|
+
} from "./chunk-TBRL2RCC.js";
|
|
19
|
+
|
|
20
|
+
// src/goal/phase-graph-builder.ts
|
|
21
|
+
var PhaseGraphBuilder = class _PhaseGraphBuilder {
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
this.opts = opts;
|
|
24
|
+
}
|
|
25
|
+
opts;
|
|
26
|
+
async build() {
|
|
27
|
+
const graphId = crypto.randomUUID();
|
|
28
|
+
const phases = /* @__PURE__ */ new Map();
|
|
29
|
+
const phaseIds = [];
|
|
30
|
+
for (let i = 0; i < this.opts.phases.length; i++) {
|
|
31
|
+
const tmpl = this.opts.phases[i] ?? { name: "", description: "", tasks: [], taskTemplates: [], parallelizable: false, priority: "medium", estimateHours: 0 };
|
|
32
|
+
const phaseId = crypto.randomUUID();
|
|
33
|
+
phaseIds.push(phaseId);
|
|
34
|
+
const store = this.opts.externalTaskStore ?? new DefaultTaskStore();
|
|
35
|
+
const tracker = new TaskTracker({ store });
|
|
36
|
+
const taskGraph = await tracker.createGraph(phaseId, `${tmpl.name} Tasks`);
|
|
37
|
+
if (tmpl.taskTemplates && tmpl.taskTemplates.length > 0) {
|
|
38
|
+
for (const tt of tmpl.taskTemplates) {
|
|
39
|
+
tracker.addNode({
|
|
40
|
+
title: tt.title,
|
|
41
|
+
description: tt.description,
|
|
42
|
+
type: tt.type,
|
|
43
|
+
priority: tt.priority,
|
|
44
|
+
status: "pending",
|
|
45
|
+
estimateHours: tt.estimateHours,
|
|
46
|
+
tags: tt.tags ?? []
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const phase = {
|
|
51
|
+
id: phaseId,
|
|
52
|
+
name: tmpl.name,
|
|
53
|
+
description: tmpl.description,
|
|
54
|
+
status: "pending",
|
|
55
|
+
taskGraph,
|
|
56
|
+
dependsOn: i > 0 ? [phaseIds[i - 1] ?? ""] : [],
|
|
57
|
+
nextPhases: i < this.opts.phases.length - 1 ? [phaseIds[i + 1] ?? ""] : [],
|
|
58
|
+
parallelizable: tmpl.parallelizable,
|
|
59
|
+
priority: tmpl.priority,
|
|
60
|
+
estimateHours: tmpl.estimateHours,
|
|
61
|
+
assignedAgents: [],
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
updatedAt: Date.now()
|
|
64
|
+
};
|
|
65
|
+
phases.set(phaseId, phase);
|
|
66
|
+
}
|
|
67
|
+
const phaseArray = Array.from(phases.values());
|
|
68
|
+
for (let i = 0; i < phaseArray.length; i++) {
|
|
69
|
+
const phase = phaseArray[i];
|
|
70
|
+
if (!phase) continue;
|
|
71
|
+
phase.nextPhases = i < phaseArray.length - 1 ? [phaseArray[i + 1]?.id ?? ""] : [];
|
|
72
|
+
}
|
|
73
|
+
const graph = {
|
|
74
|
+
id: graphId,
|
|
75
|
+
title: this.opts.title,
|
|
76
|
+
description: this.opts.description ?? "",
|
|
77
|
+
phases,
|
|
78
|
+
rootPhaseIds: phaseIds.length > 0 ? [phaseIds[0] ?? ""] : [],
|
|
79
|
+
activePhaseIds: [],
|
|
80
|
+
completedPhaseIds: [],
|
|
81
|
+
failedPhaseIds: [],
|
|
82
|
+
autonomous: this.opts.autonomous ?? true,
|
|
83
|
+
stopOnComplete: true,
|
|
84
|
+
multiBoard: this.opts.multiBoard,
|
|
85
|
+
verifyTasks: this.opts.verifyTasks,
|
|
86
|
+
chimeraReview: this.opts.chimeraReview,
|
|
87
|
+
createdAt: Date.now(),
|
|
88
|
+
updatedAt: Date.now()
|
|
89
|
+
};
|
|
90
|
+
return graph;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create phases from an existing TaskGraph.
|
|
94
|
+
* Groups tasks into phases using topological sort so dependencies
|
|
95
|
+
* stay in the same phase wherever possible.
|
|
96
|
+
*/
|
|
97
|
+
static fromTaskGraph(taskGraph, options) {
|
|
98
|
+
const tasksPerPhase = options.tasksPerPhase ?? 5;
|
|
99
|
+
const nodes = Array.from(taskGraph.nodes.values());
|
|
100
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const edge of taskGraph.edges) {
|
|
102
|
+
const list = dependents.get(edge.from) ?? [];
|
|
103
|
+
list.push(edge.to);
|
|
104
|
+
dependents.set(edge.from, list);
|
|
105
|
+
}
|
|
106
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const node of nodes) inDegree.set(node.id, 0);
|
|
108
|
+
for (const edge of taskGraph.edges) {
|
|
109
|
+
inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
const queue = [];
|
|
112
|
+
for (const [id, degree] of inDegree) {
|
|
113
|
+
if (degree === 0) queue.push(id);
|
|
114
|
+
}
|
|
115
|
+
const prioOrder = {
|
|
116
|
+
critical: 0,
|
|
117
|
+
high: 1,
|
|
118
|
+
medium: 2,
|
|
119
|
+
low: 3
|
|
120
|
+
};
|
|
121
|
+
const topoSorted = [];
|
|
122
|
+
while (queue.length > 0) {
|
|
123
|
+
queue.sort((a, b) => {
|
|
124
|
+
const na = taskGraph.nodes.get(a);
|
|
125
|
+
const nb = taskGraph.nodes.get(b);
|
|
126
|
+
const naPrio = na ? prioOrder[na.priority] : 4;
|
|
127
|
+
const nbPrio = nb ? prioOrder[nb.priority] : 4;
|
|
128
|
+
return naPrio - nbPrio;
|
|
129
|
+
});
|
|
130
|
+
const id = queue.shift();
|
|
131
|
+
topoSorted.push(id);
|
|
132
|
+
for (const depId of dependents.get(id) ?? []) {
|
|
133
|
+
const deg = (inDegree.get(depId) ?? 1) - 1;
|
|
134
|
+
inDegree.set(depId, deg);
|
|
135
|
+
if (deg === 0) queue.push(depId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const groups = [];
|
|
139
|
+
let currentGroup = [];
|
|
140
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
141
|
+
for (const id of topoSorted) {
|
|
142
|
+
if (assigned.has(id)) continue;
|
|
143
|
+
assigned.add(id);
|
|
144
|
+
currentGroup.push(id);
|
|
145
|
+
const deps = dependents.get(id) ?? [];
|
|
146
|
+
for (const depId of deps) {
|
|
147
|
+
if (!assigned.has(depId) && currentGroup.length < tasksPerPhase) {
|
|
148
|
+
assigned.add(depId);
|
|
149
|
+
currentGroup.push(depId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (currentGroup.length >= tasksPerPhase) {
|
|
153
|
+
groups.push(currentGroup);
|
|
154
|
+
currentGroup = [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (currentGroup.length > 0) groups.push(currentGroup);
|
|
158
|
+
const phaseTemplates = groups.map((groupIds, idx) => {
|
|
159
|
+
const group = groupIds.map((id) => taskGraph.nodes.get(id)).filter((n) => n != null);
|
|
160
|
+
const hasCritical = group.some((t) => t.priority === "critical");
|
|
161
|
+
const totalHours = group.reduce((sum, t) => sum + (t.estimateHours ?? 2), 0);
|
|
162
|
+
return {
|
|
163
|
+
name: `Phase ${idx + 1}: ${group[0]?.title.slice(0, 30) ?? "Tasks"}`,
|
|
164
|
+
description: group.map((t) => t.title).join(", "),
|
|
165
|
+
priority: hasCritical ? "critical" : "high",
|
|
166
|
+
estimateHours: totalHours,
|
|
167
|
+
parallelizable: false,
|
|
168
|
+
taskTemplates: group.map((t) => ({
|
|
169
|
+
title: t.title,
|
|
170
|
+
description: t.description,
|
|
171
|
+
type: t.type,
|
|
172
|
+
priority: t.priority,
|
|
173
|
+
estimateHours: t.estimateHours ?? 2,
|
|
174
|
+
tags: t.tags ?? []
|
|
175
|
+
}))
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
const builder = new _PhaseGraphBuilder({
|
|
179
|
+
...options,
|
|
180
|
+
phases: phaseTemplates
|
|
181
|
+
});
|
|
182
|
+
return builder.build();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/goal/phase-board-mutations.ts
|
|
187
|
+
function findPhaseOfTaskInGraph(graph, taskId) {
|
|
188
|
+
for (const phase of graph.phases.values()) {
|
|
189
|
+
if (phase.taskGraph.nodes.has(taskId)) return phase;
|
|
190
|
+
}
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
function movePhaseTask(ctx, taskId, toPhaseId) {
|
|
194
|
+
const from = findPhaseOfTaskInGraph(ctx.graph, taskId);
|
|
195
|
+
const to = ctx.graph.phases.get(toPhaseId);
|
|
196
|
+
if (!from || !to || from.id === toPhaseId) return false;
|
|
197
|
+
const node = from.taskGraph.nodes.get(taskId);
|
|
198
|
+
if (!node) return false;
|
|
199
|
+
const dependents = from.taskGraph.edges.filter((e) => e.from === taskId).map((e) => e.to);
|
|
200
|
+
for (const depId of dependents) {
|
|
201
|
+
const depNode = from.taskGraph.nodes.get(depId);
|
|
202
|
+
if (depNode) {
|
|
203
|
+
depNode.parentId = void 0;
|
|
204
|
+
depNode.children = void 0;
|
|
205
|
+
}
|
|
206
|
+
from.taskGraph.edges = from.taskGraph.edges.filter(
|
|
207
|
+
(e) => !(e.from === taskId && e.to === depId)
|
|
208
|
+
);
|
|
209
|
+
if (!from.taskGraph.rootNodes.includes(depId)) {
|
|
210
|
+
from.taskGraph.rootNodes.push(depId);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
from.taskGraph.nodes.delete(taskId);
|
|
214
|
+
from.taskGraph.rootNodes = from.taskGraph.rootNodes.filter((id) => id !== taskId);
|
|
215
|
+
from.taskGraph.edges = from.taskGraph.edges.filter((e) => e.from !== taskId && e.to !== taskId);
|
|
216
|
+
from.taskGraph.updatedAt = Date.now();
|
|
217
|
+
node.parentId = void 0;
|
|
218
|
+
node.children = void 0;
|
|
219
|
+
node.updatedAt = Date.now();
|
|
220
|
+
to.taskGraph.nodes.set(taskId, node);
|
|
221
|
+
to.taskGraph.rootNodes.push(taskId);
|
|
222
|
+
to.taskGraph.updatedAt = Date.now();
|
|
223
|
+
ctx.trackerCache.delete(from.id);
|
|
224
|
+
ctx.trackerCache.delete(to.id);
|
|
225
|
+
ctx.graph.updatedAt = Date.now();
|
|
226
|
+
ctx.emit("phase.taskMoved", { taskId, fromPhaseId: from.id, toPhaseId });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
function setPhaseTaskAssignee(ctx, taskId, agentId, agentName) {
|
|
230
|
+
const phase = findPhaseOfTaskInGraph(ctx.graph, taskId);
|
|
231
|
+
if (!phase) return false;
|
|
232
|
+
const tracker = ctx.getTrackerForPhase(phase);
|
|
233
|
+
tracker.updateNode(taskId, { assignee: agentName ?? agentId ?? "" });
|
|
234
|
+
ctx.graph.updatedAt = Date.now();
|
|
235
|
+
ctx.emit("phase.taskAssigned", {
|
|
236
|
+
phaseId: phase.id,
|
|
237
|
+
taskId,
|
|
238
|
+
...agentId !== void 0 ? { agentId } : {},
|
|
239
|
+
...agentName !== void 0 ? { agentName } : {}
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
function addPhaseTask(ctx, phaseId, spec) {
|
|
244
|
+
const phase = ctx.graph.phases.get(phaseId);
|
|
245
|
+
if (!phase) return null;
|
|
246
|
+
const tracker = ctx.getTrackerForPhase(phase);
|
|
247
|
+
const node = tracker.addNode({
|
|
248
|
+
title: spec.title,
|
|
249
|
+
description: spec.description ?? "",
|
|
250
|
+
type: spec.type ?? "feature",
|
|
251
|
+
priority: spec.priority ?? "medium",
|
|
252
|
+
status: "pending"
|
|
253
|
+
});
|
|
254
|
+
ctx.graph.updatedAt = Date.now();
|
|
255
|
+
ctx.emit("phase.taskAdded", { phaseId, taskId: node.id, taskTitle: node.title });
|
|
256
|
+
return node.id;
|
|
257
|
+
}
|
|
258
|
+
function requeuePhaseTask(ctx, taskId) {
|
|
259
|
+
const phase = findPhaseOfTaskInGraph(ctx.graph, taskId);
|
|
260
|
+
if (!phase) return false;
|
|
261
|
+
const tracker = ctx.getTrackerForPhase(phase);
|
|
262
|
+
tracker.updateNodeStatus(taskId, "pending");
|
|
263
|
+
ctx.taskRetryCounts.delete(`${phase.id}:${taskId}`);
|
|
264
|
+
if (phase.status === "completed" || phase.status === "failed" || phase.status === "paused") {
|
|
265
|
+
ctx.graph.failedPhaseIds = ctx.graph.failedPhaseIds.filter((id) => id !== phase.id);
|
|
266
|
+
ctx.graph.completedPhaseIds = ctx.graph.completedPhaseIds.filter((id) => id !== phase.id);
|
|
267
|
+
ctx.graph.activePhaseIds = ctx.graph.activePhaseIds.filter((id) => id !== phase.id);
|
|
268
|
+
ctx.phaseWorktrees.delete(phase.id);
|
|
269
|
+
ctx.updatePhaseStatus(phase, "pending");
|
|
270
|
+
}
|
|
271
|
+
ctx.graph.updatedAt = Date.now();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/goal/phase-orchestrator-queries.ts
|
|
276
|
+
function getReadyPhases(graph) {
|
|
277
|
+
const ready = [];
|
|
278
|
+
for (const phase of graph.phases.values()) {
|
|
279
|
+
if (phase.status !== "pending") continue;
|
|
280
|
+
const depsDone = phase.dependsOn.every((depId) => {
|
|
281
|
+
const dep = graph.phases.get(depId);
|
|
282
|
+
return dep?.status === "completed" || dep?.status === "skipped";
|
|
283
|
+
});
|
|
284
|
+
if (depsDone) {
|
|
285
|
+
ready.push(phase);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const prioOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
289
|
+
ready.sort((a, b) => (prioOrder[a.priority] ?? 4) - (prioOrder[b.priority] ?? 4));
|
|
290
|
+
return ready;
|
|
291
|
+
}
|
|
292
|
+
function getActivePhases(graph) {
|
|
293
|
+
return Array.from(graph.phases.values()).filter((p) => p.status === "running");
|
|
294
|
+
}
|
|
295
|
+
function getExecutableTasks(trackerFor, phase) {
|
|
296
|
+
const tracker = trackerFor(phase);
|
|
297
|
+
return tracker.getAllNodes({ status: ["pending", "blocked"] }).filter(
|
|
298
|
+
(n) => n.status === "pending" && tracker.canStart(n.id)
|
|
299
|
+
).sort((a, b) => {
|
|
300
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
301
|
+
return (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function getFailedTaskCount(trackerFor, phase) {
|
|
305
|
+
return trackerFor(phase).getAllNodes({ status: ["failed"] }).length;
|
|
306
|
+
}
|
|
307
|
+
function getCompletedTaskCount(trackerFor, phase) {
|
|
308
|
+
return trackerFor(phase).getAllNodes({ status: ["completed"] }).length;
|
|
309
|
+
}
|
|
310
|
+
function isGraphComplete(graph) {
|
|
311
|
+
const allPhases = Array.from(graph.phases.values());
|
|
312
|
+
return allPhases.every((p) => p.status === "completed" || p.status === "skipped");
|
|
313
|
+
}
|
|
314
|
+
function getProgress(graph, trackerFor) {
|
|
315
|
+
const phases = Array.from(graph.phases.values());
|
|
316
|
+
let pending = 0;
|
|
317
|
+
let ready = 0;
|
|
318
|
+
let running = 0;
|
|
319
|
+
let paused = 0;
|
|
320
|
+
let completed = 0;
|
|
321
|
+
let failed = 0;
|
|
322
|
+
let skipped = 0;
|
|
323
|
+
let totalTasks = 0;
|
|
324
|
+
let completedTasks = 0;
|
|
325
|
+
let failedTasks = 0;
|
|
326
|
+
let estimatedHours = 0;
|
|
327
|
+
let actualHours = 0;
|
|
328
|
+
for (const p of phases) {
|
|
329
|
+
switch (p.status) {
|
|
330
|
+
case "pending":
|
|
331
|
+
pending++;
|
|
332
|
+
break;
|
|
333
|
+
case "ready":
|
|
334
|
+
ready++;
|
|
335
|
+
break;
|
|
336
|
+
case "running":
|
|
337
|
+
running++;
|
|
338
|
+
break;
|
|
339
|
+
case "paused":
|
|
340
|
+
paused++;
|
|
341
|
+
break;
|
|
342
|
+
case "completed":
|
|
343
|
+
completed++;
|
|
344
|
+
break;
|
|
345
|
+
case "failed":
|
|
346
|
+
failed++;
|
|
347
|
+
break;
|
|
348
|
+
case "skipped":
|
|
349
|
+
skipped++;
|
|
350
|
+
break;
|
|
351
|
+
default:
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
estimatedHours += p.estimateHours;
|
|
355
|
+
if (p.actualDurationMs) actualHours += p.actualDurationMs / 36e5;
|
|
356
|
+
const tracker = trackerFor(p);
|
|
357
|
+
const progress = tracker.getProgress();
|
|
358
|
+
totalTasks += progress.total;
|
|
359
|
+
completedTasks += progress.completed;
|
|
360
|
+
failedTasks += progress.failed;
|
|
361
|
+
}
|
|
362
|
+
const totalPhases = phases.length;
|
|
363
|
+
const done = completed + skipped;
|
|
364
|
+
return {
|
|
365
|
+
totalPhases,
|
|
366
|
+
pending,
|
|
367
|
+
ready,
|
|
368
|
+
running,
|
|
369
|
+
paused,
|
|
370
|
+
completed,
|
|
371
|
+
failed,
|
|
372
|
+
skipped,
|
|
373
|
+
percentComplete: totalPhases > 0 ? Math.min(100, Math.round(done / totalPhases * 100)) : 0,
|
|
374
|
+
totalTasks,
|
|
375
|
+
completedTasks,
|
|
376
|
+
failedTasks,
|
|
377
|
+
estimatedHours,
|
|
378
|
+
actualHours
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function setIntegrationMetadata(graph, phase, status, details = {}) {
|
|
382
|
+
phase.metadata = {
|
|
383
|
+
...phase.metadata,
|
|
384
|
+
integrationStatus: status,
|
|
385
|
+
integrationBranch: details.branch,
|
|
386
|
+
integrationWorktreeDir: details.worktreeDir,
|
|
387
|
+
integrationConflictFiles: details.conflictFiles,
|
|
388
|
+
integrationError: details.error,
|
|
389
|
+
integrationUpdatedAt: Date.now()
|
|
390
|
+
};
|
|
391
|
+
phase.updatedAt = Date.now();
|
|
392
|
+
graph.updatedAt = Date.now();
|
|
393
|
+
}
|
|
394
|
+
function truncate(text, max = 500) {
|
|
395
|
+
const t = text.trim();
|
|
396
|
+
return t.length <= max ? t : `${t.slice(0, max)}\u2026 (+${t.length - max} chars)`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/goal/phase-orchestrator-integration.ts
|
|
400
|
+
function worktreeEnv(int, phase) {
|
|
401
|
+
const handle = int.phaseWorktrees.get(phase.id);
|
|
402
|
+
return handle ? { cwd: handle.dir, branch: handle.branch } : void 0;
|
|
403
|
+
}
|
|
404
|
+
async function failPhaseAfterTasks(int, phase, error) {
|
|
405
|
+
int.updatePhaseStatus(phase, "failed");
|
|
406
|
+
phase.completedAt = Date.now();
|
|
407
|
+
phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
|
|
408
|
+
int.runningPhases.delete(phase.id);
|
|
409
|
+
int.graph.activePhaseIds = int.graph.activePhaseIds.filter((id) => id !== phase.id);
|
|
410
|
+
if (!int.graph.failedPhaseIds.includes(phase.id)) int.graph.failedPhaseIds.push(phase.id);
|
|
411
|
+
int.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
|
|
412
|
+
int.ctx.onPhaseFail?.(phase, new Error(error));
|
|
413
|
+
await keepWorktreeForReview(int, phase);
|
|
414
|
+
}
|
|
415
|
+
function markPhaseMergeFailed(int, phase, error) {
|
|
416
|
+
if (phase.status !== "failed") {
|
|
417
|
+
int.graph.completedPhaseIds = int.graph.completedPhaseIds.filter((id) => id !== phase.id);
|
|
418
|
+
int.updatePhaseStatus(phase, "failed");
|
|
419
|
+
}
|
|
420
|
+
if (!int.graph.failedPhaseIds.includes(phase.id)) int.graph.failedPhaseIds.push(phase.id);
|
|
421
|
+
int.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
|
|
422
|
+
int.ctx.onPhaseFail?.(phase, new Error(error));
|
|
423
|
+
}
|
|
424
|
+
async function commitAndEnqueueMerge(int, phase) {
|
|
425
|
+
const handle = int.phaseWorktrees.get(phase.id);
|
|
426
|
+
if (!int.worktrees || !handle) return;
|
|
427
|
+
try {
|
|
428
|
+
await int.worktrees.commitAll(handle, `goal(${phase.name}): ${phase.id}`);
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
const depPromises = phase.dependsOn.map((d) => int.phaseMergePromise.get(d)).filter((p) => Boolean(p));
|
|
432
|
+
const merged = (async () => {
|
|
433
|
+
await Promise.allSettled(depPromises);
|
|
434
|
+
int.setMergeQueue(
|
|
435
|
+
int.getMergeQueue().then(() => mergeOne(int, phase, handle)).catch((err) => {
|
|
436
|
+
const msg = toErrorMessage(err);
|
|
437
|
+
int.logger.error(msg, { event: "orchestrator.merge_failed", phaseId: phase.id });
|
|
438
|
+
markPhaseMergeFailed(int, phase, msg);
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
await int.getMergeQueue();
|
|
442
|
+
})();
|
|
443
|
+
int.phaseMergePromise.set(phase.id, merged);
|
|
444
|
+
}
|
|
445
|
+
async function mergeOne(int, phase, handle) {
|
|
446
|
+
if (!int.worktrees) return;
|
|
447
|
+
try {
|
|
448
|
+
const resolve2 = int.ctx.resolveConflict ? async (info) => {
|
|
449
|
+
const shouldResolve = await shouldAttemptConflictResolution(int, phase, info);
|
|
450
|
+
if (!shouldResolve) return false;
|
|
451
|
+
int.emit("phase.conflictResolving", {
|
|
452
|
+
phaseId: phase.id,
|
|
453
|
+
name: phase.name,
|
|
454
|
+
files: info.conflictFiles
|
|
455
|
+
});
|
|
456
|
+
const resolved = await int.ctx.resolveConflict?.(phase, info);
|
|
457
|
+
return resolved ?? false;
|
|
458
|
+
} : void 0;
|
|
459
|
+
const mergeOpts = {
|
|
460
|
+
squash: true
|
|
461
|
+
};
|
|
462
|
+
if (resolve2 !== void 0) mergeOpts.resolve = resolve2;
|
|
463
|
+
const result = await int.worktrees.merge(handle, mergeOpts);
|
|
464
|
+
if (result.resolved) {
|
|
465
|
+
int.emit("phase.conflictResolved", { phaseId: phase.id, name: phase.name });
|
|
466
|
+
}
|
|
467
|
+
setIntegrationMetadata(int.graph, phase, result.ok ? "merged" : "needs_review", {
|
|
468
|
+
branch: handle.branch,
|
|
469
|
+
worktreeDir: handle.dir,
|
|
470
|
+
conflictFiles: result.conflictFiles
|
|
471
|
+
});
|
|
472
|
+
await int.worktrees.release(handle, { keep: !result.ok });
|
|
473
|
+
} catch (err) {
|
|
474
|
+
setIntegrationMetadata(int.graph, phase, "merge_failed", {
|
|
475
|
+
branch: handle.branch,
|
|
476
|
+
worktreeDir: handle.dir,
|
|
477
|
+
error: toErrorMessage(err)
|
|
478
|
+
});
|
|
479
|
+
markPhaseMergeFailed(int, phase, `worktree merge failed: ${toErrorMessage(err)}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function shouldAttemptConflictResolution(int, phase, info) {
|
|
483
|
+
if (!int.ctx.brain) return true;
|
|
484
|
+
const decision = await int.ctx.brain.decide({
|
|
485
|
+
id: `goal-conflict-${phase.id}`,
|
|
486
|
+
source: "goal",
|
|
487
|
+
question: `Should Goal try to resolve merge conflicts for phase "${phase.name}" automatically?`,
|
|
488
|
+
context: [
|
|
489
|
+
`Phase id: ${phase.id}`,
|
|
490
|
+
`Conflicted files: ${info.conflictFiles.join(", ") || "(unknown)"}`,
|
|
491
|
+
`Base working tree: ${info.cwd}`
|
|
492
|
+
].join("\n"),
|
|
493
|
+
// 'critical', not 'high': choosing `resolve` lets a resolver agent EDIT
|
|
494
|
+
// FILES IN THE BASE WORKING TREE. That is the one Brain decision in the
|
|
495
|
+
// system whose wrong answer is not recoverable by simply continuing —
|
|
496
|
+
// unlike a budget grant (bounded, capped) or a goal-done verdict (the
|
|
497
|
+
// run can be restarted).
|
|
498
|
+
//
|
|
499
|
+
// Consequences of the level, both intended:
|
|
500
|
+
// - it is at or above the council floor, so a multi-LLM panel decides
|
|
501
|
+
// it whenever one can convene;
|
|
502
|
+
// - with no council (a single-model install resolves the autonomy
|
|
503
|
+
// ceiling to 'high'), it exceeds the ceiling and never reaches an
|
|
504
|
+
// unchecked single model. The policy tier's `ask_human` then routes
|
|
505
|
+
// to the terminal policy, which refuses a recommended option at
|
|
506
|
+
// critical risk and denies — i.e. the worktree is kept for human
|
|
507
|
+
// review, which is exactly the safe outcome.
|
|
508
|
+
risk: "critical",
|
|
509
|
+
fallback: "ask_human",
|
|
510
|
+
options: [
|
|
511
|
+
{
|
|
512
|
+
id: "resolve",
|
|
513
|
+
label: "Try the configured conflict resolver",
|
|
514
|
+
consequence: "A resolver agent may edit conflicted files in the base working tree.",
|
|
515
|
+
risk: "critical"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
id: "review",
|
|
519
|
+
label: "Keep the worktree for human review",
|
|
520
|
+
consequence: "No automatic conflict resolution is attempted.",
|
|
521
|
+
risk: "low",
|
|
522
|
+
recommended: true
|
|
523
|
+
}
|
|
524
|
+
]
|
|
525
|
+
});
|
|
526
|
+
phase.metadata = {
|
|
527
|
+
...phase.metadata,
|
|
528
|
+
brainConflictDecision: decision.type,
|
|
529
|
+
brainConflictDecisionAt: Date.now()
|
|
530
|
+
};
|
|
531
|
+
if (decision.type !== "answer") return false;
|
|
532
|
+
return decision.optionId === "resolve";
|
|
533
|
+
}
|
|
534
|
+
async function keepWorktreeForReview(int, phase) {
|
|
535
|
+
const handle = int.phaseWorktrees.get(phase.id);
|
|
536
|
+
if (!int.worktrees || !handle) return;
|
|
537
|
+
try {
|
|
538
|
+
await int.worktrees.commitAll(handle, `goal(${phase.name}) [failed]: ${phase.id}`);
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
setIntegrationMetadata(int.graph, phase, "not_merged_failed_phase", {
|
|
542
|
+
branch: handle.branch,
|
|
543
|
+
worktreeDir: handle.dir
|
|
544
|
+
});
|
|
545
|
+
await int.worktrees.release(handle, { keep: true }).catch(() => {
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/goal/phase-orchestrator-runtime.ts
|
|
550
|
+
function normalizePhaseGraphForResume(graph) {
|
|
551
|
+
graph.activePhaseIds = [];
|
|
552
|
+
for (const phase of graph.phases.values()) {
|
|
553
|
+
if (phase.status === "running") {
|
|
554
|
+
phase.status = "pending";
|
|
555
|
+
phase.updatedAt = Date.now();
|
|
556
|
+
}
|
|
557
|
+
if (phase.status === "pending") {
|
|
558
|
+
for (const task of phase.taskGraph.nodes.values()) {
|
|
559
|
+
if (task.status === "in_progress") task.status = "pending";
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function createNoopEventBus() {
|
|
565
|
+
return {
|
|
566
|
+
emit: () => {
|
|
567
|
+
},
|
|
568
|
+
on: () => () => {
|
|
569
|
+
},
|
|
570
|
+
off: () => {
|
|
571
|
+
},
|
|
572
|
+
once: () => () => {
|
|
573
|
+
},
|
|
574
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
575
|
+
wildcards: [],
|
|
576
|
+
setLogger: () => {
|
|
577
|
+
},
|
|
578
|
+
onAny: () => () => {
|
|
579
|
+
},
|
|
580
|
+
offAny: () => {
|
|
581
|
+
},
|
|
582
|
+
emitAsync: async () => [],
|
|
583
|
+
waitFor: async () => {
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/goal/phase-orchestrator.ts
|
|
589
|
+
var PhaseOrchestrator = class {
|
|
590
|
+
graph;
|
|
591
|
+
ctx;
|
|
592
|
+
opts;
|
|
593
|
+
events;
|
|
594
|
+
stopped = false;
|
|
595
|
+
paused = false;
|
|
596
|
+
/**
|
|
597
|
+
* Run-wide abort source. stop() aborts it; every in-flight
|
|
598
|
+
* ctx.executeTask call observes the abort through its per-task signal
|
|
599
|
+
* (composed from this controller and the task's own timeout controller).
|
|
600
|
+
* Recreated by start() so a stopped orchestrator can be reused.
|
|
601
|
+
*/
|
|
602
|
+
stopController = new AbortController();
|
|
603
|
+
runningPhases = /* @__PURE__ */ new Set();
|
|
604
|
+
tickInterval = null;
|
|
605
|
+
trackerCache = /* @__PURE__ */ new Map();
|
|
606
|
+
taskRetryCounts = /* @__PURE__ */ new Map();
|
|
607
|
+
// ── Git-worktree isolation (optional) ──────────────────────────────────────
|
|
608
|
+
worktrees;
|
|
609
|
+
logger;
|
|
610
|
+
/** Per-phase worktree handles, keyed by phase id. */
|
|
611
|
+
phaseWorktrees = /* @__PURE__ */ new Map();
|
|
612
|
+
/** Serializes all merges back to the base branch (one at a time). */
|
|
613
|
+
mergeQueue = Promise.resolve();
|
|
614
|
+
/** Per-phase merge promise, so a phase merges only after its deps do. */
|
|
615
|
+
phaseMergePromise = /* @__PURE__ */ new Map();
|
|
616
|
+
constructor(opts) {
|
|
617
|
+
this.graph = opts.graph;
|
|
618
|
+
this.ctx = opts.ctx;
|
|
619
|
+
this.events = opts.events ?? createNoopEventBus();
|
|
620
|
+
this.worktrees = opts.worktrees;
|
|
621
|
+
this.logger = opts.logger ?? noOpLogger;
|
|
622
|
+
this.opts = {
|
|
623
|
+
maxConcurrentPhases: opts.maxConcurrentPhases ?? 1,
|
|
624
|
+
maxConcurrentTasks: opts.maxConcurrentTasks ?? 2,
|
|
625
|
+
maxRetries: opts.maxRetries ?? 2,
|
|
626
|
+
maxVerifyAttempts: opts.maxVerifyAttempts ?? 2,
|
|
627
|
+
autonomous: opts.autonomous ?? true,
|
|
628
|
+
phaseDelayMs: opts.phaseDelayMs ?? 0,
|
|
629
|
+
// Autonomous goals must fail closed. Continuing after a terminal task or
|
|
630
|
+
// gate failure can make dependent phases look complete without their
|
|
631
|
+
// required evidence.
|
|
632
|
+
stopOnFailure: opts.stopOnFailure ?? true,
|
|
633
|
+
taskTimeoutMs: opts.taskTimeoutMs ?? 6e5,
|
|
634
|
+
events: this.events
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
638
|
+
/**
|
|
639
|
+
* Start the full phase flow.
|
|
640
|
+
* In autonomous mode, starts root phases and automatically starts the next phase when they finish.
|
|
641
|
+
*/
|
|
642
|
+
async start() {
|
|
643
|
+
this.stopped = false;
|
|
644
|
+
this.paused = false;
|
|
645
|
+
this.stopController = new AbortController();
|
|
646
|
+
this.normalizeForResume();
|
|
647
|
+
this.graph.startedAt = Date.now();
|
|
648
|
+
this.graph.updatedAt = Date.now();
|
|
649
|
+
let readyPhases = this.getReadyPhases();
|
|
650
|
+
while (readyPhases.length > 0 && !this.stopped) {
|
|
651
|
+
await this.waitWhilePaused();
|
|
652
|
+
if (this.stopped) break;
|
|
653
|
+
const batch = readyPhases.slice(0, this.opts.maxConcurrentPhases);
|
|
654
|
+
await Promise.all(batch.map((p) => this.startPhase(p)));
|
|
655
|
+
if (this.opts.phaseDelayMs > 0) {
|
|
656
|
+
await this.delay(this.opts.phaseDelayMs);
|
|
657
|
+
}
|
|
658
|
+
await this.waitWhilePaused();
|
|
659
|
+
if (this.stopped) break;
|
|
660
|
+
readyPhases = this.getReadyPhases().filter(
|
|
661
|
+
(p) => !this.runningPhases.has(p.id) && p.status !== "completed" && p.status !== "failed"
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
await this.drainMerges();
|
|
665
|
+
if (this.opts.autonomous) {
|
|
666
|
+
this.tickInterval = setInterval(() => this.tick(), 1e3);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Make a (possibly resumed) graph runnable. A graph loaded from disk after an
|
|
671
|
+
* interrupted run can carry transient state from the dead process: phases left
|
|
672
|
+
* `running` and tasks left `in_progress`. The scheduler only starts `pending`
|
|
673
|
+
* phases (getReadyPhases) and only runs `pending` tasks (getExecutableTasks),
|
|
674
|
+
* so without this a resumed phase/task would stall forever. Reset that
|
|
675
|
+
* transient state to `pending`; terminal phases (completed/failed/skipped) and
|
|
676
|
+
* already-completed tasks are untouched, so completed work is never re-run.
|
|
677
|
+
* For a freshly built graph this is a no-op.
|
|
678
|
+
*/
|
|
679
|
+
normalizeForResume() {
|
|
680
|
+
normalizePhaseGraphForResume(this.graph);
|
|
681
|
+
}
|
|
682
|
+
/** Wait for all pending phase merges, dependency-ordered and globally serialized. */
|
|
683
|
+
async drainMerges() {
|
|
684
|
+
await Promise.allSettled([...this.phaseMergePromise.values()]);
|
|
685
|
+
await this.mergeQueue.catch((err) => {
|
|
686
|
+
const msg = toErrorMessage(err);
|
|
687
|
+
this.logger.warn(msg, { event: "orchestrator.merge_queue_failed" });
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/** Pause: active phases continue, but no new phase starts. */
|
|
691
|
+
pause() {
|
|
692
|
+
this.paused = true;
|
|
693
|
+
}
|
|
694
|
+
/** Resume: new phases may start again. */
|
|
695
|
+
resume() {
|
|
696
|
+
this.paused = false;
|
|
697
|
+
this.tick().catch((err) => {
|
|
698
|
+
const msg = toErrorMessage(err);
|
|
699
|
+
this.logger.error(msg, { event: "orchestrator.tick_failed" });
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
/** Stop completely, including active phases. */
|
|
703
|
+
stop() {
|
|
704
|
+
this.stopped = true;
|
|
705
|
+
this.stopController.abort();
|
|
706
|
+
if (this.tickInterval) {
|
|
707
|
+
clearInterval(this.tickInterval);
|
|
708
|
+
this.tickInterval = null;
|
|
709
|
+
}
|
|
710
|
+
for (const phaseId of this.runningPhases) {
|
|
711
|
+
const phase = this.graph.phases.get(phaseId);
|
|
712
|
+
if (phase) {
|
|
713
|
+
this.updatePhaseStatus(phase, "paused");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
this.trackerCache.clear();
|
|
717
|
+
this.taskRetryCounts.clear();
|
|
718
|
+
if (this.worktrees) {
|
|
719
|
+
for (const handle of this.worktrees.list()) {
|
|
720
|
+
void this.worktrees.release(handle, { keep: true }).catch(() => {
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// ─── Tick Loop (Autonomous) ───────────────────────────────────────────────
|
|
726
|
+
async tick() {
|
|
727
|
+
if (this.stopped || this.paused) return;
|
|
728
|
+
const active = this.getActivePhases();
|
|
729
|
+
const queued = this.getReadyPhases();
|
|
730
|
+
this.emit("autonomous.tick", {
|
|
731
|
+
activePhases: active.map((p) => p.id),
|
|
732
|
+
queuedPhases: queued.map((p) => p.id)
|
|
733
|
+
});
|
|
734
|
+
this.ctx.onTick?.({ activePhases: active, readyPhases: queued });
|
|
735
|
+
const availableSlots = this.opts.maxConcurrentPhases - active.length;
|
|
736
|
+
if (availableSlots > 0 && queued.length > 0) {
|
|
737
|
+
for (const phase of queued.slice(0, availableSlots)) {
|
|
738
|
+
if (phase.status === "pending") {
|
|
739
|
+
await this.startPhase(phase);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (this.isComplete()) {
|
|
744
|
+
this.onGraphComplete();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (this.opts.stopOnFailure && this.graph.failedPhaseIds.length > 0) {
|
|
748
|
+
const failedPhase = this.graph.phases.get(this.graph.failedPhaseIds[0] ?? "");
|
|
749
|
+
if (failedPhase) {
|
|
750
|
+
this.onGraphFailed(failedPhase);
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// ─── Phase Execution ──────────────────────────────────────────────────────
|
|
756
|
+
async startPhase(phase) {
|
|
757
|
+
if (phase.status !== "pending" && phase.status !== "ready") return;
|
|
758
|
+
this.updatePhaseStatus(phase, "running");
|
|
759
|
+
phase.startedAt = Date.now();
|
|
760
|
+
this.runningPhases.add(phase.id);
|
|
761
|
+
this.graph.activePhaseIds.push(phase.id);
|
|
762
|
+
if (this.worktrees && !this.phaseWorktrees.has(phase.id)) {
|
|
763
|
+
try {
|
|
764
|
+
const handle = await this.worktrees.allocate(phase.id, {
|
|
765
|
+
slugHint: phase.name,
|
|
766
|
+
ownerLabel: phase.name
|
|
767
|
+
});
|
|
768
|
+
if (handle.status === "active") this.phaseWorktrees.set(phase.id, handle);
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
this.emit("phase.started", { phaseId: phase.id, name: phase.name });
|
|
773
|
+
try {
|
|
774
|
+
if (phase.taskGraph.nodes.size === 0) {
|
|
775
|
+
await this.failPhaseAfterTasks(
|
|
776
|
+
phase,
|
|
777
|
+
"Phase has no executable tasks. Add task evidence before starting this phase."
|
|
778
|
+
);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
await this.executePhaseTasks(phase);
|
|
782
|
+
if (this.stopped) return;
|
|
783
|
+
const failedTasks = this.getFailedTaskCount(phase);
|
|
784
|
+
const completedTasks = this.getCompletedTaskCount(phase);
|
|
785
|
+
this.emit("phase.allTasksDone", {
|
|
786
|
+
phaseId: phase.id,
|
|
787
|
+
completed: completedTasks,
|
|
788
|
+
failed: failedTasks
|
|
789
|
+
});
|
|
790
|
+
if (failedTasks > 0 && this.opts.stopOnFailure) {
|
|
791
|
+
await this.failPhaseAfterTasks(phase, `${failedTasks} task(s) failed`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const verdict = await this.runVerifyGate(phase);
|
|
795
|
+
if (!verdict.ok) {
|
|
796
|
+
await this.failPhaseAfterTasks(
|
|
797
|
+
phase,
|
|
798
|
+
`verification failed${verdict.output ? `: ${this.truncate(verdict.output)}` : ""}`
|
|
799
|
+
);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
this.updatePhaseStatus(phase, "completed");
|
|
803
|
+
phase.completedAt = Date.now();
|
|
804
|
+
phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
|
|
805
|
+
this.runningPhases.delete(phase.id);
|
|
806
|
+
this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
|
|
807
|
+
this.graph.completedPhaseIds.push(phase.id);
|
|
808
|
+
this.emit("phase.completed", {
|
|
809
|
+
phaseId: phase.id,
|
|
810
|
+
name: phase.name,
|
|
811
|
+
durationMs: phase.actualDurationMs
|
|
812
|
+
});
|
|
813
|
+
this.ctx.onPhaseComplete?.(phase);
|
|
814
|
+
await this.commitAndEnqueueMerge(phase);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
this.updatePhaseStatus(phase, "failed");
|
|
817
|
+
phase.completedAt = Date.now();
|
|
818
|
+
phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
|
|
819
|
+
this.runningPhases.delete(phase.id);
|
|
820
|
+
this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
|
|
821
|
+
this.graph.failedPhaseIds.push(phase.id);
|
|
822
|
+
this.emit("phase.failed", {
|
|
823
|
+
phaseId: phase.id,
|
|
824
|
+
name: phase.name,
|
|
825
|
+
error: error instanceof Error ? error.message : String(error)
|
|
826
|
+
});
|
|
827
|
+
this.ctx.onPhaseFail?.(phase, error instanceof Error ? error : new Error(String(error)));
|
|
828
|
+
await this.keepWorktreeForReview(phase);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// ─── Verification gate ──────────────────────────────────────────────────────
|
|
832
|
+
/**
|
|
833
|
+
* Run the verification gate for a phase whose tasks all succeeded. Verifies in
|
|
834
|
+
* the phase's worktree; on failure, runs the repair pass and re-verifies, up to
|
|
835
|
+
* `maxVerifyAttempts` repairs. Returns the final verdict. When no `verifyPhase`
|
|
836
|
+
* callback is wired the gate is a no-op and always passes.
|
|
837
|
+
*/
|
|
838
|
+
async runVerifyGate(phase) {
|
|
839
|
+
if (!this.ctx.verifyPhase) return { ok: true };
|
|
840
|
+
const env = this.worktreeEnv(phase);
|
|
841
|
+
for (let attempt = 0; attempt <= this.opts.maxVerifyAttempts; attempt++) {
|
|
842
|
+
if (this.stopped) return { ok: false, output: "stopped before verification completed" };
|
|
843
|
+
this.emit("phase.verifying", { phaseId: phase.id, name: phase.name, attempt });
|
|
844
|
+
let verdict;
|
|
845
|
+
try {
|
|
846
|
+
verdict = await this.ctx.verifyPhase(phase, env);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
verdict = { ok: false, output: toErrorMessage(err) };
|
|
849
|
+
}
|
|
850
|
+
if (verdict.ok) return { ok: true };
|
|
851
|
+
this.emit("phase.verifyFailed", {
|
|
852
|
+
phaseId: phase.id,
|
|
853
|
+
name: phase.name,
|
|
854
|
+
attempt,
|
|
855
|
+
error: verdict.output
|
|
856
|
+
});
|
|
857
|
+
if (attempt >= this.opts.maxVerifyAttempts || !this.ctx.repairPhase || this.stopped) {
|
|
858
|
+
return { ok: false, output: verdict.output };
|
|
859
|
+
}
|
|
860
|
+
this.emit("phase.repairing", { phaseId: phase.id, name: phase.name, attempt: attempt + 1 });
|
|
861
|
+
try {
|
|
862
|
+
await this.ctx.repairPhase(
|
|
863
|
+
phase,
|
|
864
|
+
verdict.output ?? "verification failed",
|
|
865
|
+
attempt + 1,
|
|
866
|
+
env
|
|
867
|
+
);
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return { ok: false };
|
|
872
|
+
}
|
|
873
|
+
/** Worktree env (cwd/branch) for a phase, or undefined if it runs on the shared tree. */
|
|
874
|
+
/** Bundle of orchestrator state the worktree-integration steps drive. */
|
|
875
|
+
integrationCtx() {
|
|
876
|
+
return {
|
|
877
|
+
graph: this.graph,
|
|
878
|
+
ctx: this.ctx,
|
|
879
|
+
logger: this.logger,
|
|
880
|
+
worktrees: this.worktrees,
|
|
881
|
+
phaseWorktrees: this.phaseWorktrees,
|
|
882
|
+
phaseMergePromise: this.phaseMergePromise,
|
|
883
|
+
runningPhases: this.runningPhases,
|
|
884
|
+
getMergeQueue: () => this.mergeQueue,
|
|
885
|
+
setMergeQueue: (queue) => {
|
|
886
|
+
this.mergeQueue = queue;
|
|
887
|
+
},
|
|
888
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
889
|
+
updatePhaseStatus: (phase, status) => this.updatePhaseStatus(phase, status)
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
worktreeEnv(phase) {
|
|
893
|
+
return worktreeEnv(this.integrationCtx(), phase);
|
|
894
|
+
}
|
|
895
|
+
async failPhaseAfterTasks(phase, error) {
|
|
896
|
+
await failPhaseAfterTasks(this.integrationCtx(), phase, error);
|
|
897
|
+
}
|
|
898
|
+
/** Trim long verifier output so it fits cleanly in an event/error message. */
|
|
899
|
+
truncate(text, max = 500) {
|
|
900
|
+
return truncate(text, max);
|
|
901
|
+
}
|
|
902
|
+
// ─── Worktree integration ───────────────────────────────────────────────────
|
|
903
|
+
async commitAndEnqueueMerge(phase) {
|
|
904
|
+
await commitAndEnqueueMerge(this.integrationCtx(), phase);
|
|
905
|
+
}
|
|
906
|
+
/** A failed phase keeps its worktree on disk for inspection (no merge). */
|
|
907
|
+
async keepWorktreeForReview(phase) {
|
|
908
|
+
await keepWorktreeForReview(this.integrationCtx(), phase);
|
|
909
|
+
}
|
|
910
|
+
async executePhaseTasks(phase) {
|
|
911
|
+
const pendingTasks = this.getExecutableTasks(phase);
|
|
912
|
+
while (pendingTasks.length > 0 && !this.stopped) {
|
|
913
|
+
const batch = pendingTasks.splice(0, this.opts.maxConcurrentTasks);
|
|
914
|
+
const results = await Promise.allSettled(
|
|
915
|
+
batch.map((task) => this.executeSingleTask(task, phase))
|
|
916
|
+
);
|
|
917
|
+
for (let i = 0; i < results.length; i++) {
|
|
918
|
+
const result = results[i];
|
|
919
|
+
const task = batch[i];
|
|
920
|
+
if (!result || !task) continue;
|
|
921
|
+
if (result.status === "fulfilled") {
|
|
922
|
+
this.markTaskCompleted(phase, task);
|
|
923
|
+
} else {
|
|
924
|
+
this.markTaskFailed(phase, task, result.reason);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const newReady = this.getExecutableTasks(phase);
|
|
928
|
+
pendingTasks.length = 0;
|
|
929
|
+
pendingTasks.push(...newReady);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async executeSingleTask(task, phase) {
|
|
933
|
+
const tracker = this.getTrackerForPhase(phase);
|
|
934
|
+
tracker.updateNodeStatus(task.id, "in_progress");
|
|
935
|
+
this.emit("phase.taskStarted", {
|
|
936
|
+
phaseId: phase.id,
|
|
937
|
+
taskId: task.id,
|
|
938
|
+
taskTitle: task.title,
|
|
939
|
+
agentName: task.assignee
|
|
940
|
+
});
|
|
941
|
+
const handle = this.phaseWorktrees.get(phase.id);
|
|
942
|
+
const timeoutController = this.opts.taskTimeoutMs > 0 ? new AbortController() : void 0;
|
|
943
|
+
const signal = timeoutController ? AbortSignal.any([this.stopController.signal, timeoutController.signal]) : this.stopController.signal;
|
|
944
|
+
const taskPromise = this.ctx.executeTask(
|
|
945
|
+
task,
|
|
946
|
+
phase.id,
|
|
947
|
+
{ cwd: handle?.dir, branch: handle?.branch },
|
|
948
|
+
signal
|
|
949
|
+
);
|
|
950
|
+
if (!timeoutController) return taskPromise;
|
|
951
|
+
const timeoutMs = this.opts.taskTimeoutMs;
|
|
952
|
+
const timedOut = /* @__PURE__ */ Symbol("timed_out");
|
|
953
|
+
const result = await Promise.race([
|
|
954
|
+
taskPromise,
|
|
955
|
+
new Promise((resolve2) => {
|
|
956
|
+
const timer = setTimeout(() => {
|
|
957
|
+
timeoutController.abort();
|
|
958
|
+
resolve2(timedOut);
|
|
959
|
+
}, timeoutMs);
|
|
960
|
+
taskPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
|
|
961
|
+
})
|
|
962
|
+
]);
|
|
963
|
+
if (result !== timedOut) return result;
|
|
964
|
+
this.emit("phase.taskTimedOut", {
|
|
965
|
+
phaseId: phase.id,
|
|
966
|
+
taskId: task.id,
|
|
967
|
+
taskTitle: task.title,
|
|
968
|
+
timeoutMs
|
|
969
|
+
});
|
|
970
|
+
const settled = taskPromise.then(
|
|
971
|
+
() => void 0,
|
|
972
|
+
() => void 0
|
|
973
|
+
);
|
|
974
|
+
const grace = new Promise((resolve2) => {
|
|
975
|
+
const timer = setTimeout(resolve2, 5e3);
|
|
976
|
+
timer.unref?.();
|
|
977
|
+
void settled.then(() => clearTimeout(timer));
|
|
978
|
+
});
|
|
979
|
+
await Promise.race([settled, grace]);
|
|
980
|
+
throw new Error(
|
|
981
|
+
`Task "${task.title}" (${task.id}) exceeded timeout of ${timeoutMs} ms`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
markTaskCompleted(phase, task) {
|
|
985
|
+
const tracker = this.getTrackerForPhase(phase);
|
|
986
|
+
tracker.updateNodeStatus(task.id, "completed");
|
|
987
|
+
this.emit("phase.taskCompleted", {
|
|
988
|
+
phaseId: phase.id,
|
|
989
|
+
taskId: task.id,
|
|
990
|
+
taskTitle: task.title
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
markTaskFailed(phase, task, error) {
|
|
994
|
+
const tracker = this.getTrackerForPhase(phase);
|
|
995
|
+
const taskKey = `${phase.id}:${task.id}`;
|
|
996
|
+
const currentRetries = this.taskRetryCounts.get(taskKey) ?? 0;
|
|
997
|
+
if (this.stopped) {
|
|
998
|
+
tracker.updateNodeStatus(task.id, "pending", "Stopped before completion");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (currentRetries < this.opts.maxRetries) {
|
|
1002
|
+
this.taskRetryCounts.set(taskKey, currentRetries + 1);
|
|
1003
|
+
tracker.updateNodeStatus(
|
|
1004
|
+
task.id,
|
|
1005
|
+
"pending",
|
|
1006
|
+
`Retry ${currentRetries + 1}/${this.opts.maxRetries}`
|
|
1007
|
+
);
|
|
1008
|
+
this.emit("phase.taskRetrying", {
|
|
1009
|
+
phaseId: phase.id,
|
|
1010
|
+
taskId: task.id,
|
|
1011
|
+
taskTitle: task.title,
|
|
1012
|
+
attempt: currentRetries + 1,
|
|
1013
|
+
maxRetries: this.opts.maxRetries
|
|
1014
|
+
});
|
|
1015
|
+
} else {
|
|
1016
|
+
tracker.updateNodeStatus(
|
|
1017
|
+
task.id,
|
|
1018
|
+
"failed",
|
|
1019
|
+
error instanceof Error ? error.message : String(error)
|
|
1020
|
+
);
|
|
1021
|
+
this.emit("phase.taskFailed", {
|
|
1022
|
+
phaseId: phase.id,
|
|
1023
|
+
taskId: task.id,
|
|
1024
|
+
taskTitle: task.title,
|
|
1025
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
1030
|
+
getReadyPhases() {
|
|
1031
|
+
return getReadyPhases(this.graph);
|
|
1032
|
+
}
|
|
1033
|
+
getActivePhases() {
|
|
1034
|
+
return getActivePhases(this.graph);
|
|
1035
|
+
}
|
|
1036
|
+
getExecutableTasks(phase) {
|
|
1037
|
+
return getExecutableTasks((p) => this.getTrackerForPhase(p), phase);
|
|
1038
|
+
}
|
|
1039
|
+
getTrackerForPhase(phase) {
|
|
1040
|
+
const cached = this.trackerCache.get(phase.id);
|
|
1041
|
+
if (cached) return cached;
|
|
1042
|
+
const tracker = new TaskTracker({ store: new DefaultTaskStore() });
|
|
1043
|
+
tracker.setGraph(phase.taskGraph);
|
|
1044
|
+
this.trackerCache.set(phase.id, tracker);
|
|
1045
|
+
return tracker;
|
|
1046
|
+
}
|
|
1047
|
+
getFailedTaskCount(phase) {
|
|
1048
|
+
return getFailedTaskCount((p) => this.getTrackerForPhase(p), phase);
|
|
1049
|
+
}
|
|
1050
|
+
getCompletedTaskCount(phase) {
|
|
1051
|
+
return getCompletedTaskCount((p) => this.getTrackerForPhase(p), phase);
|
|
1052
|
+
}
|
|
1053
|
+
updatePhaseStatus(phase, status) {
|
|
1054
|
+
const from = phase.status;
|
|
1055
|
+
phase.status = status;
|
|
1056
|
+
phase.updatedAt = Date.now();
|
|
1057
|
+
this.graph.updatedAt = Date.now();
|
|
1058
|
+
this.emit("phase.statusChange", { phaseId: phase.id, from, to: status });
|
|
1059
|
+
}
|
|
1060
|
+
isComplete() {
|
|
1061
|
+
return isGraphComplete(this.graph);
|
|
1062
|
+
}
|
|
1063
|
+
onGraphComplete() {
|
|
1064
|
+
this.graph.completedAt = Date.now();
|
|
1065
|
+
const durationMs = this.graph.completedAt - (this.graph.startedAt ?? this.graph.completedAt);
|
|
1066
|
+
this.emit("graph.completed", { graphId: this.graph.id, durationMs });
|
|
1067
|
+
this.stop();
|
|
1068
|
+
}
|
|
1069
|
+
onGraphFailed(failedPhase) {
|
|
1070
|
+
this.emit("graph.failed", {
|
|
1071
|
+
graphId: this.graph.id,
|
|
1072
|
+
failedPhaseId: failedPhase.id,
|
|
1073
|
+
error: `Phase "${failedPhase.name}" failed`
|
|
1074
|
+
});
|
|
1075
|
+
this.stop();
|
|
1076
|
+
}
|
|
1077
|
+
// ─── Progress ─────────────────────────────────────────────────────────────
|
|
1078
|
+
getProgress() {
|
|
1079
|
+
return getProgress(this.graph, (p) => this.getTrackerForPhase(p));
|
|
1080
|
+
}
|
|
1081
|
+
getGraph() {
|
|
1082
|
+
return this.graph;
|
|
1083
|
+
}
|
|
1084
|
+
isRunning() {
|
|
1085
|
+
return !this.stopped && this.runningPhases.size > 0;
|
|
1086
|
+
}
|
|
1087
|
+
isPaused() {
|
|
1088
|
+
return this.paused;
|
|
1089
|
+
}
|
|
1090
|
+
// ─── Agent Assignment ─────────────────────────────────────────────────────
|
|
1091
|
+
assignAgent(phaseId, agentId) {
|
|
1092
|
+
const phase = this.graph.phases.get(phaseId);
|
|
1093
|
+
if (!phase) return;
|
|
1094
|
+
if (!phase.assignedAgents.includes(agentId)) {
|
|
1095
|
+
phase.assignedAgents.push(agentId);
|
|
1096
|
+
this.emit("agent.assigned", { phaseId, agentId });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
releaseAgent(phaseId, agentId) {
|
|
1100
|
+
const phase = this.graph.phases.get(phaseId);
|
|
1101
|
+
if (!phase) return;
|
|
1102
|
+
phase.assignedAgents = phase.assignedAgents.filter((id) => id !== agentId);
|
|
1103
|
+
this.emit("agent.released", { phaseId, agentId });
|
|
1104
|
+
}
|
|
1105
|
+
// ─── Interactive board mutations ──────────────────────────────────────────
|
|
1106
|
+
//
|
|
1107
|
+
// These are driven by an interactive board (WebUI/TUI), not the autonomous
|
|
1108
|
+
// loop. Each mutates the live graph, emits a typed event so every surface
|
|
1109
|
+
// stays in sync, and bumps updatedAt so the host re-persists.
|
|
1110
|
+
/** Find the phase whose task graph currently holds `taskId`. */
|
|
1111
|
+
findPhaseOfTask(taskId) {
|
|
1112
|
+
return findPhaseOfTaskInGraph(this.graph, taskId);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Move a task to another phase's task graph. Edges that referenced the task
|
|
1116
|
+
* are dropped (cross-phase dependencies are not modeled). No-op when the task
|
|
1117
|
+
* or target phase is missing, or it is already in the target phase.
|
|
1118
|
+
*/
|
|
1119
|
+
moveTask(taskId, toPhaseId) {
|
|
1120
|
+
return movePhaseTask(this.boardMutationContext(), taskId, toPhaseId);
|
|
1121
|
+
}
|
|
1122
|
+
/** (Re)assign a task to a specific agent (or clear with agentName/agentId omitted). */
|
|
1123
|
+
setTaskAssignee(taskId, agentId, agentName) {
|
|
1124
|
+
return setPhaseTaskAssignee(this.boardMutationContext(), taskId, agentId, agentName);
|
|
1125
|
+
}
|
|
1126
|
+
/** Add a new task to a phase. Returns the created task id, or null if the phase is missing. */
|
|
1127
|
+
addTask(phaseId, spec) {
|
|
1128
|
+
return addPhaseTask(this.boardMutationContext(), phaseId, spec);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Requeue a task to `pending` (clearing its retry counter) and nudge a
|
|
1132
|
+
* terminal/paused phase back to `ready` so the loop re-runs it. Backs both the
|
|
1133
|
+
* board's "retry" and "start" affordances.
|
|
1134
|
+
*/
|
|
1135
|
+
requeueTask(taskId) {
|
|
1136
|
+
return requeuePhaseTask(this.boardMutationContext(), taskId);
|
|
1137
|
+
}
|
|
1138
|
+
boardMutationContext() {
|
|
1139
|
+
return {
|
|
1140
|
+
graph: this.graph,
|
|
1141
|
+
trackerCache: this.trackerCache,
|
|
1142
|
+
taskRetryCounts: this.taskRetryCounts,
|
|
1143
|
+
phaseWorktrees: this.phaseWorktrees,
|
|
1144
|
+
getTrackerForPhase: (phase) => this.getTrackerForPhase(phase),
|
|
1145
|
+
updatePhaseStatus: (phase, status) => this.updatePhaseStatus(phase, status),
|
|
1146
|
+
emit: (event, payload) => this.emit(event, payload)
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
// ─── Events ───────────────────────────────────────────────────────────────
|
|
1150
|
+
emit(event, payload) {
|
|
1151
|
+
this.events.emit(event, payload);
|
|
1152
|
+
}
|
|
1153
|
+
async waitWhilePaused() {
|
|
1154
|
+
while (this.paused && !this.stopped) {
|
|
1155
|
+
await this.delay(100);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
delay(ms) {
|
|
1159
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/goal/goal-runner.ts
|
|
1164
|
+
var GoalRunner = class {
|
|
1165
|
+
graph = null;
|
|
1166
|
+
orchestrator = null;
|
|
1167
|
+
opts;
|
|
1168
|
+
progressInterval = null;
|
|
1169
|
+
maxRunTimer = null;
|
|
1170
|
+
graphCompletedHandler = (payload) => {
|
|
1171
|
+
const p = payload;
|
|
1172
|
+
if (this.graph && p.graphId === this.graph.id) {
|
|
1173
|
+
this.opts.onComplete?.(this.graph);
|
|
1174
|
+
this.cleanup();
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
graphFailedHandler = (payload) => {
|
|
1178
|
+
const p = payload;
|
|
1179
|
+
if (this.graph && p.graphId === this.graph.id) {
|
|
1180
|
+
const failedPhase = this.graph.phases.get(p.failedPhaseId);
|
|
1181
|
+
if (failedPhase) {
|
|
1182
|
+
this.opts.onFail?.(this.graph, failedPhase, new Error(p.error));
|
|
1183
|
+
}
|
|
1184
|
+
if (this.opts.stopOnFailure) {
|
|
1185
|
+
this.cleanup();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
/** Stores the unsubscribe function returned by EventBus.on() */
|
|
1190
|
+
unsubscribeCompleted = null;
|
|
1191
|
+
unsubscribeFailed = null;
|
|
1192
|
+
constructor(opts) {
|
|
1193
|
+
this.opts = opts;
|
|
1194
|
+
}
|
|
1195
|
+
async start() {
|
|
1196
|
+
const builder = new PhaseGraphBuilder({
|
|
1197
|
+
title: this.opts.title,
|
|
1198
|
+
description: this.opts.description,
|
|
1199
|
+
phases: this.opts.phases,
|
|
1200
|
+
autonomous: this.opts.autonomous,
|
|
1201
|
+
stopOnFailure: this.opts.stopOnFailure
|
|
1202
|
+
});
|
|
1203
|
+
this.graph = await builder.build();
|
|
1204
|
+
const ctx = {
|
|
1205
|
+
executeTask: this.opts.executeTask,
|
|
1206
|
+
brain: this.opts.brain,
|
|
1207
|
+
onPhaseComplete: (phase) => {
|
|
1208
|
+
this.opts.onPhaseComplete?.(phase);
|
|
1209
|
+
},
|
|
1210
|
+
onPhaseFail: (phase, error) => {
|
|
1211
|
+
this.opts.onPhaseFail?.(phase, error);
|
|
1212
|
+
},
|
|
1213
|
+
onTick: (tickCtx) => {
|
|
1214
|
+
this.opts.onTick?.(tickCtx);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
if (this.opts.verifyPhase !== void 0) ctx.verifyPhase = this.opts.verifyPhase;
|
|
1218
|
+
if (this.opts.repairPhase !== void 0) ctx.repairPhase = this.opts.repairPhase;
|
|
1219
|
+
if (this.opts.resolveConflict !== void 0) ctx.resolveConflict = this.opts.resolveConflict;
|
|
1220
|
+
this.orchestrator = new PhaseOrchestrator({
|
|
1221
|
+
graph: this.graph,
|
|
1222
|
+
ctx,
|
|
1223
|
+
maxConcurrentPhases: this.opts.maxConcurrentPhases,
|
|
1224
|
+
maxConcurrentTasks: this.opts.maxConcurrentTasks,
|
|
1225
|
+
maxRetries: this.opts.maxRetries,
|
|
1226
|
+
maxVerifyAttempts: this.opts.maxVerifyAttempts,
|
|
1227
|
+
autonomous: this.opts.autonomous,
|
|
1228
|
+
phaseDelayMs: this.opts.phaseDelayMs,
|
|
1229
|
+
stopOnFailure: this.opts.stopOnFailure,
|
|
1230
|
+
events: this.opts.events,
|
|
1231
|
+
worktrees: this.opts.worktrees
|
|
1232
|
+
});
|
|
1233
|
+
if (this.opts.events) {
|
|
1234
|
+
const bus = this.opts.events;
|
|
1235
|
+
const onUntyped = (event, handler) => bus.on(event, handler);
|
|
1236
|
+
this.unsubscribeCompleted = onUntyped("graph.completed", this.graphCompletedHandler);
|
|
1237
|
+
this.unsubscribeFailed = onUntyped("graph.failed", this.graphFailedHandler);
|
|
1238
|
+
}
|
|
1239
|
+
await this.orchestrator.start();
|
|
1240
|
+
this.maxRunTimer = setTimeout(
|
|
1241
|
+
() => {
|
|
1242
|
+
this.opts.onProgress?.({
|
|
1243
|
+
totalPhases: 0,
|
|
1244
|
+
pending: 0,
|
|
1245
|
+
ready: 0,
|
|
1246
|
+
running: 0,
|
|
1247
|
+
paused: 0,
|
|
1248
|
+
completed: 0,
|
|
1249
|
+
failed: 0,
|
|
1250
|
+
skipped: 0,
|
|
1251
|
+
percentComplete: 0,
|
|
1252
|
+
totalTasks: 0,
|
|
1253
|
+
completedTasks: 0,
|
|
1254
|
+
failedTasks: 0,
|
|
1255
|
+
estimatedHours: 0,
|
|
1256
|
+
actualHours: 0
|
|
1257
|
+
});
|
|
1258
|
+
this.stop();
|
|
1259
|
+
},
|
|
1260
|
+
this.opts.maxRunDurationMs ?? 7 * 24 * 60 * 6e4
|
|
1261
|
+
);
|
|
1262
|
+
if (this.opts.maxRunDurationMs !== void 0 && this.opts.maxRunDurationMs <= 0) {
|
|
1263
|
+
clearTimeout(this.maxRunTimer);
|
|
1264
|
+
this.maxRunTimer = null;
|
|
1265
|
+
}
|
|
1266
|
+
this.maxRunTimer?.unref?.();
|
|
1267
|
+
if (this.opts.onProgress) {
|
|
1268
|
+
this.progressInterval = setInterval(() => {
|
|
1269
|
+
const progress = this.orchestrator?.getProgress();
|
|
1270
|
+
if (progress) this.opts.onProgress?.(progress);
|
|
1271
|
+
}, 2e3);
|
|
1272
|
+
}
|
|
1273
|
+
return this.graph;
|
|
1274
|
+
}
|
|
1275
|
+
pause() {
|
|
1276
|
+
this.orchestrator?.pause();
|
|
1277
|
+
}
|
|
1278
|
+
resume() {
|
|
1279
|
+
this.orchestrator?.resume();
|
|
1280
|
+
}
|
|
1281
|
+
stop() {
|
|
1282
|
+
this.orchestrator?.stop();
|
|
1283
|
+
this.cleanup();
|
|
1284
|
+
}
|
|
1285
|
+
getProgress() {
|
|
1286
|
+
return this.orchestrator?.getProgress() ?? null;
|
|
1287
|
+
}
|
|
1288
|
+
getGraph() {
|
|
1289
|
+
return this.graph;
|
|
1290
|
+
}
|
|
1291
|
+
isRunning() {
|
|
1292
|
+
return this.orchestrator?.isRunning() ?? false;
|
|
1293
|
+
}
|
|
1294
|
+
isPaused() {
|
|
1295
|
+
return this.orchestrator?.isPaused() ?? false;
|
|
1296
|
+
}
|
|
1297
|
+
assignAgent(phaseId, agentId) {
|
|
1298
|
+
this.orchestrator?.assignAgent(phaseId, agentId);
|
|
1299
|
+
}
|
|
1300
|
+
releaseAgent(phaseId, agentId) {
|
|
1301
|
+
this.orchestrator?.releaseAgent(phaseId, agentId);
|
|
1302
|
+
}
|
|
1303
|
+
cleanup() {
|
|
1304
|
+
if (this.progressInterval) {
|
|
1305
|
+
clearInterval(this.progressInterval);
|
|
1306
|
+
this.progressInterval = null;
|
|
1307
|
+
}
|
|
1308
|
+
if (this.maxRunTimer) {
|
|
1309
|
+
clearTimeout(this.maxRunTimer);
|
|
1310
|
+
this.maxRunTimer = null;
|
|
1311
|
+
}
|
|
1312
|
+
this.unsubscribeCompleted?.();
|
|
1313
|
+
this.unsubscribeCompleted = null;
|
|
1314
|
+
this.unsubscribeFailed?.();
|
|
1315
|
+
this.unsubscribeFailed = null;
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
async function createGoalRunnerFromTaskGraph(taskGraph, options) {
|
|
1319
|
+
const graph = await PhaseGraphBuilder.fromTaskGraph(taskGraph, {
|
|
1320
|
+
title: options.title ?? taskGraph.title,
|
|
1321
|
+
tasksPerPhase: options.tasksPerPhase
|
|
1322
|
+
});
|
|
1323
|
+
const phases = Array.from(graph.phases.values()).map((p) => ({
|
|
1324
|
+
name: p.name,
|
|
1325
|
+
description: p.description,
|
|
1326
|
+
priority: p.priority,
|
|
1327
|
+
estimateHours: p.estimateHours,
|
|
1328
|
+
parallelizable: p.parallelizable
|
|
1329
|
+
}));
|
|
1330
|
+
return new GoalRunner({
|
|
1331
|
+
...options,
|
|
1332
|
+
title: options.title ?? taskGraph.title,
|
|
1333
|
+
phases
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/goal/goal-planner.ts
|
|
1338
|
+
var VALID_TASK_TYPES = /* @__PURE__ */ new Set([
|
|
1339
|
+
"feature",
|
|
1340
|
+
"bugfix",
|
|
1341
|
+
"refactor",
|
|
1342
|
+
"docs",
|
|
1343
|
+
"test",
|
|
1344
|
+
"chore"
|
|
1345
|
+
]);
|
|
1346
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set([
|
|
1347
|
+
"critical",
|
|
1348
|
+
"high",
|
|
1349
|
+
"medium",
|
|
1350
|
+
"low"
|
|
1351
|
+
]);
|
|
1352
|
+
var GoalPlanner = class {
|
|
1353
|
+
constructor(opts) {
|
|
1354
|
+
this.opts = opts;
|
|
1355
|
+
}
|
|
1356
|
+
opts;
|
|
1357
|
+
/** Convert the goal into a phase-and-todo plan. */
|
|
1358
|
+
async plan() {
|
|
1359
|
+
const prompt = this.buildPrompt();
|
|
1360
|
+
const raw = await this.opts.runOnce(prompt);
|
|
1361
|
+
const { phases, warnings } = this.parse(raw);
|
|
1362
|
+
const totalTasks = phases.reduce((s, p) => s + (p.taskTemplates?.length ?? 0), 0);
|
|
1363
|
+
const estimatedCostTokens = totalTasks > 0 ? totalTasks * 5 * 2e3 : void 0;
|
|
1364
|
+
const maxTotal = this.opts.maxTotalTasks ?? 0;
|
|
1365
|
+
let budgetExceeded = false;
|
|
1366
|
+
if (maxTotal > 0 && totalTasks > maxTotal) {
|
|
1367
|
+
warnings.push(
|
|
1368
|
+
`Plan has ${totalTasks} total tasks, exceeding the limit of ${maxTotal}. Consider narrowing the scope or increasing the limit.`
|
|
1369
|
+
);
|
|
1370
|
+
budgetExceeded = true;
|
|
1371
|
+
}
|
|
1372
|
+
return { phases, raw, parseFailed: phases.length === 0, warnings, estimatedCostTokens, budgetExceeded };
|
|
1373
|
+
}
|
|
1374
|
+
/** Instruction prompt for the plan the model should produce. */
|
|
1375
|
+
buildPrompt() {
|
|
1376
|
+
const minP = this.opts.minPhases ?? 3;
|
|
1377
|
+
const maxP = this.opts.maxPhases ?? 8;
|
|
1378
|
+
const todos = this.opts.todosPerPhase ?? 6;
|
|
1379
|
+
const ctx = this.opts.projectContext?.trim();
|
|
1380
|
+
return renderInstructionTemplate(readBundledInstructionText("goal/phase-planner.md"), {
|
|
1381
|
+
minPhases: String(minP),
|
|
1382
|
+
maxPhases: String(maxP),
|
|
1383
|
+
todosPerPhase: String(todos),
|
|
1384
|
+
goal: this.opts.goal,
|
|
1385
|
+
projectContext: ctx ? `
|
|
1386
|
+
PROJECT CONTEXT:
|
|
1387
|
+
${ctx}
|
|
1388
|
+
` : ""
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/** Extract JSON from raw output, validate it, and convert it to PhaseTemplate[].
|
|
1392
|
+
* Returns `{ phases, warnings }` where warnings describe coerced defaults. */
|
|
1393
|
+
parse(raw) {
|
|
1394
|
+
const json = extractJSONArray(raw);
|
|
1395
|
+
if (!json) return { phases: [], warnings: ["No JSON array found in model output"] };
|
|
1396
|
+
let data;
|
|
1397
|
+
try {
|
|
1398
|
+
data = JSON.parse(json);
|
|
1399
|
+
} catch {
|
|
1400
|
+
return { phases: [], warnings: ["Failed to parse JSON from model output"] };
|
|
1401
|
+
}
|
|
1402
|
+
if (!Array.isArray(data)) return { phases: [], warnings: ["Model output is not a JSON array"] };
|
|
1403
|
+
const warnings = [];
|
|
1404
|
+
const phases = [];
|
|
1405
|
+
for (const entry of data) {
|
|
1406
|
+
const result = this.coercePhase(entry);
|
|
1407
|
+
if (result) {
|
|
1408
|
+
warnings.push(...result.warnings);
|
|
1409
|
+
if (result.phase.taskTemplates?.length) {
|
|
1410
|
+
phases.push(result.phase);
|
|
1411
|
+
} else {
|
|
1412
|
+
warnings.push(`Phase "${result.phase.name}" has no executable tasks and was rejected.`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return { phases, warnings };
|
|
1417
|
+
}
|
|
1418
|
+
coercePhase(entry) {
|
|
1419
|
+
if (!entry || typeof entry !== "object") return null;
|
|
1420
|
+
const e = entry;
|
|
1421
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
1422
|
+
if (!name) return null;
|
|
1423
|
+
const w = [];
|
|
1424
|
+
const rawTasks = Array.isArray(e.tasks) ? e.tasks : Array.isArray(e.taskTemplates) ? e.taskTemplates : [];
|
|
1425
|
+
const taskTemplates = [];
|
|
1426
|
+
for (const t of rawTasks) {
|
|
1427
|
+
const result = this.coerceTask(t);
|
|
1428
|
+
if (result) {
|
|
1429
|
+
taskTemplates.push(result.task);
|
|
1430
|
+
w.push(...result.warnings);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const coercedPriority = coercePriority(e.priority);
|
|
1434
|
+
if (typeof e.priority === "string" && coercedPriority !== e.priority) {
|
|
1435
|
+
w.push('Phase "' + name + '": invalid priority "' + String(e.priority) + '", defaulting to "' + coercedPriority + '"');
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
phase: {
|
|
1439
|
+
name,
|
|
1440
|
+
description: typeof e.description === "string" ? e.description : "",
|
|
1441
|
+
priority: coercedPriority,
|
|
1442
|
+
estimateHours: coerceHours(e.estimateHours, 4),
|
|
1443
|
+
parallelizable: e.parallelizable === true,
|
|
1444
|
+
taskTemplates
|
|
1445
|
+
},
|
|
1446
|
+
warnings: w
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
coerceTask(t) {
|
|
1450
|
+
if (!t || typeof t !== "object") return null;
|
|
1451
|
+
const o = t;
|
|
1452
|
+
const title = typeof o.title === "string" ? o.title.trim() : "";
|
|
1453
|
+
if (!title) return null;
|
|
1454
|
+
const w = [];
|
|
1455
|
+
let type = "feature";
|
|
1456
|
+
if (typeof o.type === "string") {
|
|
1457
|
+
if (VALID_TASK_TYPES.has(o.type)) {
|
|
1458
|
+
type = o.type;
|
|
1459
|
+
} else {
|
|
1460
|
+
w.push('Task "' + title + '": invalid type "' + o.type + '", defaulting to "feature"');
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const coercedPriority = coercePriority(o.priority);
|
|
1464
|
+
if (typeof o.priority === "string" && coercedPriority !== o.priority) {
|
|
1465
|
+
w.push('Task "' + title + '": invalid priority "' + String(o.priority) + '", defaulting to "' + coercedPriority + '"');
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
task: {
|
|
1469
|
+
title,
|
|
1470
|
+
description: typeof o.description === "string" ? o.description : "",
|
|
1471
|
+
type,
|
|
1472
|
+
priority: coercedPriority,
|
|
1473
|
+
estimateHours: coerceHours(o.estimateHours, 2),
|
|
1474
|
+
tags: Array.isArray(o.tags) ? o.tags.map(String) : []
|
|
1475
|
+
},
|
|
1476
|
+
warnings: w
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
function coercePriority(value) {
|
|
1481
|
+
return VALID_PRIORITIES.has(value) ? value : "medium";
|
|
1482
|
+
}
|
|
1483
|
+
function coerceHours(value, fallback) {
|
|
1484
|
+
const n = Number(value);
|
|
1485
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
1486
|
+
}
|
|
1487
|
+
function extractJSONArray(text) {
|
|
1488
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1489
|
+
const candidates = [];
|
|
1490
|
+
if (fence?.[1]) candidates.push(fence[1]);
|
|
1491
|
+
candidates.push(text);
|
|
1492
|
+
for (const candidate of candidates) {
|
|
1493
|
+
const balanced = firstBalancedArray(candidate);
|
|
1494
|
+
if (balanced) return balanced;
|
|
1495
|
+
}
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
function firstBalancedArray(text) {
|
|
1499
|
+
const start = text.indexOf("[");
|
|
1500
|
+
if (start === -1) return null;
|
|
1501
|
+
let depth = 0;
|
|
1502
|
+
let inString = false;
|
|
1503
|
+
let escaped = false;
|
|
1504
|
+
for (let i = start; i < text.length; i++) {
|
|
1505
|
+
const ch = text[i] ?? "";
|
|
1506
|
+
if (inString) {
|
|
1507
|
+
if (escaped) {
|
|
1508
|
+
escaped = false;
|
|
1509
|
+
} else if (ch === "\\") {
|
|
1510
|
+
escaped = true;
|
|
1511
|
+
} else if (ch === '"') {
|
|
1512
|
+
inString = false;
|
|
1513
|
+
}
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
if (ch === '"') {
|
|
1517
|
+
inString = true;
|
|
1518
|
+
} else if (ch === "[") {
|
|
1519
|
+
depth++;
|
|
1520
|
+
} else if (ch === "]") {
|
|
1521
|
+
depth--;
|
|
1522
|
+
if (depth === 0) {
|
|
1523
|
+
return text.slice(start, i + 1);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/goal/goal-assessor.ts
|
|
1531
|
+
var GoalAssessor = class {
|
|
1532
|
+
constructor(opts) {
|
|
1533
|
+
this.opts = opts;
|
|
1534
|
+
}
|
|
1535
|
+
opts;
|
|
1536
|
+
/** Analyze the goal and return a structured realism assessment. */
|
|
1537
|
+
async assess() {
|
|
1538
|
+
const prompt = this.buildPrompt();
|
|
1539
|
+
const raw = await this.opts.runOnce(prompt);
|
|
1540
|
+
return this.parse(raw);
|
|
1541
|
+
}
|
|
1542
|
+
/** Build the instruction prompt for the LLM. */
|
|
1543
|
+
buildPrompt() {
|
|
1544
|
+
return renderInstructionTemplate(
|
|
1545
|
+
readBundledInstructionText("goal/goal-assessor.md"),
|
|
1546
|
+
{ goal: this.opts.goal }
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
/** Extract and validate the JSON object from the raw LLM output. */
|
|
1550
|
+
parse(raw) {
|
|
1551
|
+
const json = extractJSONObject(raw);
|
|
1552
|
+
if (!json) {
|
|
1553
|
+
return {
|
|
1554
|
+
realistic: true,
|
|
1555
|
+
durationClaimed: null,
|
|
1556
|
+
explanation: "",
|
|
1557
|
+
recommendedDuration: null,
|
|
1558
|
+
concerns: [],
|
|
1559
|
+
raw,
|
|
1560
|
+
parseFailed: true,
|
|
1561
|
+
parseError: "No JSON object found in model output"
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
let data;
|
|
1565
|
+
try {
|
|
1566
|
+
data = JSON.parse(json);
|
|
1567
|
+
} catch {
|
|
1568
|
+
return {
|
|
1569
|
+
realistic: true,
|
|
1570
|
+
durationClaimed: null,
|
|
1571
|
+
explanation: "",
|
|
1572
|
+
recommendedDuration: null,
|
|
1573
|
+
concerns: [],
|
|
1574
|
+
raw,
|
|
1575
|
+
parseFailed: true,
|
|
1576
|
+
parseError: "Failed to parse JSON from model output"
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
realistic: data.realistic === true,
|
|
1581
|
+
durationClaimed: typeof data.durationClaimed === "string" ? data.durationClaimed : null,
|
|
1582
|
+
explanation: typeof data.explanation === "string" ? data.explanation : "",
|
|
1583
|
+
recommendedDuration: typeof data.recommendedDuration === "string" ? data.recommendedDuration : null,
|
|
1584
|
+
concerns: Array.isArray(data.concerns) ? data.concerns.filter((c) => typeof c === "string") : [],
|
|
1585
|
+
raw,
|
|
1586
|
+
parseFailed: false
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
function extractJSONObject(text) {
|
|
1591
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1592
|
+
const candidates = [];
|
|
1593
|
+
if (fence?.[1]) candidates.push(fence[1]);
|
|
1594
|
+
candidates.push(text);
|
|
1595
|
+
for (const candidate of candidates) {
|
|
1596
|
+
const balanced = firstBalancedObject(candidate);
|
|
1597
|
+
if (balanced) return balanced;
|
|
1598
|
+
}
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
function firstBalancedObject(text) {
|
|
1602
|
+
const start = text.indexOf("{");
|
|
1603
|
+
if (start === -1) return null;
|
|
1604
|
+
let depth = 0;
|
|
1605
|
+
let inString = false;
|
|
1606
|
+
let escaped = false;
|
|
1607
|
+
for (let i = start; i < text.length; i++) {
|
|
1608
|
+
const ch = text[i] ?? "";
|
|
1609
|
+
if (inString) {
|
|
1610
|
+
if (escaped) {
|
|
1611
|
+
escaped = false;
|
|
1612
|
+
} else if (ch === "\\") {
|
|
1613
|
+
escaped = true;
|
|
1614
|
+
} else if (ch === '"') {
|
|
1615
|
+
inString = false;
|
|
1616
|
+
}
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (ch === '"') {
|
|
1620
|
+
inString = true;
|
|
1621
|
+
} else if (ch === "{") {
|
|
1622
|
+
depth++;
|
|
1623
|
+
} else if (ch === "}") {
|
|
1624
|
+
depth--;
|
|
1625
|
+
if (depth === 0) {
|
|
1626
|
+
return text.slice(start, i + 1);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/goal/types.ts
|
|
1634
|
+
var PHASE_EVENT_NAMES = [
|
|
1635
|
+
"phase.statusChange",
|
|
1636
|
+
"phase.started",
|
|
1637
|
+
"phase.completed",
|
|
1638
|
+
"phase.failed",
|
|
1639
|
+
"phase.taskStarted",
|
|
1640
|
+
"phase.taskCompleted",
|
|
1641
|
+
"phase.taskFailed",
|
|
1642
|
+
"phase.taskRetrying",
|
|
1643
|
+
"phase.taskTimedOut",
|
|
1644
|
+
"phase.allTasksDone",
|
|
1645
|
+
"phase.verifying",
|
|
1646
|
+
"phase.verifyFailed",
|
|
1647
|
+
"phase.repairing",
|
|
1648
|
+
"phase.conflictResolving",
|
|
1649
|
+
"phase.conflictResolved",
|
|
1650
|
+
"phase.taskMoved",
|
|
1651
|
+
"phase.taskAssigned",
|
|
1652
|
+
"phase.taskAdded",
|
|
1653
|
+
"graph.completed",
|
|
1654
|
+
"graph.failed",
|
|
1655
|
+
"autonomous.tick",
|
|
1656
|
+
"agent.assigned",
|
|
1657
|
+
"agent.released"
|
|
1658
|
+
];
|
|
1659
|
+
|
|
1660
|
+
// src/goal/phase-store.ts
|
|
1661
|
+
import * as fsp from "node:fs/promises";
|
|
1662
|
+
import * as path from "node:path";
|
|
1663
|
+
var PHASE_STORE_VERSION = 1;
|
|
1664
|
+
var PhaseStore = class {
|
|
1665
|
+
baseDir;
|
|
1666
|
+
legacyBaseDirs;
|
|
1667
|
+
constructor(opts) {
|
|
1668
|
+
this.baseDir = opts.baseDir;
|
|
1669
|
+
this.legacyBaseDirs = (opts.legacyBaseDirs ?? []).filter(
|
|
1670
|
+
(dir) => path.resolve(dir) !== path.resolve(this.baseDir)
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
async save(graph) {
|
|
1674
|
+
const filePath = this.getFilePath(graph.id);
|
|
1675
|
+
const serialized = this.serializeGraph(graph);
|
|
1676
|
+
await withFileLock(filePath, async () => {
|
|
1677
|
+
await atomicWrite(filePath, JSON.stringify(serialized, null, 2), { mode: 384 });
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
async load(graphId) {
|
|
1681
|
+
const filePath = this.getFilePath(graphId);
|
|
1682
|
+
const current = await this.loadFromPath(filePath);
|
|
1683
|
+
if (current) return current;
|
|
1684
|
+
for (const legacyDir of this.legacyBaseDirs) {
|
|
1685
|
+
const legacyPath = path.join(legacyDir, `${graphId}.json`);
|
|
1686
|
+
const legacy = await this.loadFromPath(legacyPath);
|
|
1687
|
+
if (!legacy) continue;
|
|
1688
|
+
try {
|
|
1689
|
+
await this.save(legacy);
|
|
1690
|
+
await this.removeMigratedLegacyFile(legacyDir, legacyPath);
|
|
1691
|
+
} catch {
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
return legacy;
|
|
1695
|
+
}
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
async delete(graphId) {
|
|
1699
|
+
const paths = [
|
|
1700
|
+
this.getFilePath(graphId),
|
|
1701
|
+
...this.legacyBaseDirs.map((dir) => path.join(dir, `${graphId}.json`))
|
|
1702
|
+
];
|
|
1703
|
+
await Promise.all(paths.map((filePath) => fsp.unlink(filePath).catch(() => void 0)));
|
|
1704
|
+
}
|
|
1705
|
+
async list() {
|
|
1706
|
+
try {
|
|
1707
|
+
await this.migrateLegacyGraphs();
|
|
1708
|
+
const entries = await fsp.readdir(this.baseDir, { withFileTypes: true });
|
|
1709
|
+
const graphs = [];
|
|
1710
|
+
for (const entry of entries) {
|
|
1711
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
1712
|
+
try {
|
|
1713
|
+
const raw = await fsp.readFile(path.join(this.baseDir, entry.name), "utf8");
|
|
1714
|
+
const serialized = JSON.parse(raw);
|
|
1715
|
+
const done = serialized.completedPhaseIds.length;
|
|
1716
|
+
const total = serialized.phases.length;
|
|
1717
|
+
graphs.push({
|
|
1718
|
+
id: serialized.id,
|
|
1719
|
+
title: serialized.title,
|
|
1720
|
+
updatedAt: serialized.updatedAt,
|
|
1721
|
+
status: done === total ? "completed" : done > 0 ? "in_progress" : "pending"
|
|
1722
|
+
});
|
|
1723
|
+
} catch {
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return graphs.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1727
|
+
} catch {
|
|
1728
|
+
return [];
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
getFilePath(graphId) {
|
|
1732
|
+
return path.join(this.baseDir, `${graphId}.json`);
|
|
1733
|
+
}
|
|
1734
|
+
async loadFromPath(filePath) {
|
|
1735
|
+
try {
|
|
1736
|
+
const raw = await fsp.readFile(filePath, "utf8");
|
|
1737
|
+
const serialized = JSON.parse(raw);
|
|
1738
|
+
return this.deserializeGraph(serialized);
|
|
1739
|
+
} catch {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async migrateLegacyGraphs() {
|
|
1744
|
+
for (const legacyDir of this.legacyBaseDirs) {
|
|
1745
|
+
const entries = await fsp.readdir(legacyDir, { withFileTypes: true }).catch(() => []);
|
|
1746
|
+
for (const entry of entries) {
|
|
1747
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
1748
|
+
const legacyPath = path.join(legacyDir, entry.name);
|
|
1749
|
+
const currentPath = this.getFilePath(path.basename(entry.name, ".json"));
|
|
1750
|
+
if (await this.pathExists(currentPath)) {
|
|
1751
|
+
await this.removeMigratedLegacyFile(legacyDir, legacyPath);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
const graph = await this.loadFromPath(legacyPath);
|
|
1755
|
+
if (!graph) continue;
|
|
1756
|
+
await this.save(graph);
|
|
1757
|
+
await this.removeMigratedLegacyFile(legacyDir, legacyPath);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async pathExists(filePath) {
|
|
1762
|
+
try {
|
|
1763
|
+
await fsp.access(filePath);
|
|
1764
|
+
return true;
|
|
1765
|
+
} catch {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
async removeMigratedLegacyFile(legacyDir, legacyPath) {
|
|
1770
|
+
await fsp.unlink(legacyPath).catch(() => void 0);
|
|
1771
|
+
await fsp.rmdir(legacyDir).catch(() => void 0);
|
|
1772
|
+
}
|
|
1773
|
+
serializeGraph(graph) {
|
|
1774
|
+
return {
|
|
1775
|
+
version: PHASE_STORE_VERSION,
|
|
1776
|
+
id: graph.id,
|
|
1777
|
+
title: graph.title,
|
|
1778
|
+
description: graph.description,
|
|
1779
|
+
phases: Array.from(graph.phases.values()).map((p) => this.serializePhase(p)),
|
|
1780
|
+
rootPhaseIds: graph.rootPhaseIds,
|
|
1781
|
+
activePhaseIds: graph.activePhaseIds,
|
|
1782
|
+
completedPhaseIds: graph.completedPhaseIds,
|
|
1783
|
+
failedPhaseIds: graph.failedPhaseIds,
|
|
1784
|
+
autonomous: graph.autonomous,
|
|
1785
|
+
stopOnComplete: graph.stopOnComplete,
|
|
1786
|
+
createdAt: graph.createdAt,
|
|
1787
|
+
updatedAt: graph.updatedAt,
|
|
1788
|
+
startedAt: graph.startedAt,
|
|
1789
|
+
completedAt: graph.completedAt
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
serializePhase(phase) {
|
|
1793
|
+
return {
|
|
1794
|
+
id: phase.id,
|
|
1795
|
+
name: phase.name,
|
|
1796
|
+
description: phase.description,
|
|
1797
|
+
status: phase.status,
|
|
1798
|
+
taskGraph: this.serializeTaskGraph(phase.taskGraph),
|
|
1799
|
+
dependsOn: phase.dependsOn,
|
|
1800
|
+
nextPhases: phase.nextPhases,
|
|
1801
|
+
parallelizable: phase.parallelizable,
|
|
1802
|
+
priority: phase.priority,
|
|
1803
|
+
estimateHours: phase.estimateHours,
|
|
1804
|
+
actualDurationMs: phase.actualDurationMs,
|
|
1805
|
+
startedAt: phase.startedAt,
|
|
1806
|
+
completedAt: phase.completedAt,
|
|
1807
|
+
assignedAgents: phase.assignedAgents,
|
|
1808
|
+
metadata: phase.metadata,
|
|
1809
|
+
createdAt: phase.createdAt,
|
|
1810
|
+
updatedAt: phase.updatedAt
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
serializeTaskGraph(graph) {
|
|
1814
|
+
return {
|
|
1815
|
+
id: graph.id,
|
|
1816
|
+
specId: graph.specId,
|
|
1817
|
+
requiredRequirementIds: graph.requiredRequirementIds,
|
|
1818
|
+
title: graph.title,
|
|
1819
|
+
nodes: Array.from(graph.nodes.values()).map((n) => this.serializeTaskNode(n)),
|
|
1820
|
+
edges: graph.edges,
|
|
1821
|
+
rootNodes: graph.rootNodes,
|
|
1822
|
+
createdAt: graph.createdAt,
|
|
1823
|
+
updatedAt: graph.updatedAt
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
serializeTaskNode(node) {
|
|
1827
|
+
return { ...node };
|
|
1828
|
+
}
|
|
1829
|
+
deserializeGraph(serialized) {
|
|
1830
|
+
const fileVersion = serialized.version ?? 0;
|
|
1831
|
+
if (fileVersion > PHASE_STORE_VERSION) {
|
|
1832
|
+
throw new Error(
|
|
1833
|
+
`Cannot load phase graph: file version ${fileVersion} is newer than supported version ${PHASE_STORE_VERSION}. Upgrade WrongStack to load this file.`
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
const phases = /* @__PURE__ */ new Map();
|
|
1837
|
+
for (const sp of serialized.phases) {
|
|
1838
|
+
phases.set(sp.id, this.deserializePhase(sp));
|
|
1839
|
+
}
|
|
1840
|
+
return {
|
|
1841
|
+
id: serialized.id,
|
|
1842
|
+
title: serialized.title,
|
|
1843
|
+
description: serialized.description,
|
|
1844
|
+
phases,
|
|
1845
|
+
rootPhaseIds: serialized.rootPhaseIds,
|
|
1846
|
+
activePhaseIds: serialized.activePhaseIds,
|
|
1847
|
+
completedPhaseIds: serialized.completedPhaseIds,
|
|
1848
|
+
failedPhaseIds: serialized.failedPhaseIds,
|
|
1849
|
+
autonomous: serialized.autonomous,
|
|
1850
|
+
stopOnComplete: serialized.stopOnComplete,
|
|
1851
|
+
createdAt: serialized.createdAt,
|
|
1852
|
+
updatedAt: serialized.updatedAt,
|
|
1853
|
+
startedAt: serialized.startedAt,
|
|
1854
|
+
completedAt: serialized.completedAt
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
deserializePhase(serialized) {
|
|
1858
|
+
return {
|
|
1859
|
+
id: serialized.id,
|
|
1860
|
+
name: serialized.name,
|
|
1861
|
+
description: serialized.description,
|
|
1862
|
+
status: serialized.status,
|
|
1863
|
+
taskGraph: this.deserializeTaskGraph(serialized.taskGraph),
|
|
1864
|
+
dependsOn: serialized.dependsOn,
|
|
1865
|
+
nextPhases: serialized.nextPhases,
|
|
1866
|
+
parallelizable: serialized.parallelizable,
|
|
1867
|
+
priority: serialized.priority,
|
|
1868
|
+
estimateHours: serialized.estimateHours,
|
|
1869
|
+
actualDurationMs: serialized.actualDurationMs,
|
|
1870
|
+
startedAt: serialized.startedAt,
|
|
1871
|
+
completedAt: serialized.completedAt,
|
|
1872
|
+
assignedAgents: serialized.assignedAgents,
|
|
1873
|
+
metadata: serialized.metadata,
|
|
1874
|
+
createdAt: serialized.createdAt,
|
|
1875
|
+
updatedAt: serialized.updatedAt
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
deserializeTaskGraph(serialized) {
|
|
1879
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1880
|
+
for (const sn of serialized.nodes) {
|
|
1881
|
+
nodes.set(sn.id, sn);
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
id: serialized.id,
|
|
1885
|
+
specId: serialized.specId,
|
|
1886
|
+
requiredRequirementIds: serialized.requiredRequirementIds,
|
|
1887
|
+
title: serialized.title,
|
|
1888
|
+
nodes,
|
|
1889
|
+
edges: serialized.edges ?? [],
|
|
1890
|
+
rootNodes: serialized.rootNodes ?? [],
|
|
1891
|
+
createdAt: serialized.createdAt,
|
|
1892
|
+
updatedAt: serialized.updatedAt
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
// src/goal/checkpoint.ts
|
|
1898
|
+
import * as fsp2 from "node:fs/promises";
|
|
1899
|
+
import * as path2 from "node:path";
|
|
1900
|
+
var CheckpointManager = class {
|
|
1901
|
+
store;
|
|
1902
|
+
maxCheckpoints;
|
|
1903
|
+
maxCheckpointAgeMs;
|
|
1904
|
+
checkpoints = /* @__PURE__ */ new Map();
|
|
1905
|
+
baseDir;
|
|
1906
|
+
constructor(opts) {
|
|
1907
|
+
this.store = opts.store;
|
|
1908
|
+
this.maxCheckpoints = opts.maxCheckpoints ?? 10;
|
|
1909
|
+
this.maxCheckpointAgeMs = opts.maxCheckpointAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1910
|
+
this.baseDir = opts.baseDir ?? path2.join(opts.store.baseDir, ".checkpoints");
|
|
1911
|
+
}
|
|
1912
|
+
async initialize() {
|
|
1913
|
+
await fsp2.mkdir(this.baseDir, { recursive: true });
|
|
1914
|
+
await this.loadFromDisk();
|
|
1915
|
+
}
|
|
1916
|
+
async saveCheckpoint(graph, label) {
|
|
1917
|
+
await this.store.save(graph);
|
|
1918
|
+
const activePhase = Array.from(graph.phases.values()).find(
|
|
1919
|
+
(p) => p.status === "running" || p.status === "paused"
|
|
1920
|
+
);
|
|
1921
|
+
const checkpoint = {
|
|
1922
|
+
id: crypto.randomUUID(),
|
|
1923
|
+
graphId: graph.id,
|
|
1924
|
+
phaseId: activePhase?.id ?? graph.rootPhaseIds[0] ?? "",
|
|
1925
|
+
phaseStatus: activePhase?.status ?? "pending",
|
|
1926
|
+
taskStatuses: activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
|
|
1927
|
+
taskId: t.id,
|
|
1928
|
+
status: t.status,
|
|
1929
|
+
title: t.title
|
|
1930
|
+
})) : [],
|
|
1931
|
+
timestamp: Date.now(),
|
|
1932
|
+
label
|
|
1933
|
+
};
|
|
1934
|
+
this.checkpoints.set(checkpoint.id, checkpoint);
|
|
1935
|
+
await this.saveToDisk(checkpoint);
|
|
1936
|
+
await this.pruneCheckpoints(graph.id);
|
|
1937
|
+
return checkpoint;
|
|
1938
|
+
}
|
|
1939
|
+
async restoreCheckpoint(checkpointId) {
|
|
1940
|
+
let checkpoint = this.checkpoints.get(checkpointId);
|
|
1941
|
+
if (!checkpoint) {
|
|
1942
|
+
await this.loadFromDisk();
|
|
1943
|
+
checkpoint = this.checkpoints.get(checkpointId);
|
|
1944
|
+
}
|
|
1945
|
+
if (!checkpoint) return null;
|
|
1946
|
+
const graph = await this.store.load(checkpoint.graphId);
|
|
1947
|
+
if (!graph) return null;
|
|
1948
|
+
const phase = graph.phases.get(checkpoint.phaseId);
|
|
1949
|
+
if (phase) {
|
|
1950
|
+
phase.status = checkpoint.phaseStatus;
|
|
1951
|
+
phase.updatedAt = Date.now();
|
|
1952
|
+
for (const ts of checkpoint.taskStatuses) {
|
|
1953
|
+
const task = phase.taskGraph.nodes.get(ts.taskId);
|
|
1954
|
+
if (task) {
|
|
1955
|
+
task.status = ts.status;
|
|
1956
|
+
task.updatedAt = Date.now();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
graph.updatedAt = Date.now();
|
|
1961
|
+
return graph;
|
|
1962
|
+
}
|
|
1963
|
+
listCheckpoints(graphId) {
|
|
1964
|
+
const all = Array.from(this.checkpoints.values());
|
|
1965
|
+
const filtered = graphId ? all.filter((c) => c.graphId === graphId) : all;
|
|
1966
|
+
return filtered.sort((a, b) => b.timestamp - a.timestamp);
|
|
1967
|
+
}
|
|
1968
|
+
async deleteCheckpoint(checkpointId) {
|
|
1969
|
+
const checkpoint = this.checkpoints.get(checkpointId);
|
|
1970
|
+
if (!checkpoint) return false;
|
|
1971
|
+
this.checkpoints.delete(checkpointId);
|
|
1972
|
+
await this.deleteFromDisk(checkpointId);
|
|
1973
|
+
return true;
|
|
1974
|
+
}
|
|
1975
|
+
async saveToDisk(checkpoint) {
|
|
1976
|
+
const filePath = path2.join(this.baseDir, `${checkpoint.graphId}.json`);
|
|
1977
|
+
const serialized = {
|
|
1978
|
+
...checkpoint
|
|
1979
|
+
};
|
|
1980
|
+
await withFileLock(filePath, async () => {
|
|
1981
|
+
let existing = [];
|
|
1982
|
+
try {
|
|
1983
|
+
const raw = await fsp2.readFile(filePath, "utf8");
|
|
1984
|
+
const parsed = JSON.parse(raw);
|
|
1985
|
+
if (Array.isArray(parsed)) {
|
|
1986
|
+
existing = parsed;
|
|
1987
|
+
}
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
existing.push(serialized);
|
|
1991
|
+
await atomicWrite(filePath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
async deleteFromDisk(checkpointId) {
|
|
1995
|
+
let entries;
|
|
1996
|
+
try {
|
|
1997
|
+
entries = await fsp2.readdir(this.baseDir);
|
|
1998
|
+
} catch {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
for (const filename of entries) {
|
|
2002
|
+
if (!filename.endsWith(".json")) continue;
|
|
2003
|
+
const filePath = path2.join(this.baseDir, filename);
|
|
2004
|
+
try {
|
|
2005
|
+
await withFileLock(filePath, async () => {
|
|
2006
|
+
const raw = await fsp2.readFile(filePath, "utf8");
|
|
2007
|
+
const parsed = JSON.parse(raw);
|
|
2008
|
+
if (!Array.isArray(parsed)) return;
|
|
2009
|
+
const existing = parsed;
|
|
2010
|
+
const filtered = existing.filter((c) => c.id !== checkpointId);
|
|
2011
|
+
if (filtered.length !== existing.length) {
|
|
2012
|
+
if (filtered.length === 0) {
|
|
2013
|
+
await fsp2.unlink(filePath);
|
|
2014
|
+
} else {
|
|
2015
|
+
await atomicWrite(filePath, JSON.stringify(filtered, null, 2), { mode: 384 });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
async loadFromDisk() {
|
|
2024
|
+
let entries;
|
|
2025
|
+
try {
|
|
2026
|
+
entries = await fsp2.readdir(this.baseDir);
|
|
2027
|
+
} catch {
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
for (const filename of entries) {
|
|
2031
|
+
if (!filename.endsWith(".json")) continue;
|
|
2032
|
+
const filePath = path2.join(this.baseDir, filename);
|
|
2033
|
+
try {
|
|
2034
|
+
const raw = await fsp2.readFile(filePath, "utf8");
|
|
2035
|
+
const parsed = JSON.parse(raw);
|
|
2036
|
+
if (!Array.isArray(parsed)) continue;
|
|
2037
|
+
const checkpoints = parsed;
|
|
2038
|
+
for (const sc of checkpoints) {
|
|
2039
|
+
const checkpoint = {
|
|
2040
|
+
id: sc.id,
|
|
2041
|
+
graphId: sc.graphId,
|
|
2042
|
+
phaseId: sc.phaseId,
|
|
2043
|
+
phaseStatus: sc.phaseStatus,
|
|
2044
|
+
taskStatuses: sc.taskStatuses,
|
|
2045
|
+
timestamp: sc.timestamp,
|
|
2046
|
+
label: sc.label
|
|
2047
|
+
};
|
|
2048
|
+
this.checkpoints.set(checkpoint.id, checkpoint);
|
|
2049
|
+
}
|
|
2050
|
+
} catch {
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
async pruneCheckpoints(graphId) {
|
|
2055
|
+
const now = Date.now();
|
|
2056
|
+
const graphCheckpoints = Array.from(this.checkpoints.values()).filter((checkpoint) => checkpoint.graphId === graphId).sort((a, b) => a.timestamp - b.timestamp);
|
|
2057
|
+
for (const cp of graphCheckpoints) {
|
|
2058
|
+
if (now - cp.timestamp > this.maxCheckpointAgeMs) {
|
|
2059
|
+
this.checkpoints.delete(cp.id);
|
|
2060
|
+
await this.deleteFromDisk(cp.id);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
const remaining = Array.from(this.checkpoints.values()).filter((checkpoint) => checkpoint.graphId === graphId).sort((a, b) => a.timestamp - b.timestamp);
|
|
2064
|
+
while (remaining.length > this.maxCheckpoints) {
|
|
2065
|
+
const oldest = remaining.shift();
|
|
2066
|
+
if (oldest) {
|
|
2067
|
+
this.checkpoints.delete(oldest.id);
|
|
2068
|
+
await this.deleteFromDisk(oldest.id);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
export {
|
|
2075
|
+
PhaseGraphBuilder,
|
|
2076
|
+
PhaseOrchestrator,
|
|
2077
|
+
GoalRunner,
|
|
2078
|
+
createGoalRunnerFromTaskGraph,
|
|
2079
|
+
GoalPlanner,
|
|
2080
|
+
extractJSONArray,
|
|
2081
|
+
GoalAssessor,
|
|
2082
|
+
PHASE_EVENT_NAMES,
|
|
2083
|
+
PhaseStore,
|
|
2084
|
+
CheckpointManager
|
|
2085
|
+
};
|
|
2086
|
+
//# sourceMappingURL=chunk-XQ2XGIBF.js.map
|