@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/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
+ });