@harness-engineering/orchestrator 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.mts +443 -0
- package/dist/index.d.ts +443 -0
- package/dist/index.js +1380 -0
- package/dist/index.mjs +1330 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ClaudeBackend: () => ClaudeBackend,
|
|
34
|
+
LinearGraphQLStub: () => LinearGraphQLStub,
|
|
35
|
+
MockBackend: () => MockBackend,
|
|
36
|
+
Orchestrator: () => Orchestrator,
|
|
37
|
+
PromptRenderer: () => PromptRenderer,
|
|
38
|
+
RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
|
|
39
|
+
WorkflowLoader: () => WorkflowLoader,
|
|
40
|
+
WorkspaceHooks: () => WorkspaceHooks,
|
|
41
|
+
WorkspaceManager: () => WorkspaceManager,
|
|
42
|
+
applyEvent: () => applyEvent,
|
|
43
|
+
calculateRetryDelay: () => calculateRetryDelay,
|
|
44
|
+
canDispatch: () => canDispatch,
|
|
45
|
+
createEmptyState: () => createEmptyState,
|
|
46
|
+
getAvailableSlots: () => getAvailableSlots,
|
|
47
|
+
getDefaultConfig: () => getDefaultConfig,
|
|
48
|
+
getPerStateCount: () => getPerStateCount,
|
|
49
|
+
isEligible: () => isEligible,
|
|
50
|
+
launchTUI: () => launchTUI,
|
|
51
|
+
reconcile: () => reconcile,
|
|
52
|
+
selectCandidates: () => selectCandidates,
|
|
53
|
+
sortCandidates: () => sortCandidates,
|
|
54
|
+
validateWorkflowConfig: () => validateWorkflowConfig
|
|
55
|
+
});
|
|
56
|
+
module.exports = __toCommonJS(index_exports);
|
|
57
|
+
|
|
58
|
+
// src/core/retry.ts
|
|
59
|
+
var CONTINUATION_DELAY_MS = 1e3;
|
|
60
|
+
var BASE_FAILURE_DELAY_MS = 1e4;
|
|
61
|
+
var DEFAULT_MAX_RETRY_BACKOFF_MS = 3e5;
|
|
62
|
+
function calculateRetryDelay(attempt, type, maxRetryBackoffMs = DEFAULT_MAX_RETRY_BACKOFF_MS) {
|
|
63
|
+
if (type === "continuation") {
|
|
64
|
+
return CONTINUATION_DELAY_MS;
|
|
65
|
+
}
|
|
66
|
+
const delay = BASE_FAILURE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
67
|
+
return Math.min(delay, maxRetryBackoffMs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/core/candidate-selection.ts
|
|
71
|
+
function sortCandidates(issues) {
|
|
72
|
+
return [...issues].sort((a, b) => {
|
|
73
|
+
const pa = a.priority ?? Number.MAX_SAFE_INTEGER;
|
|
74
|
+
const pb = b.priority ?? Number.MAX_SAFE_INTEGER;
|
|
75
|
+
if (pa !== pb) return pa - pb;
|
|
76
|
+
const ca = a.createdAt ?? "\uFFFF";
|
|
77
|
+
const cb = b.createdAt ?? "\uFFFF";
|
|
78
|
+
if (ca !== cb) return ca < cb ? -1 : 1;
|
|
79
|
+
return a.identifier.localeCompare(b.identifier);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function isEligible(issue, state, activeStates, terminalStates) {
|
|
83
|
+
if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const normalizedState = issue.state.toLowerCase();
|
|
87
|
+
const normalizedActive = activeStates.map((s) => s.toLowerCase());
|
|
88
|
+
const normalizedTerminal = terminalStates.map((s) => s.toLowerCase());
|
|
89
|
+
if (!normalizedActive.includes(normalizedState)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (normalizedTerminal.includes(normalizedState)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (state.claimed.has(issue.id)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (state.running.has(issue.id)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (normalizedState === "todo" && issue.blockedBy.length > 0) {
|
|
102
|
+
const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
|
|
103
|
+
if (blocker.state === null) return true;
|
|
104
|
+
return !normalizedTerminal.includes(blocker.state.toLowerCase());
|
|
105
|
+
});
|
|
106
|
+
if (hasNonTerminalBlocker) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function selectCandidates(issues, state, activeStates, terminalStates) {
|
|
113
|
+
const sorted = sortCandidates(issues);
|
|
114
|
+
return sorted.filter((issue) => isEligible(issue, state, activeStates, terminalStates));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/core/concurrency.ts
|
|
118
|
+
function getAvailableSlots(state) {
|
|
119
|
+
return Math.max(state.maxConcurrentAgents - state.running.size, 0);
|
|
120
|
+
}
|
|
121
|
+
function getPerStateCount(running) {
|
|
122
|
+
const counts = /* @__PURE__ */ new Map();
|
|
123
|
+
for (const entry of running.values()) {
|
|
124
|
+
const key = entry.issue.state.toLowerCase();
|
|
125
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
126
|
+
}
|
|
127
|
+
return counts;
|
|
128
|
+
}
|
|
129
|
+
function canDispatch(state, issueState, maxConcurrentAgentsByState) {
|
|
130
|
+
if (getAvailableSlots(state) <= 0) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const normalizedState = issueState.toLowerCase();
|
|
134
|
+
const perStateCap = maxConcurrentAgentsByState[normalizedState];
|
|
135
|
+
if (perStateCap !== void 0) {
|
|
136
|
+
const perStateCounts = getPerStateCount(state.running);
|
|
137
|
+
const currentCount = perStateCounts.get(normalizedState) ?? 0;
|
|
138
|
+
if (currentCount >= perStateCap) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/core/reconciliation.ts
|
|
146
|
+
function reconcile(state, runningStates, activeStates, terminalStates) {
|
|
147
|
+
const effects = [];
|
|
148
|
+
const normalizedActive = activeStates.map((s) => s.toLowerCase());
|
|
149
|
+
const normalizedTerminal = terminalStates.map((s) => s.toLowerCase());
|
|
150
|
+
for (const [issueId, entry] of state.running) {
|
|
151
|
+
const currentIssue = runningStates.get(issueId);
|
|
152
|
+
if (!currentIssue) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const normalizedState = currentIssue.state.toLowerCase();
|
|
156
|
+
if (normalizedTerminal.includes(normalizedState)) {
|
|
157
|
+
effects.push({ type: "stop", issueId, reason: `terminal_state: ${normalizedState}` });
|
|
158
|
+
effects.push({ type: "cleanWorkspace", issueId, identifier: entry.identifier });
|
|
159
|
+
effects.push({ type: "releaseClaim", issueId });
|
|
160
|
+
} else if (!normalizedActive.includes(normalizedState)) {
|
|
161
|
+
effects.push({ type: "stop", issueId, reason: `non_active_state: ${normalizedState}` });
|
|
162
|
+
effects.push({ type: "releaseClaim", issueId });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return effects;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/core/state-machine.ts
|
|
169
|
+
function cloneState(state) {
|
|
170
|
+
return {
|
|
171
|
+
...state,
|
|
172
|
+
running: new Map(state.running),
|
|
173
|
+
claimed: new Set(state.claimed),
|
|
174
|
+
retryAttempts: new Map(state.retryAttempts),
|
|
175
|
+
completed: new Set(state.completed),
|
|
176
|
+
tokenTotals: { ...state.tokenTotals },
|
|
177
|
+
rateLimits: { ...state.rateLimits }
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function handleTick(state, candidates, runningStates, config, nowMs) {
|
|
181
|
+
const next = cloneState(state);
|
|
182
|
+
const effects = [];
|
|
183
|
+
const reconcileEffects = reconcile(
|
|
184
|
+
next,
|
|
185
|
+
runningStates,
|
|
186
|
+
config.tracker.activeStates,
|
|
187
|
+
config.tracker.terminalStates
|
|
188
|
+
);
|
|
189
|
+
effects.push(...reconcileEffects);
|
|
190
|
+
for (const effect of reconcileEffects) {
|
|
191
|
+
if (effect.type === "stop") {
|
|
192
|
+
next.running.delete(effect.issueId);
|
|
193
|
+
}
|
|
194
|
+
if (effect.type === "releaseClaim") {
|
|
195
|
+
next.claimed.delete(effect.issueId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const eligible = selectCandidates(
|
|
199
|
+
candidates,
|
|
200
|
+
next,
|
|
201
|
+
config.tracker.activeStates,
|
|
202
|
+
config.tracker.terminalStates
|
|
203
|
+
);
|
|
204
|
+
for (const issue of eligible) {
|
|
205
|
+
if (!canDispatch(next, issue.state, config.agent.maxConcurrentAgentsByState)) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
next.claimed.add(issue.id);
|
|
209
|
+
next.running.set(issue.id, {
|
|
210
|
+
issueId: issue.id,
|
|
211
|
+
identifier: issue.identifier,
|
|
212
|
+
issue,
|
|
213
|
+
attempt: null,
|
|
214
|
+
workspacePath: "",
|
|
215
|
+
startedAt: new Date(nowMs).toISOString(),
|
|
216
|
+
phase: "PreparingWorkspace",
|
|
217
|
+
session: null
|
|
218
|
+
});
|
|
219
|
+
effects.push({
|
|
220
|
+
type: "dispatch",
|
|
221
|
+
issue,
|
|
222
|
+
attempt: null
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return { nextState: next, effects };
|
|
226
|
+
}
|
|
227
|
+
function handleWorkerExit(state, issueId, reason, error, attempt, config) {
|
|
228
|
+
const next = cloneState(state);
|
|
229
|
+
const effects = [];
|
|
230
|
+
const entry = next.running.get(issueId);
|
|
231
|
+
next.running.delete(issueId);
|
|
232
|
+
if (reason === "normal") {
|
|
233
|
+
next.completed.add(issueId);
|
|
234
|
+
const delayMs = calculateRetryDelay(1, "continuation");
|
|
235
|
+
effects.push({
|
|
236
|
+
type: "scheduleRetry",
|
|
237
|
+
issueId,
|
|
238
|
+
identifier: entry?.identifier ?? issueId,
|
|
239
|
+
attempt: 1,
|
|
240
|
+
delayMs,
|
|
241
|
+
error: null
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
const nextAttempt = (attempt ?? 0) + 1;
|
|
245
|
+
const delayMs = calculateRetryDelay(nextAttempt, "failure", config.agent.maxRetryBackoffMs);
|
|
246
|
+
effects.push({
|
|
247
|
+
type: "scheduleRetry",
|
|
248
|
+
issueId,
|
|
249
|
+
identifier: entry?.identifier ?? issueId,
|
|
250
|
+
attempt: nextAttempt,
|
|
251
|
+
delayMs,
|
|
252
|
+
error: error ?? "unknown error"
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return { nextState: next, effects };
|
|
256
|
+
}
|
|
257
|
+
function handleAgentUpdate(state, issueId, event) {
|
|
258
|
+
const next = cloneState(state);
|
|
259
|
+
const effects = [];
|
|
260
|
+
const entry = next.running.get(issueId);
|
|
261
|
+
if (entry && entry.session) {
|
|
262
|
+
const updatedSession = { ...entry.session };
|
|
263
|
+
updatedSession.lastEvent = event.type;
|
|
264
|
+
updatedSession.lastTimestamp = event.timestamp;
|
|
265
|
+
if (event.usage) {
|
|
266
|
+
updatedSession.inputTokens += event.usage.inputTokens;
|
|
267
|
+
updatedSession.outputTokens += event.usage.outputTokens;
|
|
268
|
+
updatedSession.totalTokens += event.usage.totalTokens;
|
|
269
|
+
effects.push({
|
|
270
|
+
type: "updateTokens",
|
|
271
|
+
issueId,
|
|
272
|
+
usage: event.usage
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (event.sessionId) {
|
|
276
|
+
updatedSession.sessionId = event.sessionId;
|
|
277
|
+
}
|
|
278
|
+
next.running.set(issueId, { ...entry, session: updatedSession });
|
|
279
|
+
}
|
|
280
|
+
return { nextState: next, effects };
|
|
281
|
+
}
|
|
282
|
+
function handleRetryFired(state, issueId, candidates, config, nowMs) {
|
|
283
|
+
const next = cloneState(state);
|
|
284
|
+
const effects = [];
|
|
285
|
+
const retryEntry = next.retryAttempts.get(issueId);
|
|
286
|
+
next.retryAttempts.delete(issueId);
|
|
287
|
+
if (!retryEntry) {
|
|
288
|
+
return { nextState: next, effects };
|
|
289
|
+
}
|
|
290
|
+
const issue = candidates.find((c) => c.id === issueId);
|
|
291
|
+
if (!issue) {
|
|
292
|
+
next.claimed.delete(issueId);
|
|
293
|
+
effects.push({ type: "releaseClaim", issueId });
|
|
294
|
+
return { nextState: next, effects };
|
|
295
|
+
}
|
|
296
|
+
const normalizedState = issue.state.toLowerCase();
|
|
297
|
+
const normalizedActive = config.tracker.activeStates.map((s) => s.toLowerCase());
|
|
298
|
+
if (!normalizedActive.includes(normalizedState)) {
|
|
299
|
+
next.claimed.delete(issueId);
|
|
300
|
+
effects.push({ type: "releaseClaim", issueId });
|
|
301
|
+
return { nextState: next, effects };
|
|
302
|
+
}
|
|
303
|
+
if (!canDispatch(next, issue.state, config.agent.maxConcurrentAgentsByState)) {
|
|
304
|
+
const nextAttempt = retryEntry.attempt + 1;
|
|
305
|
+
const delayMs = calculateRetryDelay(nextAttempt, "failure", config.agent.maxRetryBackoffMs);
|
|
306
|
+
next.retryAttempts.set(issueId, {
|
|
307
|
+
issueId,
|
|
308
|
+
identifier: retryEntry.identifier,
|
|
309
|
+
attempt: nextAttempt,
|
|
310
|
+
dueAtMs: nowMs + delayMs,
|
|
311
|
+
error: "no available orchestrator slots"
|
|
312
|
+
});
|
|
313
|
+
effects.push({
|
|
314
|
+
type: "scheduleRetry",
|
|
315
|
+
issueId,
|
|
316
|
+
identifier: retryEntry.identifier,
|
|
317
|
+
attempt: nextAttempt,
|
|
318
|
+
delayMs,
|
|
319
|
+
error: "no available orchestrator slots"
|
|
320
|
+
});
|
|
321
|
+
return { nextState: next, effects };
|
|
322
|
+
}
|
|
323
|
+
effects.push({
|
|
324
|
+
type: "dispatch",
|
|
325
|
+
issue,
|
|
326
|
+
attempt: retryEntry.attempt
|
|
327
|
+
});
|
|
328
|
+
return { nextState: next, effects };
|
|
329
|
+
}
|
|
330
|
+
function handleStallDetected(state, issueId, config) {
|
|
331
|
+
const next = cloneState(state);
|
|
332
|
+
const effects = [];
|
|
333
|
+
const entry = next.running.get(issueId);
|
|
334
|
+
next.running.delete(issueId);
|
|
335
|
+
const attempt = (entry?.attempt ?? 0) + 1;
|
|
336
|
+
const delayMs = calculateRetryDelay(attempt, "failure", config.agent.maxRetryBackoffMs);
|
|
337
|
+
effects.push({
|
|
338
|
+
type: "stop",
|
|
339
|
+
issueId,
|
|
340
|
+
reason: "stall_detected"
|
|
341
|
+
});
|
|
342
|
+
effects.push({
|
|
343
|
+
type: "scheduleRetry",
|
|
344
|
+
issueId,
|
|
345
|
+
identifier: entry?.identifier ?? issueId,
|
|
346
|
+
attempt,
|
|
347
|
+
delayMs,
|
|
348
|
+
error: "stall detected"
|
|
349
|
+
});
|
|
350
|
+
return { nextState: next, effects };
|
|
351
|
+
}
|
|
352
|
+
function applyEvent(state, event, config) {
|
|
353
|
+
switch (event.type) {
|
|
354
|
+
case "tick":
|
|
355
|
+
return handleTick(state, event.candidates, event.runningStates, config, event.nowMs);
|
|
356
|
+
case "worker_exit":
|
|
357
|
+
return handleWorkerExit(
|
|
358
|
+
state,
|
|
359
|
+
event.issueId,
|
|
360
|
+
event.reason,
|
|
361
|
+
event.error,
|
|
362
|
+
event.attempt,
|
|
363
|
+
config
|
|
364
|
+
);
|
|
365
|
+
case "agent_update":
|
|
366
|
+
return handleAgentUpdate(state, event.issueId, event.event);
|
|
367
|
+
case "retry_fired":
|
|
368
|
+
return handleRetryFired(state, event.issueId, event.candidates, config, event.nowMs);
|
|
369
|
+
case "stall_detected":
|
|
370
|
+
return handleStallDetected(state, event.issueId, config);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/core/state-helpers.ts
|
|
375
|
+
function createEmptyState(config) {
|
|
376
|
+
return {
|
|
377
|
+
pollIntervalMs: config.polling.intervalMs,
|
|
378
|
+
maxConcurrentAgents: config.agent.maxConcurrentAgents,
|
|
379
|
+
running: /* @__PURE__ */ new Map(),
|
|
380
|
+
claimed: /* @__PURE__ */ new Set(),
|
|
381
|
+
retryAttempts: /* @__PURE__ */ new Map(),
|
|
382
|
+
completed: /* @__PURE__ */ new Set(),
|
|
383
|
+
tokenTotals: {
|
|
384
|
+
inputTokens: 0,
|
|
385
|
+
outputTokens: 0,
|
|
386
|
+
totalTokens: 0,
|
|
387
|
+
secondsRunning: 0
|
|
388
|
+
},
|
|
389
|
+
rateLimits: {
|
|
390
|
+
requestsRemaining: null,
|
|
391
|
+
requestsLimit: null,
|
|
392
|
+
tokensRemaining: null,
|
|
393
|
+
tokensLimit: null
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/workflow/loader.ts
|
|
399
|
+
var fs = __toESM(require("fs/promises"));
|
|
400
|
+
var import_yaml = require("yaml");
|
|
401
|
+
var import_types2 = require("@harness-engineering/types");
|
|
402
|
+
|
|
403
|
+
// src/workflow/config.ts
|
|
404
|
+
var import_types = require("@harness-engineering/types");
|
|
405
|
+
function validateWorkflowConfig(config) {
|
|
406
|
+
if (!config || typeof config !== "object")
|
|
407
|
+
return (0, import_types.Err)(new Error("Config is missing or not an object"));
|
|
408
|
+
const c = config;
|
|
409
|
+
if (!c.tracker) return (0, import_types.Err)(new Error("Config is missing tracker section"));
|
|
410
|
+
if (!c.polling) return (0, import_types.Err)(new Error("Config is missing polling section"));
|
|
411
|
+
if (!c.workspace) return (0, import_types.Err)(new Error("Config is missing workspace section"));
|
|
412
|
+
if (!c.hooks) return (0, import_types.Err)(new Error("Config is missing hooks section"));
|
|
413
|
+
if (!c.agent) return (0, import_types.Err)(new Error("Config is missing agent section"));
|
|
414
|
+
if (!c.server) return (0, import_types.Err)(new Error("Config is missing server section"));
|
|
415
|
+
return (0, import_types.Ok)(config);
|
|
416
|
+
}
|
|
417
|
+
function getDefaultConfig() {
|
|
418
|
+
return {
|
|
419
|
+
tracker: {
|
|
420
|
+
kind: "roadmap",
|
|
421
|
+
filePath: "docs/roadmap.md",
|
|
422
|
+
activeStates: ["planned", "in-progress"],
|
|
423
|
+
terminalStates: ["done"]
|
|
424
|
+
},
|
|
425
|
+
polling: {
|
|
426
|
+
intervalMs: 3e4
|
|
427
|
+
},
|
|
428
|
+
workspace: {
|
|
429
|
+
root: ".harness/workspaces"
|
|
430
|
+
},
|
|
431
|
+
hooks: {
|
|
432
|
+
afterCreate: null,
|
|
433
|
+
beforeRun: null,
|
|
434
|
+
afterRun: null,
|
|
435
|
+
beforeRemove: null,
|
|
436
|
+
timeoutMs: 6e4
|
|
437
|
+
},
|
|
438
|
+
agent: {
|
|
439
|
+
backend: "mock",
|
|
440
|
+
maxConcurrentAgents: 1,
|
|
441
|
+
maxTurns: 10,
|
|
442
|
+
maxRetryBackoffMs: 5e3,
|
|
443
|
+
maxConcurrentAgentsByState: {},
|
|
444
|
+
turnTimeoutMs: 3e5,
|
|
445
|
+
readTimeoutMs: 3e4,
|
|
446
|
+
stallTimeoutMs: 6e4
|
|
447
|
+
},
|
|
448
|
+
server: {
|
|
449
|
+
port: 8080
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/workflow/loader.ts
|
|
455
|
+
var WorkflowLoader = class {
|
|
456
|
+
async loadWorkflow(filePath) {
|
|
457
|
+
try {
|
|
458
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
459
|
+
const parts = content.split("---");
|
|
460
|
+
if (parts.length < 3) {
|
|
461
|
+
return (0, import_types2.Err)(
|
|
462
|
+
new Error(
|
|
463
|
+
`Invalid WORKFLOW.md format at ${filePath}. Expected frontmatter surrounded by '---'.`
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const yamlContent = parts[1].trim();
|
|
468
|
+
const promptTemplate = parts.slice(2).join("---").trim();
|
|
469
|
+
const configData = (0, import_yaml.parse)(yamlContent);
|
|
470
|
+
const configResult = validateWorkflowConfig(configData);
|
|
471
|
+
if (!configResult.ok) {
|
|
472
|
+
return (0, import_types2.Err)(configResult.error);
|
|
473
|
+
}
|
|
474
|
+
return (0, import_types2.Ok)({
|
|
475
|
+
config: configResult.value,
|
|
476
|
+
promptTemplate
|
|
477
|
+
});
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return (0, import_types2.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/tracker/adapters/roadmap.ts
|
|
485
|
+
var fs2 = __toESM(require("fs/promises"));
|
|
486
|
+
var import_node_crypto = require("crypto");
|
|
487
|
+
var import_core = require("@harness-engineering/core");
|
|
488
|
+
var import_types3 = require("@harness-engineering/types");
|
|
489
|
+
var RoadmapTrackerAdapter = class {
|
|
490
|
+
config;
|
|
491
|
+
/**
|
|
492
|
+
* Creates a new RoadmapTrackerAdapter.
|
|
493
|
+
*
|
|
494
|
+
* @param config - The tracker configuration including the file path
|
|
495
|
+
*/
|
|
496
|
+
constructor(config) {
|
|
497
|
+
this.config = config;
|
|
498
|
+
if (!config.filePath) {
|
|
499
|
+
throw new Error("RoadmapTrackerAdapter requires a filePath in TrackerConfig");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Fetches all issues that are in an "active" state according to the config.
|
|
504
|
+
*/
|
|
505
|
+
async fetchCandidateIssues() {
|
|
506
|
+
return this.fetchIssuesByStates(this.config.activeStates);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Fetches issues that match any of the given state names.
|
|
510
|
+
*
|
|
511
|
+
* @param stateNames - List of statuses to filter by
|
|
512
|
+
*/
|
|
513
|
+
async fetchIssuesByStates(stateNames) {
|
|
514
|
+
try {
|
|
515
|
+
if (!this.config.filePath) return (0, import_types3.Err)(new Error("Missing filePath"));
|
|
516
|
+
const content = await fs2.readFile(this.config.filePath, "utf-8");
|
|
517
|
+
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
518
|
+
if (!roadmapResult.ok) return roadmapResult;
|
|
519
|
+
const issues = [];
|
|
520
|
+
for (const milestone of roadmapResult.value.milestones) {
|
|
521
|
+
for (const feature of milestone.features) {
|
|
522
|
+
if (stateNames.includes(feature.status)) {
|
|
523
|
+
issues.push(this.mapFeatureToIssue(feature));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return (0, import_types3.Ok)(issues);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return (0, import_types3.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Fetches full issue details for a list of identifiers.
|
|
534
|
+
*
|
|
535
|
+
* @param issueIds - List of issue IDs to fetch
|
|
536
|
+
*/
|
|
537
|
+
async fetchIssueStatesByIds(issueIds) {
|
|
538
|
+
try {
|
|
539
|
+
if (!this.config.filePath) return (0, import_types3.Err)(new Error("Missing filePath"));
|
|
540
|
+
const content = await fs2.readFile(this.config.filePath, "utf-8");
|
|
541
|
+
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
542
|
+
if (!roadmapResult.ok) return roadmapResult;
|
|
543
|
+
const issueMap = /* @__PURE__ */ new Map();
|
|
544
|
+
for (const milestone of roadmapResult.value.milestones) {
|
|
545
|
+
for (const feature of milestone.features) {
|
|
546
|
+
const issue = this.mapFeatureToIssue(feature);
|
|
547
|
+
if (issueIds.includes(issue.id)) {
|
|
548
|
+
issueMap.set(issue.id, issue);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return (0, import_types3.Ok)(issueMap);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return (0, import_types3.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Maps a raw RoadmapFeature from the parser to the unified Issue model.
|
|
559
|
+
*/
|
|
560
|
+
mapFeatureToIssue(feature) {
|
|
561
|
+
const id = this.generateId(feature.name);
|
|
562
|
+
return {
|
|
563
|
+
id,
|
|
564
|
+
identifier: id,
|
|
565
|
+
title: feature.name,
|
|
566
|
+
description: feature.summary,
|
|
567
|
+
priority: null,
|
|
568
|
+
state: feature.status,
|
|
569
|
+
branchName: null,
|
|
570
|
+
url: null,
|
|
571
|
+
labels: [],
|
|
572
|
+
blockedBy: feature.blockedBy.map((b) => ({
|
|
573
|
+
id: this.generateId(b),
|
|
574
|
+
identifier: b,
|
|
575
|
+
state: null
|
|
576
|
+
})),
|
|
577
|
+
createdAt: null,
|
|
578
|
+
updatedAt: null
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Generates a deterministic, URL-safe identifier for a feature name.
|
|
583
|
+
*/
|
|
584
|
+
generateId(name) {
|
|
585
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(name).digest("hex").slice(0, 8);
|
|
586
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 20);
|
|
587
|
+
return `${sanitized}-${hash}`;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/tracker/extensions/linear.ts
|
|
592
|
+
var import_types4 = require("@harness-engineering/types");
|
|
593
|
+
var LinearGraphQLStub = class {
|
|
594
|
+
async query(query, _variables) {
|
|
595
|
+
console.log("Linear GraphQL query (stub):", query);
|
|
596
|
+
return (0, import_types4.Ok)({ data: {} });
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/workspace/manager.ts
|
|
601
|
+
var fs3 = __toESM(require("fs/promises"));
|
|
602
|
+
var path = __toESM(require("path"));
|
|
603
|
+
var import_types5 = require("@harness-engineering/types");
|
|
604
|
+
var WorkspaceManager = class {
|
|
605
|
+
config;
|
|
606
|
+
constructor(config) {
|
|
607
|
+
this.config = config;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Sanitizes an issue identifier to be safe for use as a directory name.
|
|
611
|
+
*/
|
|
612
|
+
sanitizeIdentifier(identifier) {
|
|
613
|
+
return identifier.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Resolves the full path for an issue's workspace.
|
|
617
|
+
*/
|
|
618
|
+
resolvePath(identifier) {
|
|
619
|
+
const sanitized = this.sanitizeIdentifier(identifier);
|
|
620
|
+
return path.join(this.config.root, sanitized);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Ensures the workspace directory exists.
|
|
624
|
+
*/
|
|
625
|
+
async ensureWorkspace(identifier) {
|
|
626
|
+
try {
|
|
627
|
+
const workspacePath = this.resolvePath(identifier);
|
|
628
|
+
await fs3.mkdir(workspacePath, { recursive: true });
|
|
629
|
+
return (0, import_types5.Ok)(workspacePath);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
return (0, import_types5.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Checks if a workspace exists.
|
|
636
|
+
*/
|
|
637
|
+
async exists(identifier) {
|
|
638
|
+
try {
|
|
639
|
+
const workspacePath = this.resolvePath(identifier);
|
|
640
|
+
await fs3.access(workspacePath);
|
|
641
|
+
return true;
|
|
642
|
+
} catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Removes a workspace directory.
|
|
648
|
+
*/
|
|
649
|
+
async removeWorkspace(identifier) {
|
|
650
|
+
try {
|
|
651
|
+
const workspacePath = this.resolvePath(identifier);
|
|
652
|
+
await fs3.rm(workspacePath, { recursive: true, force: true });
|
|
653
|
+
return (0, import_types5.Ok)(void 0);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
return (0, import_types5.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// src/workspace/hooks.ts
|
|
661
|
+
var import_node_child_process = require("child_process");
|
|
662
|
+
var import_types6 = require("@harness-engineering/types");
|
|
663
|
+
var WorkspaceHooks = class {
|
|
664
|
+
config;
|
|
665
|
+
constructor(config) {
|
|
666
|
+
this.config = config;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Executes a shell hook in the specified workspace directory.
|
|
670
|
+
*/
|
|
671
|
+
async executeHook(hookName, cwd) {
|
|
672
|
+
const command = this.config[hookName];
|
|
673
|
+
if (!command) {
|
|
674
|
+
return (0, import_types6.Ok)(void 0);
|
|
675
|
+
}
|
|
676
|
+
return new Promise((resolve) => {
|
|
677
|
+
const child = (0, import_node_child_process.spawn)(command, {
|
|
678
|
+
shell: true,
|
|
679
|
+
cwd,
|
|
680
|
+
env: process.env
|
|
681
|
+
});
|
|
682
|
+
const timeout = setTimeout(() => {
|
|
683
|
+
child.kill();
|
|
684
|
+
resolve((0, import_types6.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
685
|
+
}, this.config.timeoutMs);
|
|
686
|
+
child.on("exit", (code) => {
|
|
687
|
+
clearTimeout(timeout);
|
|
688
|
+
if (code === 0) {
|
|
689
|
+
resolve((0, import_types6.Ok)(void 0));
|
|
690
|
+
} else {
|
|
691
|
+
resolve((0, import_types6.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
child.on("error", (error) => {
|
|
695
|
+
clearTimeout(timeout);
|
|
696
|
+
resolve((0, import_types6.Err)(error));
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async afterCreate(cwd) {
|
|
701
|
+
return this.executeHook("afterCreate", cwd);
|
|
702
|
+
}
|
|
703
|
+
async beforeRun(cwd) {
|
|
704
|
+
return this.executeHook("beforeRun", cwd);
|
|
705
|
+
}
|
|
706
|
+
async afterRun(cwd) {
|
|
707
|
+
return this.executeHook("afterRun", cwd);
|
|
708
|
+
}
|
|
709
|
+
async beforeRemove(cwd) {
|
|
710
|
+
return this.executeHook("beforeRemove", cwd);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/agent/backends/mock.ts
|
|
715
|
+
var import_types7 = require("@harness-engineering/types");
|
|
716
|
+
var MockBackend = class {
|
|
717
|
+
name = "mock";
|
|
718
|
+
async startSession(params) {
|
|
719
|
+
const session = {
|
|
720
|
+
sessionId: `mock-session-${Date.now()}`,
|
|
721
|
+
workspacePath: params.workspacePath,
|
|
722
|
+
backendName: this.name,
|
|
723
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
724
|
+
};
|
|
725
|
+
return (0, import_types7.Ok)(session);
|
|
726
|
+
}
|
|
727
|
+
async *runTurn(session, _params) {
|
|
728
|
+
yield {
|
|
729
|
+
type: "status",
|
|
730
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
731
|
+
content: "Thinking...",
|
|
732
|
+
sessionId: session.sessionId
|
|
733
|
+
};
|
|
734
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
735
|
+
yield {
|
|
736
|
+
type: "thought",
|
|
737
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
738
|
+
content: "I will list the directory.",
|
|
739
|
+
sessionId: session.sessionId
|
|
740
|
+
};
|
|
741
|
+
return {
|
|
742
|
+
success: true,
|
|
743
|
+
sessionId: session.sessionId,
|
|
744
|
+
usage: {
|
|
745
|
+
inputTokens: 100,
|
|
746
|
+
outputTokens: 50,
|
|
747
|
+
totalTokens: 150
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
async stopSession(_session) {
|
|
752
|
+
return (0, import_types7.Ok)(void 0);
|
|
753
|
+
}
|
|
754
|
+
async healthCheck() {
|
|
755
|
+
return (0, import_types7.Ok)(void 0);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/agent/backends/claude.ts
|
|
760
|
+
var import_node_child_process2 = require("child_process");
|
|
761
|
+
var readline = __toESM(require("readline"));
|
|
762
|
+
var import_types8 = require("@harness-engineering/types");
|
|
763
|
+
var ClaudeBackend = class {
|
|
764
|
+
name = "claude";
|
|
765
|
+
command;
|
|
766
|
+
constructor(command = "claude") {
|
|
767
|
+
this.command = command;
|
|
768
|
+
}
|
|
769
|
+
async startSession(params) {
|
|
770
|
+
const session = {
|
|
771
|
+
sessionId: `claude-session-${Date.now()}`,
|
|
772
|
+
workspacePath: params.workspacePath,
|
|
773
|
+
backendName: this.name,
|
|
774
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
775
|
+
};
|
|
776
|
+
return (0, import_types8.Ok)(session);
|
|
777
|
+
}
|
|
778
|
+
async *runTurn(session, params) {
|
|
779
|
+
const args = ["-p", params.prompt, "--output-format", "json"];
|
|
780
|
+
if (params.isContinuation) {
|
|
781
|
+
args.push("--resume", session.sessionId);
|
|
782
|
+
}
|
|
783
|
+
const child = (0, import_node_child_process2.spawn)(this.command, args, {
|
|
784
|
+
cwd: session.workspacePath,
|
|
785
|
+
env: process.env
|
|
786
|
+
});
|
|
787
|
+
const rl = readline.createInterface({
|
|
788
|
+
input: child.stdout,
|
|
789
|
+
terminal: false
|
|
790
|
+
});
|
|
791
|
+
let lastResult = null;
|
|
792
|
+
try {
|
|
793
|
+
for await (const line of rl) {
|
|
794
|
+
try {
|
|
795
|
+
const event = JSON.parse(line);
|
|
796
|
+
yield event;
|
|
797
|
+
if (event.type === "result") {
|
|
798
|
+
lastResult = event.content;
|
|
799
|
+
}
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} finally {
|
|
804
|
+
if (child.exitCode === null) {
|
|
805
|
+
child.kill("SIGTERM");
|
|
806
|
+
}
|
|
807
|
+
rl.close();
|
|
808
|
+
}
|
|
809
|
+
return lastResult || {
|
|
810
|
+
success: true,
|
|
811
|
+
sessionId: session.sessionId,
|
|
812
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
async stopSession(_session) {
|
|
816
|
+
return (0, import_types8.Ok)(void 0);
|
|
817
|
+
}
|
|
818
|
+
async healthCheck() {
|
|
819
|
+
return new Promise((resolve) => {
|
|
820
|
+
const child = (0, import_node_child_process2.spawn)(this.command, ["--version"]);
|
|
821
|
+
child.on("exit", (code) => {
|
|
822
|
+
if (code === 0) {
|
|
823
|
+
resolve((0, import_types8.Ok)(void 0));
|
|
824
|
+
} else {
|
|
825
|
+
resolve(
|
|
826
|
+
(0, import_types8.Err)({
|
|
827
|
+
category: "agent_not_found",
|
|
828
|
+
message: `Claude command '${this.command}' not found or failed`
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
child.on("error", () => {
|
|
834
|
+
resolve(
|
|
835
|
+
(0, import_types8.Err)({
|
|
836
|
+
category: "agent_not_found",
|
|
837
|
+
message: `Claude command '${this.command}' not found`
|
|
838
|
+
})
|
|
839
|
+
);
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// src/prompt/renderer.ts
|
|
846
|
+
var import_liquidjs = require("liquidjs");
|
|
847
|
+
var PromptRenderer = class {
|
|
848
|
+
engine;
|
|
849
|
+
constructor() {
|
|
850
|
+
this.engine = new import_liquidjs.Liquid({
|
|
851
|
+
strictVariables: true,
|
|
852
|
+
strictFilters: true
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
async render(template, context) {
|
|
856
|
+
try {
|
|
857
|
+
return await this.engine.render(this.engine.parse(template), context);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (error instanceof Error) {
|
|
860
|
+
throw new Error(`Template rendering failed: ${error.message}`, { cause: error });
|
|
861
|
+
}
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// src/orchestrator.ts
|
|
868
|
+
var import_node_events = require("events");
|
|
869
|
+
|
|
870
|
+
// src/agent/runner.ts
|
|
871
|
+
var AgentRunner = class {
|
|
872
|
+
backend;
|
|
873
|
+
options;
|
|
874
|
+
constructor(backend, options) {
|
|
875
|
+
this.backend = backend;
|
|
876
|
+
this.options = options;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Run a multi-turn agent session for an issue.
|
|
880
|
+
*/
|
|
881
|
+
async *runSession(_issue, workspacePath, prompt) {
|
|
882
|
+
const startResult = await this.backend.startSession({
|
|
883
|
+
workspacePath,
|
|
884
|
+
permissionMode: "full"
|
|
885
|
+
// Default for now
|
|
886
|
+
});
|
|
887
|
+
if (!startResult.ok) {
|
|
888
|
+
throw new Error(`Failed to start agent session: ${startResult.error.message}`);
|
|
889
|
+
}
|
|
890
|
+
const session = startResult.value;
|
|
891
|
+
let currentTurn = 0;
|
|
892
|
+
let lastResult = {
|
|
893
|
+
success: false,
|
|
894
|
+
sessionId: session.sessionId,
|
|
895
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
896
|
+
};
|
|
897
|
+
try {
|
|
898
|
+
while (currentTurn < this.options.maxTurns) {
|
|
899
|
+
currentTurn++;
|
|
900
|
+
const turnParams = {
|
|
901
|
+
sessionId: session.sessionId,
|
|
902
|
+
prompt: currentTurn === 1 ? prompt : "Continue your work.",
|
|
903
|
+
isContinuation: currentTurn > 1
|
|
904
|
+
};
|
|
905
|
+
const turnGen = this.backend.runTurn(session, turnParams);
|
|
906
|
+
let next = await turnGen.next();
|
|
907
|
+
while (!next.done) {
|
|
908
|
+
yield next.value;
|
|
909
|
+
next = await turnGen.next();
|
|
910
|
+
}
|
|
911
|
+
lastResult = next.value;
|
|
912
|
+
if (lastResult.success) {
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
} finally {
|
|
917
|
+
await this.backend.stopSession(session);
|
|
918
|
+
}
|
|
919
|
+
return lastResult;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/server/http.ts
|
|
924
|
+
var http = __toESM(require("http"));
|
|
925
|
+
var OrchestratorServer = class {
|
|
926
|
+
server;
|
|
927
|
+
orchestrator;
|
|
928
|
+
port;
|
|
929
|
+
constructor(orchestrator, port) {
|
|
930
|
+
this.orchestrator = orchestrator;
|
|
931
|
+
this.port = port;
|
|
932
|
+
this.server = http.createServer(this.handleRequest.bind(this));
|
|
933
|
+
}
|
|
934
|
+
handleRequest(req, res) {
|
|
935
|
+
const { method, url } = req;
|
|
936
|
+
if (method === "GET" && url === "/api/v1/state") {
|
|
937
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
938
|
+
res.end(JSON.stringify(this.orchestrator.getSnapshot()));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
942
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
943
|
+
}
|
|
944
|
+
async start() {
|
|
945
|
+
return new Promise((resolve) => {
|
|
946
|
+
this.server.listen(this.port, "127.0.0.1", () => {
|
|
947
|
+
console.log(`Orchestrator API listening on localhost:${this.port}`);
|
|
948
|
+
resolve();
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
stop() {
|
|
953
|
+
this.server.close();
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/logging/logger.ts
|
|
958
|
+
var StructuredLogger = class {
|
|
959
|
+
info(message, context) {
|
|
960
|
+
this.log("info", message, context);
|
|
961
|
+
}
|
|
962
|
+
warn(message, context) {
|
|
963
|
+
this.log("warn", message, context);
|
|
964
|
+
}
|
|
965
|
+
error(message, context) {
|
|
966
|
+
this.log("error", message, context);
|
|
967
|
+
}
|
|
968
|
+
log(level, message, context) {
|
|
969
|
+
const entry = {
|
|
970
|
+
level,
|
|
971
|
+
message,
|
|
972
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
973
|
+
...context ? { context } : {}
|
|
974
|
+
};
|
|
975
|
+
console.log(this.formatLog(entry));
|
|
976
|
+
}
|
|
977
|
+
formatLog(entry) {
|
|
978
|
+
const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
979
|
+
const contextStr = entry.context ? ` ${this.safeStringify(entry.context)}` : "";
|
|
980
|
+
return `${timestamp} ${entry.level.toUpperCase()}: ${entry.message}${contextStr}`;
|
|
981
|
+
}
|
|
982
|
+
safeStringify(obj) {
|
|
983
|
+
try {
|
|
984
|
+
return JSON.stringify(obj);
|
|
985
|
+
} catch {
|
|
986
|
+
return "[Circular or Non-serializable]";
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// src/orchestrator.ts
|
|
992
|
+
var Orchestrator = class extends import_node_events.EventEmitter {
|
|
993
|
+
state;
|
|
994
|
+
config;
|
|
995
|
+
tracker;
|
|
996
|
+
workspace;
|
|
997
|
+
hooks;
|
|
998
|
+
runner;
|
|
999
|
+
renderer;
|
|
1000
|
+
promptTemplate;
|
|
1001
|
+
server;
|
|
1002
|
+
interval;
|
|
1003
|
+
logger;
|
|
1004
|
+
/**
|
|
1005
|
+
* Creates a new Orchestrator instance.
|
|
1006
|
+
*
|
|
1007
|
+
* @param config - The workflow configuration
|
|
1008
|
+
* @param promptTemplate - The template used to generate agent instructions
|
|
1009
|
+
* @param overrides - Optional dependency overrides for testing or custom behavior
|
|
1010
|
+
*/
|
|
1011
|
+
constructor(config, promptTemplate, overrides) {
|
|
1012
|
+
super();
|
|
1013
|
+
this.config = config;
|
|
1014
|
+
this.promptTemplate = promptTemplate;
|
|
1015
|
+
this.state = createEmptyState(config);
|
|
1016
|
+
this.logger = new StructuredLogger();
|
|
1017
|
+
this.tracker = overrides?.tracker || this.createTracker();
|
|
1018
|
+
this.workspace = new WorkspaceManager(config.workspace);
|
|
1019
|
+
this.hooks = new WorkspaceHooks(config.hooks);
|
|
1020
|
+
this.renderer = new PromptRenderer();
|
|
1021
|
+
this.runner = new AgentRunner(overrides?.backend || this.createBackend(), {
|
|
1022
|
+
maxTurns: config.agent.maxTurns
|
|
1023
|
+
});
|
|
1024
|
+
if (config.server?.port) {
|
|
1025
|
+
this.server = new OrchestratorServer(this, config.server.port);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
createTracker() {
|
|
1029
|
+
if (this.config.tracker.kind === "roadmap") {
|
|
1030
|
+
return new RoadmapTrackerAdapter(this.config.tracker);
|
|
1031
|
+
}
|
|
1032
|
+
throw new Error(`Unsupported tracker kind: ${this.config.tracker.kind}`);
|
|
1033
|
+
}
|
|
1034
|
+
createBackend() {
|
|
1035
|
+
if (this.config.agent.backend === "mock") {
|
|
1036
|
+
return new MockBackend();
|
|
1037
|
+
} else if (this.config.agent.backend === "claude") {
|
|
1038
|
+
return new ClaudeBackend(this.config.agent.command);
|
|
1039
|
+
}
|
|
1040
|
+
throw new Error(`Unsupported agent backend: ${this.config.agent.backend}`);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Run a single tick of the orchestrator loop.
|
|
1044
|
+
*
|
|
1045
|
+
* This method fetches the latest issue states, applies them to the state machine,
|
|
1046
|
+
* and executes any resulting side effects (dispatching new agents, stopping agents, etc.).
|
|
1047
|
+
*/
|
|
1048
|
+
async tick() {
|
|
1049
|
+
const candidatesResult = await this.tracker.fetchCandidateIssues();
|
|
1050
|
+
if (!candidatesResult.ok) {
|
|
1051
|
+
this.logger.error("Failed to fetch candidate issues", {
|
|
1052
|
+
error: String(candidatesResult.error)
|
|
1053
|
+
});
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const runningIds = Array.from(this.state.running.keys());
|
|
1057
|
+
const runningStatesResult = await this.tracker.fetchIssueStatesByIds(runningIds);
|
|
1058
|
+
if (!runningStatesResult.ok) {
|
|
1059
|
+
this.logger.error("Failed to fetch running issue states", {
|
|
1060
|
+
error: String(runningStatesResult.error)
|
|
1061
|
+
});
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const tickEvent = {
|
|
1065
|
+
type: "tick",
|
|
1066
|
+
candidates: candidatesResult.value,
|
|
1067
|
+
runningStates: runningStatesResult.value,
|
|
1068
|
+
nowMs: Date.now()
|
|
1069
|
+
};
|
|
1070
|
+
const { nextState, effects } = applyEvent(this.state, tickEvent, this.config);
|
|
1071
|
+
this.state = nextState;
|
|
1072
|
+
for (const effect of effects) {
|
|
1073
|
+
await this.handleEffect(effect);
|
|
1074
|
+
}
|
|
1075
|
+
this.emit("state_change", this.getSnapshot());
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Processes a side effect generated by the state machine.
|
|
1079
|
+
*
|
|
1080
|
+
* @param effect - The effect to handle
|
|
1081
|
+
*/
|
|
1082
|
+
async handleEffect(effect) {
|
|
1083
|
+
switch (effect.type) {
|
|
1084
|
+
case "dispatch":
|
|
1085
|
+
await this.dispatchIssue(effect.issue, effect.attempt);
|
|
1086
|
+
break;
|
|
1087
|
+
case "stop":
|
|
1088
|
+
await this.stopIssue(effect.issueId);
|
|
1089
|
+
break;
|
|
1090
|
+
case "updateTokens":
|
|
1091
|
+
break;
|
|
1092
|
+
case "emitLog":
|
|
1093
|
+
this.logger.log(effect.level, effect.message, effect.context);
|
|
1094
|
+
break;
|
|
1095
|
+
case "releaseClaim":
|
|
1096
|
+
break;
|
|
1097
|
+
case "cleanWorkspace":
|
|
1098
|
+
await this.workspace.removeWorkspace(effect.identifier);
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Dispatches a new agent to work on an issue.
|
|
1104
|
+
*
|
|
1105
|
+
* @param issue - The issue to resolve
|
|
1106
|
+
* @param attempt - The retry attempt number
|
|
1107
|
+
*/
|
|
1108
|
+
async dispatchIssue(issue, attempt) {
|
|
1109
|
+
this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
|
|
1110
|
+
issueId: issue.id
|
|
1111
|
+
});
|
|
1112
|
+
try {
|
|
1113
|
+
const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
|
|
1114
|
+
if (!workspaceResult.ok) throw workspaceResult.error;
|
|
1115
|
+
const workspacePath = workspaceResult.value;
|
|
1116
|
+
const hookResult = await this.hooks.beforeRun(workspacePath);
|
|
1117
|
+
if (!hookResult.ok) throw hookResult.error;
|
|
1118
|
+
const prompt = await this.renderer.render(this.promptTemplate, {
|
|
1119
|
+
issue,
|
|
1120
|
+
attempt: attempt || 1
|
|
1121
|
+
});
|
|
1122
|
+
this.runAgentInBackgroundTask(issue, workspacePath, prompt, attempt);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
this.logger.error(`Dispatch failed for ${issue.identifier}`, { error: String(error) });
|
|
1125
|
+
await this.emitWorkerExit(issue.id, "error", attempt, String(error));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Runs an agent session in a background task to avoid blocking the main loop.
|
|
1130
|
+
*/
|
|
1131
|
+
runAgentInBackgroundTask(issue, workspacePath, prompt, attempt) {
|
|
1132
|
+
(async () => {
|
|
1133
|
+
try {
|
|
1134
|
+
const sessionGen = this.runner.runSession(issue, workspacePath, prompt);
|
|
1135
|
+
for await (const event of sessionGen) {
|
|
1136
|
+
this.emit("agent_event", { issueId: issue.id, event });
|
|
1137
|
+
}
|
|
1138
|
+
await this.emitWorkerExit(issue.id, "normal", attempt);
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
this.logger.error(`Agent runner failed for ${issue.identifier}`, { error: String(error) });
|
|
1141
|
+
await this.emitWorkerExit(issue.id, "error", attempt, String(error));
|
|
1142
|
+
}
|
|
1143
|
+
})().catch((err) => {
|
|
1144
|
+
this.logger.error("Fatal error in background task", { error: String(err) });
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Informs the state machine that an agent worker has exited.
|
|
1149
|
+
*/
|
|
1150
|
+
async emitWorkerExit(issueId, reason, attempt, error) {
|
|
1151
|
+
const event = {
|
|
1152
|
+
type: "worker_exit",
|
|
1153
|
+
issueId,
|
|
1154
|
+
reason,
|
|
1155
|
+
error,
|
|
1156
|
+
attempt
|
|
1157
|
+
};
|
|
1158
|
+
const { nextState, effects } = applyEvent(this.state, event, this.config);
|
|
1159
|
+
this.state = nextState;
|
|
1160
|
+
for (const effect of effects) {
|
|
1161
|
+
await this.handleEffect(effect);
|
|
1162
|
+
}
|
|
1163
|
+
this.emit("state_change", this.getSnapshot());
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Stops execution for a specific issue.
|
|
1167
|
+
*
|
|
1168
|
+
* @param issueId - The ID of the issue to stop
|
|
1169
|
+
*/
|
|
1170
|
+
async stopIssue(issueId) {
|
|
1171
|
+
this.logger.info(`Stopping issue: ${issueId}`);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Starts the polling loop and the internal HTTP server.
|
|
1175
|
+
*/
|
|
1176
|
+
start() {
|
|
1177
|
+
if (this.server) {
|
|
1178
|
+
void this.server.start();
|
|
1179
|
+
}
|
|
1180
|
+
const intervalMs = this.config.polling.intervalMs || 3e4;
|
|
1181
|
+
this.interval = setInterval(() => {
|
|
1182
|
+
void this.tick();
|
|
1183
|
+
}, intervalMs);
|
|
1184
|
+
void this.tick();
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Stops the orchestrator, clearing the polling interval and stopping the server.
|
|
1188
|
+
*/
|
|
1189
|
+
async stop() {
|
|
1190
|
+
if (this.interval) {
|
|
1191
|
+
clearInterval(this.interval);
|
|
1192
|
+
this.interval = void 0;
|
|
1193
|
+
}
|
|
1194
|
+
if (this.server) {
|
|
1195
|
+
this.server.stop();
|
|
1196
|
+
}
|
|
1197
|
+
this.logger.info("Orchestrator stopped.");
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Returns a point-in-time snapshot of the orchestrator's internal state.
|
|
1201
|
+
*/
|
|
1202
|
+
getSnapshot() {
|
|
1203
|
+
return {
|
|
1204
|
+
running: Array.from(this.state.running.entries()),
|
|
1205
|
+
retryAttempts: Array.from(this.state.retryAttempts.entries()),
|
|
1206
|
+
claimed: Array.from(this.state.claimed),
|
|
1207
|
+
tokenTotals: this.state.tokenTotals,
|
|
1208
|
+
maxConcurrentAgents: this.state.maxConcurrentAgents
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// src/tui/launcher.tsx
|
|
1214
|
+
var import_ink5 = require("ink");
|
|
1215
|
+
|
|
1216
|
+
// src/tui/app.tsx
|
|
1217
|
+
var import_react2 = require("react");
|
|
1218
|
+
var import_ink4 = require("ink");
|
|
1219
|
+
|
|
1220
|
+
// src/tui/components/Header.tsx
|
|
1221
|
+
var import_react = require("react");
|
|
1222
|
+
var import_ink = require("ink");
|
|
1223
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
1224
|
+
var Header = () => {
|
|
1225
|
+
const [uptime, setUptime] = (0, import_react.useState)(0);
|
|
1226
|
+
(0, import_react.useEffect)(() => {
|
|
1227
|
+
const timer = setInterval(() => {
|
|
1228
|
+
setUptime((prev) => prev + 1);
|
|
1229
|
+
}, 1e3);
|
|
1230
|
+
return () => clearInterval(timer);
|
|
1231
|
+
}, []);
|
|
1232
|
+
const formatUptime = (seconds) => {
|
|
1233
|
+
const h = Math.floor(seconds / 3600);
|
|
1234
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1235
|
+
const s = seconds % 60;
|
|
1236
|
+
return `${h}h ${m}m ${s}s`;
|
|
1237
|
+
};
|
|
1238
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, justifyContent: "space-between", children: [
|
|
1239
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink.Text, { bold: true, color: "cyan", children: "Harness Orchestrator" }),
|
|
1240
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Text, { children: [
|
|
1241
|
+
"Uptime: ",
|
|
1242
|
+
formatUptime(uptime)
|
|
1243
|
+
] })
|
|
1244
|
+
] });
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// src/tui/components/Stats.tsx
|
|
1248
|
+
var import_ink2 = require("ink");
|
|
1249
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1250
|
+
var Stats = ({ tokenTotals, runningCount, maxConcurrency }) => {
|
|
1251
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Box, { flexDirection: "row", paddingX: 1, gap: 4, children: [
|
|
1252
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Box, { flexDirection: "column", children: [
|
|
1253
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { bold: true, children: "Concurrency" }),
|
|
1254
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { children: [
|
|
1255
|
+
"Active: ",
|
|
1256
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { color: runningCount > 0 ? "green" : "gray", children: runningCount }),
|
|
1257
|
+
" /",
|
|
1258
|
+
" ",
|
|
1259
|
+
maxConcurrency
|
|
1260
|
+
] })
|
|
1261
|
+
] }),
|
|
1262
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Box, { flexDirection: "column", children: [
|
|
1263
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { bold: true, children: "Token Usage" }),
|
|
1264
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { children: [
|
|
1265
|
+
"Input: ",
|
|
1266
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { color: "yellow", children: tokenTotals.inputTokens.toLocaleString() })
|
|
1267
|
+
] }),
|
|
1268
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { children: [
|
|
1269
|
+
"Output: ",
|
|
1270
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { color: "yellow", children: tokenTotals.outputTokens.toLocaleString() })
|
|
1271
|
+
] }),
|
|
1272
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { children: [
|
|
1273
|
+
"Total: ",
|
|
1274
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { color: "yellow", children: tokenTotals.totalTokens.toLocaleString() })
|
|
1275
|
+
] })
|
|
1276
|
+
] }),
|
|
1277
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Box, { flexDirection: "column", children: [
|
|
1278
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { bold: true, children: "Efficiency" }),
|
|
1279
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { children: [
|
|
1280
|
+
"Time Running: ",
|
|
1281
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { color: "blue", children: [
|
|
1282
|
+
Math.round(tokenTotals.secondsRunning),
|
|
1283
|
+
"s"
|
|
1284
|
+
] })
|
|
1285
|
+
] })
|
|
1286
|
+
] })
|
|
1287
|
+
] });
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/tui/components/AgentsTable.tsx
|
|
1291
|
+
var import_ink3 = require("ink");
|
|
1292
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1293
|
+
var AgentsTable = ({ agents }) => {
|
|
1294
|
+
if (agents.length === 0) {
|
|
1295
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { paddingX: 1, marginY: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "gray", italic: true, children: "No active agents." }) });
|
|
1296
|
+
}
|
|
1297
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Box, { flexDirection: "column", marginY: 1, children: [
|
|
1298
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, underline: true, children: "Active Agents" }),
|
|
1299
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Box, { flexDirection: "row", borderStyle: "single", borderColor: "gray", children: [
|
|
1300
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Identifier" }) }),
|
|
1301
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Phase" }) }),
|
|
1302
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 10, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Tokens" }) }),
|
|
1303
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { flexGrow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Message" }) })
|
|
1304
|
+
] }),
|
|
1305
|
+
agents.map((agent) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Box, { flexDirection: "row", children: [
|
|
1306
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { children: agent.identifier }) }),
|
|
1307
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "cyan", children: agent.phase }) }),
|
|
1308
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 10, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "yellow", children: agent.session?.totalTokens || 0 }) }),
|
|
1309
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { flexGrow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { wrap: "truncate-end", children: agent.session?.lastMessage || "-" }) })
|
|
1310
|
+
] }, agent.issueId))
|
|
1311
|
+
] });
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
// src/tui/app.tsx
|
|
1315
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1316
|
+
var Dashboard = ({ orchestrator }) => {
|
|
1317
|
+
const [state, setState] = (0, import_react2.useState)(orchestrator.getSnapshot());
|
|
1318
|
+
const { exit } = (0, import_ink4.useApp)();
|
|
1319
|
+
(0, import_react2.useEffect)(() => {
|
|
1320
|
+
const handleStateChange = (newState) => {
|
|
1321
|
+
setState(newState);
|
|
1322
|
+
};
|
|
1323
|
+
orchestrator.on("state_change", handleStateChange);
|
|
1324
|
+
return () => {
|
|
1325
|
+
orchestrator.off("state_change", handleStateChange);
|
|
1326
|
+
};
|
|
1327
|
+
}, [orchestrator]);
|
|
1328
|
+
(0, import_ink4.useInput)((input, key) => {
|
|
1329
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
1330
|
+
orchestrator.stop();
|
|
1331
|
+
exit();
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
const runningAgents = state.running.map(([_, entry]) => entry);
|
|
1335
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ink4.Box, { flexDirection: "column", padding: 1, children: [
|
|
1336
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Header, {}),
|
|
1337
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1338
|
+
Stats,
|
|
1339
|
+
{
|
|
1340
|
+
tokenTotals: state.tokenTotals,
|
|
1341
|
+
runningCount: state.running.length,
|
|
1342
|
+
maxConcurrency: state.maxConcurrentAgents
|
|
1343
|
+
}
|
|
1344
|
+
),
|
|
1345
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AgentsTable, { agents: runningAgents }),
|
|
1346
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Box, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Text, { color: "gray", children: "Press 'q' to quit" }) })
|
|
1347
|
+
] });
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// src/tui/launcher.tsx
|
|
1351
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1352
|
+
function launchTUI(orchestrator) {
|
|
1353
|
+
const { waitUntilExit } = (0, import_ink5.render)(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Dashboard, { orchestrator }));
|
|
1354
|
+
return { waitUntilExit };
|
|
1355
|
+
}
|
|
1356
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1357
|
+
0 && (module.exports = {
|
|
1358
|
+
ClaudeBackend,
|
|
1359
|
+
LinearGraphQLStub,
|
|
1360
|
+
MockBackend,
|
|
1361
|
+
Orchestrator,
|
|
1362
|
+
PromptRenderer,
|
|
1363
|
+
RoadmapTrackerAdapter,
|
|
1364
|
+
WorkflowLoader,
|
|
1365
|
+
WorkspaceHooks,
|
|
1366
|
+
WorkspaceManager,
|
|
1367
|
+
applyEvent,
|
|
1368
|
+
calculateRetryDelay,
|
|
1369
|
+
canDispatch,
|
|
1370
|
+
createEmptyState,
|
|
1371
|
+
getAvailableSlots,
|
|
1372
|
+
getDefaultConfig,
|
|
1373
|
+
getPerStateCount,
|
|
1374
|
+
isEligible,
|
|
1375
|
+
launchTUI,
|
|
1376
|
+
reconcile,
|
|
1377
|
+
selectCandidates,
|
|
1378
|
+
sortCandidates,
|
|
1379
|
+
validateWorkflowConfig
|
|
1380
|
+
});
|