codeharness 0.29.0 → 0.29.2

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 CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  validateDockerfile,
41
41
  warn,
42
42
  writeState
43
- } from "./chunk-PTHST5Z7.js";
43
+ } from "./chunk-3ZSXMCZV.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { Command } from "commander";
@@ -1896,7 +1896,7 @@ var AgentResolveError = class extends Error {
1896
1896
  }
1897
1897
  };
1898
1898
  var TEMPLATES_DIR = resolve2(getPackageRoot(), "templates/agents");
1899
- var DEFAULT_MODEL = "claude-sonnet-4-6-20250514";
1899
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
1900
1900
  var SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
1901
1901
  function validateName(name) {
1902
1902
  if (!name || !SAFE_NAME_RE.test(name)) {
@@ -3357,7 +3357,12 @@ ${coverageDedup}`;
3357
3357
  ...task.plugins ?? definition.plugins ? { plugins: task.plugins ?? definition.plugins } : {},
3358
3358
  ...task.max_budget_usd != null ? { timeout: task.max_budget_usd } : {}
3359
3359
  };
3360
- info(`[${taskName}] ${storyKey} \u2014 dispatching via ${driverName} (model: ${model})`);
3360
+ const emit = config.onEvent;
3361
+ if (emit) {
3362
+ emit({ type: "dispatch-start", taskName, storyKey, driverName, model });
3363
+ } else {
3364
+ info(`[${taskName}] ${storyKey} \u2014 dispatching via ${driverName} (model: ${model})...`);
3365
+ }
3361
3366
  let output = "";
3362
3367
  let resultSessionId = "";
3363
3368
  let cost = 0;
@@ -3368,6 +3373,9 @@ ${coverageDedup}`;
3368
3373
  const startMs = Date.now();
3369
3374
  try {
3370
3375
  for await (const event of driver.dispatch(dispatchOpts)) {
3376
+ if (emit) {
3377
+ emit({ type: "stream-event", taskName, storyKey, driverName, streamEvent: event });
3378
+ }
3371
3379
  if (event.type === "text") {
3372
3380
  output += event.text;
3373
3381
  }
@@ -3407,6 +3415,13 @@ ${coverageDedup}`;
3407
3415
  await workspace.cleanup();
3408
3416
  }
3409
3417
  }
3418
+ const elapsedMs = Date.now() - startMs;
3419
+ if (emit) {
3420
+ emit({ type: "dispatch-end", taskName, storyKey, driverName, elapsedMs, costUsd: cost });
3421
+ } else {
3422
+ const elapsed = (elapsedMs / 1e3).toFixed(1);
3423
+ info(`[${taskName}] ${storyKey} \u2014 done (${elapsed}s, cost: $${cost.toFixed(4)})`);
3424
+ }
3410
3425
  if (errorEvent) {
3411
3426
  const categoryToCode = {
3412
3427
  RATE_LIMIT: "RATE_LIMIT",
@@ -3976,6 +3991,11 @@ async function executeWorkflow(config) {
3976
3991
  } catch (err) {
3977
3992
  const engineError = handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3978
3993
  errors.push(engineError);
3994
+ if (config.onEvent) {
3995
+ config.onEvent({ type: "dispatch-error", taskName, storyKey: PER_RUN_SENTINEL, error: { code: engineError.code, message: engineError.message } });
3996
+ } else {
3997
+ warn(`[${taskName}] ${PER_RUN_SENTINEL} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
3998
+ }
3979
3999
  state = recordErrorInState(state, taskName, PER_RUN_SENTINEL, engineError);
3980
4000
  writeWorkflowState(state, projectDir);
3981
4001
  if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
@@ -4008,6 +4028,11 @@ async function executeWorkflow(config) {
4008
4028
  } catch (err) {
4009
4029
  const engineError = handleDispatchError(err, taskName, item.key);
4010
4030
  errors.push(engineError);
4031
+ if (config.onEvent) {
4032
+ config.onEvent({ type: "dispatch-error", taskName, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
4033
+ } else {
4034
+ warn(`[${taskName}] ${item.key} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4035
+ }
4011
4036
  state = recordErrorInState(state, taskName, item.key, engineError);
4012
4037
  writeWorkflowState(state, projectDir);
4013
4038
  if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
@@ -4857,6 +4882,1145 @@ var LanePool = class {
4857
4882
  }
4858
4883
  };
4859
4884
 
4885
+ // src/lib/ink-renderer.tsx
4886
+ import { render as inkRender } from "ink";
4887
+
4888
+ // src/lib/ink-components.tsx
4889
+ import { Text as Text8, Box as Box8 } from "ink";
4890
+
4891
+ // src/lib/ink-activity-components.tsx
4892
+ import { Text, Box } from "ink";
4893
+ import { Spinner } from "@inkjs/ui";
4894
+ import { jsx, jsxs } from "react/jsx-runtime";
4895
+ var MESSAGE_STYLE = {
4896
+ ok: { prefix: "[OK]", color: "green" },
4897
+ warn: { prefix: "[WARN]", color: "yellow" },
4898
+ fail: { prefix: "[FAIL]", color: "red" }
4899
+ };
4900
+ function StoryMessageLine({ msg }) {
4901
+ const style = MESSAGE_STYLE[msg.type];
4902
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
4903
+ /* @__PURE__ */ jsxs(Text, { children: [
4904
+ /* @__PURE__ */ jsx(Text, { color: style.color, bold: true, children: style.prefix }),
4905
+ /* @__PURE__ */ jsx(Text, { children: ` Story ${msg.key}: ${msg.message}` })
4906
+ ] }),
4907
+ msg.details?.map((d, j) => /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2514 ${d}` }, j))
4908
+ ] });
4909
+ }
4910
+ function CompletedTool({ entry }) {
4911
+ const argsSummary = entry.args.length > 60 ? entry.args.slice(0, 60) + "\u2026" : entry.args;
4912
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
4913
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 " }),
4914
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
4915
+ /* @__PURE__ */ jsx(Text, { children: entry.name }),
4916
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "] " }),
4917
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: argsSummary }),
4918
+ entry.driver && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${entry.driver})` })
4919
+ ] });
4920
+ }
4921
+ var VISIBLE_COMPLETED_TOOLS = 5;
4922
+ function CompletedTools({ tools }) {
4923
+ const visible = tools.slice(-VISIBLE_COMPLETED_TOOLS);
4924
+ const hidden = tools.length - visible.length;
4925
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
4926
+ hidden > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${hidden} earlier tools` }),
4927
+ visible.map((entry, i) => /* @__PURE__ */ jsx(CompletedTool, { entry }, i))
4928
+ ] });
4929
+ }
4930
+ function ActiveTool({ name, driverName }) {
4931
+ return /* @__PURE__ */ jsxs(Box, { children: [
4932
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A1 " }),
4933
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
4934
+ /* @__PURE__ */ jsx(Text, { bold: true, children: name }),
4935
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "] " }),
4936
+ driverName && /* @__PURE__ */ jsx(Text, { dimColor: true, children: `(${driverName}) ` }),
4937
+ /* @__PURE__ */ jsx(Spinner, { label: "" })
4938
+ ] });
4939
+ }
4940
+ function LastThought({ text }) {
4941
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
4942
+ /* @__PURE__ */ jsx(Text, { children: "\u{1F4AD} " }),
4943
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: text })
4944
+ ] });
4945
+ }
4946
+ function RetryNotice({ info: info3 }) {
4947
+ return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
4948
+ "\u23F3 API retry ",
4949
+ info3.attempt,
4950
+ " (waiting ",
4951
+ info3.delay,
4952
+ "ms)"
4953
+ ] });
4954
+ }
4955
+ function DriverCostSummary({ driverCosts }) {
4956
+ if (!driverCosts) return null;
4957
+ const entries = Object.entries(driverCosts).sort(([a], [b]) => a.localeCompare(b));
4958
+ if (entries.length === 0) return null;
4959
+ const parts = entries.map(([driver, cost]) => `${driver} $${cost.toFixed(2)}`).join(", ");
4960
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `Cost: ${parts}` });
4961
+ }
4962
+
4963
+ // src/lib/ink-app.tsx
4964
+ import { Box as Box7, Static, Text as Text7, useInput } from "ink";
4965
+
4966
+ // src/lib/ink-workflow.tsx
4967
+ import { Text as Text2, Box as Box2 } from "ink";
4968
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4969
+ var termWidth = () => Math.min(process.stdout.columns || 60, 80);
4970
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4971
+ function isLoopBlock2(step) {
4972
+ return typeof step === "object" && step !== null && "loop" in step;
4973
+ }
4974
+ function formatCost(costUsd) {
4975
+ if (costUsd == null) return "...";
4976
+ return `$${costUsd.toFixed(2)}`;
4977
+ }
4978
+ function formatElapsed2(ms) {
4979
+ if (ms == null) return "...";
4980
+ const seconds = Math.round(ms / 1e3);
4981
+ if (seconds >= 60) {
4982
+ return `${Math.floor(seconds / 60)}m`;
4983
+ }
4984
+ return `${seconds}s`;
4985
+ }
4986
+ function TaskNode({ name, status, spinnerFrame }) {
4987
+ const s = status ?? "pending";
4988
+ switch (s) {
4989
+ case "done":
4990
+ return /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
4991
+ name,
4992
+ " \u2713"
4993
+ ] });
4994
+ case "active": {
4995
+ const frame = SPINNER_FRAMES[(spinnerFrame ?? 0) % SPINNER_FRAMES.length];
4996
+ return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
4997
+ frame,
4998
+ " ",
4999
+ name
5000
+ ] });
5001
+ }
5002
+ case "failed":
5003
+ return /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
5004
+ name,
5005
+ " \u2717"
5006
+ ] });
5007
+ case "pending":
5008
+ default:
5009
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: name });
5010
+ }
5011
+ }
5012
+ function loopIteration(tasks, taskStates) {
5013
+ const anyStarted = tasks.some((t) => {
5014
+ const s = taskStates[t];
5015
+ return s !== void 0 && s !== "pending";
5016
+ });
5017
+ return anyStarted ? 1 : 0;
5018
+ }
5019
+ function collectTaskNames(flow) {
5020
+ const names = [];
5021
+ for (const step of flow) {
5022
+ if (isLoopBlock2(step)) {
5023
+ names.push(...step.loop);
5024
+ } else {
5025
+ names.push(step);
5026
+ }
5027
+ }
5028
+ return names;
5029
+ }
5030
+ function hasMetaData(taskMeta) {
5031
+ if (!taskMeta) return false;
5032
+ return Object.keys(taskMeta).length > 0;
5033
+ }
5034
+ function WorkflowGraph({ flow, currentTask, taskStates, taskMeta }) {
5035
+ if (flow.length === 0 || Object.keys(taskStates).length === 0) {
5036
+ return null;
5037
+ }
5038
+ const meta = taskMeta ?? {};
5039
+ const showMeta = hasMetaData(taskMeta);
5040
+ const spinnerFrame = Math.floor(Date.now() / 80);
5041
+ const elements = [];
5042
+ for (let i = 0; i < flow.length; i++) {
5043
+ const step = flow[i];
5044
+ if (i > 0) {
5045
+ elements.push(/* @__PURE__ */ jsx2(Text2, { children: " \u2192 " }, `arrow-${i}`));
5046
+ }
5047
+ if (isLoopBlock2(step)) {
5048
+ const iteration = loopIteration(step.loop, taskStates);
5049
+ const loopNodes = [];
5050
+ for (let j = 0; j < step.loop.length; j++) {
5051
+ if (j > 0) {
5052
+ loopNodes.push(/* @__PURE__ */ jsx2(Text2, { children: " \u2192 " }, `loop-arrow-${i}-${j}`));
5053
+ }
5054
+ loopNodes.push(
5055
+ /* @__PURE__ */ jsx2(TaskNode, { name: step.loop[j], status: taskStates[step.loop[j]], spinnerFrame }, `loop-task-${i}-${j}`)
5056
+ );
5057
+ }
5058
+ elements.push(
5059
+ /* @__PURE__ */ jsxs2(Text2, { children: [
5060
+ /* @__PURE__ */ jsxs2(Text2, { children: [
5061
+ "loop(",
5062
+ iteration,
5063
+ ")[ "
5064
+ ] }),
5065
+ loopNodes,
5066
+ /* @__PURE__ */ jsx2(Text2, { children: " ]" })
5067
+ ] }, `loop-${i}`)
5068
+ );
5069
+ } else {
5070
+ elements.push(
5071
+ /* @__PURE__ */ jsx2(TaskNode, { name: step, status: taskStates[step], spinnerFrame }, `task-${i}`)
5072
+ );
5073
+ }
5074
+ }
5075
+ let driverRow = null;
5076
+ let costRow = null;
5077
+ if (showMeta) {
5078
+ const taskNames = collectTaskNames(flow);
5079
+ const driverParts = [];
5080
+ const costParts = [];
5081
+ let hasAnyCost = false;
5082
+ for (const name of taskNames) {
5083
+ const m = meta[name];
5084
+ const driver = m?.driver ?? "";
5085
+ driverParts.push(driver);
5086
+ const state = taskStates[name];
5087
+ if (state === "done") {
5088
+ const costStr = formatCost(m?.costUsd);
5089
+ const timeStr = formatElapsed2(m?.elapsedMs);
5090
+ costParts.push(`${costStr} / ${timeStr}`);
5091
+ hasAnyCost = true;
5092
+ } else {
5093
+ costParts.push("");
5094
+ }
5095
+ }
5096
+ const hasSomeDriver = driverParts.some((d) => d.length > 0);
5097
+ if (hasSomeDriver) {
5098
+ const driverLabels = [];
5099
+ for (let idx = 0; idx < taskNames.length; idx++) {
5100
+ if (idx > 0) {
5101
+ driverLabels.push(/* @__PURE__ */ jsx2(Text2, { children: " " }, `drv-sep-${idx}`));
5102
+ }
5103
+ driverLabels.push(
5104
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: driverParts[idx] || " " }, `drv-${idx}`)
5105
+ );
5106
+ }
5107
+ driverRow = /* @__PURE__ */ jsxs2(Text2, { children: [
5108
+ " ",
5109
+ driverLabels
5110
+ ] });
5111
+ }
5112
+ if (hasAnyCost) {
5113
+ const costLabels = [];
5114
+ for (let idx = 0; idx < taskNames.length; idx++) {
5115
+ if (idx > 0) {
5116
+ costLabels.push(/* @__PURE__ */ jsx2(Text2, { children: " " }, `cost-sep-${idx}`));
5117
+ }
5118
+ costLabels.push(
5119
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: costParts[idx] || " " }, `cost-${idx}`)
5120
+ );
5121
+ }
5122
+ costRow = /* @__PURE__ */ jsxs2(Text2, { children: [
5123
+ " ",
5124
+ costLabels
5125
+ ] });
5126
+ }
5127
+ }
5128
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
5129
+ /* @__PURE__ */ jsx2(Text2, { children: "\u2501".repeat(termWidth()) }),
5130
+ /* @__PURE__ */ jsxs2(Text2, { children: [
5131
+ " ",
5132
+ elements
5133
+ ] }),
5134
+ driverRow,
5135
+ costRow,
5136
+ /* @__PURE__ */ jsx2(Text2, { children: "\u2501".repeat(termWidth()) })
5137
+ ] });
5138
+ }
5139
+
5140
+ // src/lib/ink-lane-container.tsx
5141
+ import { Text as Text4, Box as Box4 } from "ink";
5142
+
5143
+ // src/lib/ink-lane.tsx
5144
+ import { Text as Text3, Box as Box3 } from "ink";
5145
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
5146
+ function formatLaneCost(cost) {
5147
+ if (cost == null) return "--";
5148
+ return `$${cost.toFixed(2)}`;
5149
+ }
5150
+ function formatLaneElapsed(ms) {
5151
+ if (ms == null) return "--";
5152
+ const totalSeconds = Math.round(ms / 1e3);
5153
+ const minutes = Math.floor(totalSeconds / 60);
5154
+ if (minutes >= 60) {
5155
+ const hours = Math.floor(minutes / 60);
5156
+ const remainingMinutes = minutes % 60;
5157
+ return `${hours}h ${remainingMinutes}m`;
5158
+ }
5159
+ if (minutes >= 1) {
5160
+ return `${minutes}m`;
5161
+ }
5162
+ return `${totalSeconds}s`;
5163
+ }
5164
+ function StoryProgressBar({ entries }) {
5165
+ if (entries.length === 0) return null;
5166
+ const items = [];
5167
+ for (let i = 0; i < entries.length; i++) {
5168
+ const entry = entries[i];
5169
+ if (i > 0) items.push(/* @__PURE__ */ jsx3(Text3, { children: " " }, `sp-${i}`));
5170
+ switch (entry.status) {
5171
+ case "done":
5172
+ items.push(/* @__PURE__ */ jsx3(Text3, { color: "green", children: `\u2713 ${entry.key}` }, `s-${i}`));
5173
+ break;
5174
+ case "in-progress":
5175
+ items.push(/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: `\u25C6 ${entry.key}` }, `s-${i}`));
5176
+ break;
5177
+ case "pending":
5178
+ items.push(/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `\u25CB ${entry.key}` }, `s-${i}`));
5179
+ break;
5180
+ }
5181
+ }
5182
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
5183
+ " ",
5184
+ items
5185
+ ] });
5186
+ }
5187
+ function Lane(props) {
5188
+ const {
5189
+ epicId,
5190
+ epicTitle,
5191
+ currentStory,
5192
+ phase,
5193
+ acProgress,
5194
+ storyProgressEntries,
5195
+ driver,
5196
+ cost,
5197
+ elapsedTime,
5198
+ laneIndex
5199
+ } = props;
5200
+ const laneLabel = laneIndex != null ? `Lane ${laneIndex}: ` : "";
5201
+ const titleLine = `${laneLabel}Epic ${epicId} \u2014 ${epicTitle}`;
5202
+ const storyParts = [];
5203
+ if (currentStory) storyParts.push(currentStory);
5204
+ if (phase) storyParts.push(`\u25C6 ${phase}`);
5205
+ if (acProgress) storyParts.push(`(AC ${acProgress})`);
5206
+ const storyLine = storyParts.length > 0 ? ` ${storyParts.join(" ")}` : null;
5207
+ const driverLine = ` ${driver ?? "unknown"} | ${formatLaneCost(cost)} / ${formatLaneElapsed(elapsedTime)}`;
5208
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
5209
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: titleLine }),
5210
+ storyLine && /* @__PURE__ */ jsx3(Text3, { children: storyLine }),
5211
+ /* @__PURE__ */ jsx3(StoryProgressBar, { entries: storyProgressEntries }),
5212
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: driverLine })
5213
+ ] });
5214
+ }
5215
+
5216
+ // src/lib/ink-lane-container.tsx
5217
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
5218
+ function getLayoutMode(terminalWidth) {
5219
+ if (terminalWidth >= 120) return "side-by-side";
5220
+ if (terminalWidth >= 80) return "stacked";
5221
+ return "single";
5222
+ }
5223
+ function truncate(str, maxLen) {
5224
+ if (maxLen < 4) return str.slice(0, maxLen);
5225
+ if (str.length <= maxLen) return str;
5226
+ return str.slice(0, maxLen - 1) + "\u2026";
5227
+ }
5228
+ function CollapsedLanes({ lanes }) {
5229
+ if (lanes.length === 0) return null;
5230
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: lanes.map((lane) => {
5231
+ const storyPart = lane.currentStory ?? "--";
5232
+ const phasePart = lane.phase ?? "--";
5233
+ const costPart = formatLaneCost(lane.cost);
5234
+ const timePart = formatLaneElapsed(lane.elapsedTime);
5235
+ const line = `Lane ${lane.laneIndex}: ${lane.epicTitle} \u2502 ${storyPart} \u25C6 ${phasePart} \u2502 ${costPart} / ${timePart}`;
5236
+ return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: line }, `collapsed-${lane.laneIndex}`);
5237
+ }) });
5238
+ }
5239
+ function LaneContainer({ lanes, terminalWidth }) {
5240
+ if (lanes.length === 0) return null;
5241
+ const mode = getLayoutMode(terminalWidth);
5242
+ let fullLanes;
5243
+ let collapsedLaneData;
5244
+ if (mode === "single") {
5245
+ let mostRecentIndex = 0;
5246
+ let mostRecentTime = -Infinity;
5247
+ for (let i = 0; i < lanes.length; i++) {
5248
+ const t = lanes[i].lastActivityTime ?? 0;
5249
+ if (t >= mostRecentTime) {
5250
+ mostRecentTime = t;
5251
+ mostRecentIndex = i;
5252
+ }
5253
+ }
5254
+ fullLanes = [lanes[mostRecentIndex]];
5255
+ collapsedLaneData = lanes.filter((_, i) => i !== mostRecentIndex).map((lane, i) => {
5256
+ const originalIndex = i >= mostRecentIndex ? i + 2 : i + 1;
5257
+ return {
5258
+ laneIndex: originalIndex,
5259
+ epicTitle: truncate(lane.epicTitle, 30),
5260
+ currentStory: lane.currentStory,
5261
+ phase: lane.phase,
5262
+ cost: lane.cost,
5263
+ elapsedTime: lane.elapsedTime
5264
+ };
5265
+ });
5266
+ } else {
5267
+ fullLanes = lanes.slice(0, 2);
5268
+ collapsedLaneData = lanes.slice(2).map((lane, i) => ({
5269
+ laneIndex: i + 3,
5270
+ epicTitle: truncate(lane.epicTitle, 30),
5271
+ currentStory: lane.currentStory,
5272
+ phase: lane.phase,
5273
+ cost: lane.cost,
5274
+ elapsedTime: lane.elapsedTime
5275
+ }));
5276
+ }
5277
+ const laneWidth = mode === "side-by-side" ? Math.floor(terminalWidth / 2) - 1 : terminalWidth;
5278
+ const fullLaneElements = [];
5279
+ for (let i = 0; i < fullLanes.length; i++) {
5280
+ const lane = fullLanes[i];
5281
+ const laneIndex = mode === "single" ? void 0 : i + 1;
5282
+ if (i > 0 && mode !== "side-by-side") {
5283
+ fullLaneElements.push(/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2500".repeat(Math.min(terminalWidth, 60)) }, `sep-${i}`));
5284
+ }
5285
+ fullLaneElements.push(
5286
+ /* @__PURE__ */ jsx4(Box4, { width: mode === "side-by-side" ? laneWidth : void 0, flexDirection: "column", children: /* @__PURE__ */ jsx4(
5287
+ Lane,
5288
+ {
5289
+ epicId: lane.epicId,
5290
+ epicTitle: lane.epicTitle,
5291
+ currentStory: lane.currentStory,
5292
+ phase: lane.phase,
5293
+ acProgress: lane.acProgress,
5294
+ storyProgressEntries: lane.storyProgressEntries,
5295
+ driver: lane.driver,
5296
+ cost: lane.cost,
5297
+ elapsedTime: lane.elapsedTime,
5298
+ laneIndex
5299
+ }
5300
+ ) }, `lane-${lane.epicId}`)
5301
+ );
5302
+ }
5303
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
5304
+ mode === "side-by-side" ? /* @__PURE__ */ jsx4(Box4, { flexDirection: "row", children: fullLaneElements }) : /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: fullLaneElements }),
5305
+ /* @__PURE__ */ jsx4(CollapsedLanes, { lanes: collapsedLaneData })
5306
+ ] });
5307
+ }
5308
+
5309
+ // src/lib/ink-summary-bar.tsx
5310
+ import { Text as Text5, Box as Box5 } from "ink";
5311
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
5312
+ function formatConflictText(count) {
5313
+ return count === 1 ? "1 conflict" : `${count} conflicts`;
5314
+ }
5315
+ function formatCost2(cost) {
5316
+ return `$${cost.toFixed(2)}`;
5317
+ }
5318
+ function SummaryBar({ doneStories, mergingEpic, pendingEpics, completedLanes }) {
5319
+ const doneSection = doneStories.length > 0 ? doneStories.map((s) => `${s} \u2713`).join(" ") : "\u2014";
5320
+ let mergingNode;
5321
+ if (!mergingEpic) {
5322
+ mergingNode = /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2014" });
5323
+ } else if (mergingEpic.status === "resolving") {
5324
+ const conflictText = mergingEpic.conflictCount != null ? ` (resolving ${formatConflictText(mergingEpic.conflictCount)})` : "";
5325
+ mergingNode = /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: `${mergingEpic.epicId} \u2192 main${conflictText} \u25CC` });
5326
+ } else if (mergingEpic.status === "in-progress") {
5327
+ mergingNode = /* @__PURE__ */ jsx5(Text5, { children: `${mergingEpic.epicId} \u2192 main \u25CC` });
5328
+ } else {
5329
+ mergingNode = /* @__PURE__ */ jsx5(Text5, { color: "green", children: `${mergingEpic.epicId} \u2192 main \u2713` });
5330
+ }
5331
+ const pendingSection = pendingEpics.length > 0 ? pendingEpics.join(", ") : "\u2014";
5332
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
5333
+ /* @__PURE__ */ jsxs5(Text5, { children: [
5334
+ /* @__PURE__ */ jsx5(Text5, { color: "green", children: `Done: ${doneSection}` }),
5335
+ /* @__PURE__ */ jsx5(Text5, { children: " \u2502 " }),
5336
+ /* @__PURE__ */ jsx5(Text5, { children: "Merging: " }),
5337
+ mergingNode,
5338
+ /* @__PURE__ */ jsx5(Text5, { children: " \u2502 " }),
5339
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Pending: ${pendingSection}` })
5340
+ ] }),
5341
+ completedLanes && completedLanes.length > 0 && completedLanes.map((lane) => /* @__PURE__ */ jsx5(Text5, { color: "green", children: `[OK] Lane ${lane.laneIndex}: Epic ${lane.epicId} complete (${lane.storyCount} stories, ${formatCost2(lane.cost)}, ${lane.elapsed})` }, `lane-complete-${lane.laneIndex}`))
5342
+ ] });
5343
+ }
5344
+
5345
+ // src/lib/ink-merge-status.tsx
5346
+ import { Text as Text6, Box as Box6 } from "ink";
5347
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
5348
+ function MergeStatus({ mergeState }) {
5349
+ if (!mergeState) return null;
5350
+ const lines = [];
5351
+ if (mergeState.outcome === "clean") {
5352
+ const count = mergeState.conflictCount ?? 0;
5353
+ lines.push(
5354
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: `[OK] Merge ${mergeState.epicId} \u2192 main: clean (${count} conflicts)` }, "merge-clean")
5355
+ );
5356
+ } else if (mergeState.outcome === "resolved") {
5357
+ const count = mergeState.conflictCount ?? mergeState.conflicts?.length ?? 0;
5358
+ const suffix = count === 1 ? "" : "s";
5359
+ lines.push(
5360
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: `[OK] Merge ${mergeState.epicId} \u2192 main: ${count} conflict${suffix} auto-resolved` }, "merge-resolved")
5361
+ );
5362
+ if (mergeState.conflicts && mergeState.conflicts.length > 0) {
5363
+ for (let i = 0; i < mergeState.conflicts.length; i++) {
5364
+ lines.push(
5365
+ /* @__PURE__ */ jsxs6(Text6, { children: [
5366
+ " \u2514 ",
5367
+ mergeState.conflicts[i]
5368
+ ] }, `conflict-${i}`)
5369
+ );
5370
+ }
5371
+ }
5372
+ } else if (mergeState.outcome === "escalated") {
5373
+ lines.push(
5374
+ /* @__PURE__ */ jsx6(Text6, { color: "red", children: `[FAIL] Merge ${mergeState.epicId} \u2192 main: ${mergeState.reason ?? "unknown error"}` }, "merge-escalated")
5375
+ );
5376
+ if (mergeState.conflicts && mergeState.conflicts.length > 0) {
5377
+ for (let i = 0; i < mergeState.conflicts.length; i++) {
5378
+ lines.push(
5379
+ /* @__PURE__ */ jsxs6(Text6, { children: [
5380
+ " \u2514 ",
5381
+ mergeState.conflicts[i]
5382
+ ] }, `esc-conflict-${i}`)
5383
+ );
5384
+ }
5385
+ }
5386
+ lines.push(
5387
+ /* @__PURE__ */ jsx6(Text6, { color: "red", children: " \u2192 Manual resolution required" }, "manual")
5388
+ );
5389
+ if (mergeState.worktreePath) {
5390
+ lines.push(
5391
+ /* @__PURE__ */ jsx6(Text6, { color: "red", children: ` \u2192 Worktree preserved: ${mergeState.worktreePath}` }, "worktree")
5392
+ );
5393
+ }
5394
+ } else if (mergeState.outcome === "in-progress") {
5395
+ lines.push(
5396
+ /* @__PURE__ */ jsx6(Text6, { children: `Merging ${mergeState.epicId} \u2192 main \u25CC` }, "merge-inprog")
5397
+ );
5398
+ }
5399
+ if (mergeState.testResults) {
5400
+ const t = mergeState.testResults;
5401
+ const hasFailed = t.failed > 0;
5402
+ const prefix = hasFailed ? "[FAIL]" : "[OK]";
5403
+ const color = hasFailed ? "red" : "green";
5404
+ let testLine = `${prefix} Tests: ${t.passed}/${t.total} passed (${t.durationSecs}s)`;
5405
+ if (t.coverage != null) {
5406
+ testLine += ` ${t.coverage}% coverage`;
5407
+ }
5408
+ lines.push(
5409
+ /* @__PURE__ */ jsx6(Text6, { color, children: testLine }, "tests")
5410
+ );
5411
+ }
5412
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: lines });
5413
+ }
5414
+
5415
+ // src/lib/ink-app.tsx
5416
+ import { Fragment, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
5417
+ function LaneActivityHeader({ activeLaneId, laneCount }) {
5418
+ if (laneCount <= 1 || !activeLaneId) return null;
5419
+ return /* @__PURE__ */ jsx7(Text7, { children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: `[Lane ${activeLaneId} \u25B8]` }) });
5420
+ }
5421
+ function App({ state, onCycleLane }) {
5422
+ const lanes = state.lanes;
5423
+ const laneCount = lanes?.length ?? 0;
5424
+ const terminalWidth = process.stdout.columns || 80;
5425
+ useInput((_input, key) => {
5426
+ if (key.ctrl && _input === "l" && onCycleLane && laneCount > 1) {
5427
+ onCycleLane();
5428
+ }
5429
+ }, { isActive: typeof process.stdin.setRawMode === "function" });
5430
+ const activeLaneCount = state.laneCount ?? 0;
5431
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
5432
+ /* @__PURE__ */ jsx7(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx7(StoryMessageLine, { msg }, i) }),
5433
+ /* @__PURE__ */ jsx7(Header, { info: state.sprintInfo, laneCount: laneCount > 1 ? laneCount : void 0 }),
5434
+ laneCount > 1 ? /* @__PURE__ */ jsx7(LaneContainer, { lanes, terminalWidth }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
5435
+ /* @__PURE__ */ jsx7(WorkflowGraph, { flow: state.workflowFlow, currentTask: state.currentTaskName, taskStates: state.taskStates, taskMeta: state.taskMeta }),
5436
+ /* @__PURE__ */ jsx7(StoryBreakdown, { stories: state.stories, sprintInfo: state.sprintInfo }),
5437
+ /* @__PURE__ */ jsx7(DriverCostSummary, { driverCosts: state.driverCosts })
5438
+ ] }),
5439
+ laneCount > 1 && state.summaryBar && /* @__PURE__ */ jsxs7(Fragment, { children: [
5440
+ /* @__PURE__ */ jsx7(Separator, {}),
5441
+ /* @__PURE__ */ jsx7(SummaryBar, { ...state.summaryBar })
5442
+ ] }),
5443
+ laneCount > 1 && state.mergeState && /* @__PURE__ */ jsxs7(Fragment, { children: [
5444
+ /* @__PURE__ */ jsx7(Separator, {}),
5445
+ /* @__PURE__ */ jsx7(MergeStatus, { mergeState: state.mergeState })
5446
+ ] }),
5447
+ (state.stories.length > 0 || laneCount > 1 && (state.summaryBar || state.mergeState)) && /* @__PURE__ */ jsx7(Separator, {}),
5448
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 1, children: [
5449
+ /* @__PURE__ */ jsx7(LaneActivityHeader, { activeLaneId: state.activeLaneId ?? null, laneCount: activeLaneCount }),
5450
+ /* @__PURE__ */ jsx7(CompletedTools, { tools: state.completedTools }),
5451
+ state.activeTool && /* @__PURE__ */ jsx7(ActiveTool, { name: state.activeTool.name, driverName: state.activeDriverName }),
5452
+ state.lastThought && /* @__PURE__ */ jsx7(LastThought, { text: state.lastThought }),
5453
+ state.retryInfo && /* @__PURE__ */ jsx7(RetryNotice, { info: state.retryInfo })
5454
+ ] })
5455
+ ] });
5456
+ }
5457
+
5458
+ // src/lib/ink-components.tsx
5459
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
5460
+ function Separator() {
5461
+ const width = process.stdout.columns || 60;
5462
+ return /* @__PURE__ */ jsx8(Text8, { children: "\u2501".repeat(width) });
5463
+ }
5464
+ function shortKey(key) {
5465
+ const m = key.match(/^(\d+-\d+)/);
5466
+ return m ? m[1] : key;
5467
+ }
5468
+ function formatCost3(cost) {
5469
+ return `$${cost.toFixed(2)}`;
5470
+ }
5471
+ function Header({ info: info3, laneCount }) {
5472
+ if (!info3) return null;
5473
+ const parts = ["codeharness run"];
5474
+ if (laneCount != null && laneCount > 1) {
5475
+ parts.push(`${laneCount} lanes`);
5476
+ }
5477
+ if (info3.iterationCount != null) {
5478
+ parts.push(`iteration ${info3.iterationCount}`);
5479
+ }
5480
+ if (info3.elapsed) {
5481
+ parts.push(`${info3.elapsed} elapsed`);
5482
+ }
5483
+ const displayCost = laneCount != null && laneCount > 1 && info3.laneTotalCost != null ? info3.laneTotalCost : info3.totalCost;
5484
+ if (displayCost != null) {
5485
+ parts.push(`${formatCost3(displayCost)} spent`);
5486
+ }
5487
+ const headerLine = parts.join(" | ");
5488
+ let phaseLine = "";
5489
+ if (info3.phase) {
5490
+ phaseLine = `Phase: ${info3.phase}`;
5491
+ if (info3.acProgress) {
5492
+ phaseLine += ` \u2192 AC ${info3.acProgress}`;
5493
+ }
5494
+ if (info3.currentCommand) {
5495
+ phaseLine += ` (${info3.currentCommand})`;
5496
+ }
5497
+ }
5498
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
5499
+ /* @__PURE__ */ jsx8(Text8, { children: headerLine }),
5500
+ /* @__PURE__ */ jsx8(Separator, {}),
5501
+ /* @__PURE__ */ jsx8(Text8, { children: `Story: ${info3.storyKey || "(waiting)"}` }),
5502
+ phaseLine && /* @__PURE__ */ jsx8(Text8, { children: phaseLine })
5503
+ ] });
5504
+ }
5505
+ function StoryBreakdown({ stories, sprintInfo }) {
5506
+ if (stories.length === 0) return null;
5507
+ const done = [];
5508
+ const inProgress = [];
5509
+ const pending = [];
5510
+ const failed = [];
5511
+ const blocked = [];
5512
+ for (const s of stories) {
5513
+ switch (s.status) {
5514
+ case "done":
5515
+ done.push(s);
5516
+ break;
5517
+ case "in-progress":
5518
+ inProgress.push(s);
5519
+ break;
5520
+ case "pending":
5521
+ pending.push(s);
5522
+ break;
5523
+ case "failed":
5524
+ failed.push(s);
5525
+ break;
5526
+ case "blocked":
5527
+ blocked.push(s);
5528
+ break;
5529
+ }
5530
+ }
5531
+ const lines = [];
5532
+ if (done.length > 0) {
5533
+ const doneItems = done.map((s) => {
5534
+ let item = `${shortKey(s.key)} \u2713`;
5535
+ if (s.costByDriver && Object.keys(s.costByDriver).length > 0) {
5536
+ const costParts = Object.keys(s.costByDriver).sort().map(
5537
+ (driver) => `${driver} ${formatCost3(s.costByDriver[driver])}`
5538
+ );
5539
+ item += ` ${costParts.join(", ")}`;
5540
+ }
5541
+ return item;
5542
+ }).join(" ");
5543
+ lines.push(
5544
+ /* @__PURE__ */ jsxs8(Text8, { children: [
5545
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: "Done: " }),
5546
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: doneItems })
5547
+ ] }, "done")
5548
+ );
5549
+ }
5550
+ if (inProgress.length > 0) {
5551
+ for (const s of inProgress) {
5552
+ let thisText = `${shortKey(s.key)} \u25C6`;
5553
+ if (sprintInfo && sprintInfo.storyKey && shortKey(s.key) === shortKey(sprintInfo.storyKey)) {
5554
+ if (sprintInfo.phase) thisText += ` ${sprintInfo.phase}`;
5555
+ if (sprintInfo.acProgress) thisText += ` (${sprintInfo.acProgress} ACs)`;
5556
+ }
5557
+ lines.push(
5558
+ /* @__PURE__ */ jsxs8(Text8, { children: [
5559
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "This: " }),
5560
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: thisText })
5561
+ ] }, `this-${s.key}`)
5562
+ );
5563
+ }
5564
+ }
5565
+ if (pending.length > 0) {
5566
+ lines.push(
5567
+ /* @__PURE__ */ jsxs8(Text8, { children: [
5568
+ /* @__PURE__ */ jsx8(Text8, { children: "Next: " }),
5569
+ /* @__PURE__ */ jsx8(Text8, { children: shortKey(pending[0].key) }),
5570
+ pending.length > 1 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` (+${pending.length - 1} more)` })
5571
+ ] }, "next")
5572
+ );
5573
+ }
5574
+ if (blocked.length > 0) {
5575
+ const blockedItems = blocked.map((s) => {
5576
+ let item = `${shortKey(s.key)} \u2715`;
5577
+ if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
5578
+ return item;
5579
+ }).join(" ");
5580
+ lines.push(
5581
+ /* @__PURE__ */ jsxs8(Text8, { children: [
5582
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Blocked: " }),
5583
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: blockedItems })
5584
+ ] }, "blocked")
5585
+ );
5586
+ }
5587
+ if (failed.length > 0) {
5588
+ const failedItems = failed.map((s) => {
5589
+ let item = `${shortKey(s.key)} \u2717`;
5590
+ if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
5591
+ return item;
5592
+ }).join(" ");
5593
+ lines.push(
5594
+ /* @__PURE__ */ jsxs8(Text8, { children: [
5595
+ /* @__PURE__ */ jsx8(Text8, { color: "red", children: "Failed: " }),
5596
+ /* @__PURE__ */ jsx8(Text8, { color: "red", children: failedItems })
5597
+ ] }, "failed")
5598
+ );
5599
+ }
5600
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: lines });
5601
+ }
5602
+
5603
+ // src/lib/ink-renderer.tsx
5604
+ import { jsx as jsx9 } from "react/jsx-runtime";
5605
+ var noopHandle = {
5606
+ update(_event, _driverName, _laneId) {
5607
+ },
5608
+ updateSprintState() {
5609
+ },
5610
+ updateStories() {
5611
+ },
5612
+ addMessage() {
5613
+ },
5614
+ updateWorkflowState() {
5615
+ },
5616
+ processLaneEvent() {
5617
+ },
5618
+ updateMergeState() {
5619
+ },
5620
+ cleanup() {
5621
+ }
5622
+ };
5623
+ var MAX_COMPLETED_TOOLS = 50;
5624
+ function startRenderer(options) {
5625
+ if (options?.quiet || !process.stdout.isTTY && !options?._forceTTY) {
5626
+ return noopHandle;
5627
+ }
5628
+ let state = {
5629
+ sprintInfo: options?.sprintState ?? null,
5630
+ stories: [],
5631
+ messages: [],
5632
+ completedTools: [],
5633
+ activeTool: null,
5634
+ activeToolArgs: "",
5635
+ lastThought: null,
5636
+ retryInfo: null,
5637
+ workflowFlow: [],
5638
+ currentTaskName: null,
5639
+ taskStates: {},
5640
+ taskMeta: {},
5641
+ activeDriverName: null,
5642
+ driverCosts: {},
5643
+ activeLaneId: null,
5644
+ laneCount: 0
5645
+ };
5646
+ const laneStates = /* @__PURE__ */ new Map();
5647
+ let pinnedLane = false;
5648
+ let currentStoryCosts = {};
5649
+ let lastStoryKey = state.sprintInfo?.storyKey ?? null;
5650
+ const pendingStoryCosts = /* @__PURE__ */ new Map();
5651
+ let cleaned = false;
5652
+ const inkInstance = inkRender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }), {
5653
+ exitOnCtrlC: false,
5654
+ patchConsole: !options?._forceTTY,
5655
+ maxFps: 15
5656
+ });
5657
+ function rerender() {
5658
+ if (!cleaned) {
5659
+ state = { ...state };
5660
+ inkInstance.rerender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }));
5661
+ }
5662
+ }
5663
+ function onSigint() {
5664
+ cleanupFull();
5665
+ process.kill(process.pid, "SIGINT");
5666
+ }
5667
+ function onSigterm() {
5668
+ cleanupFull();
5669
+ process.kill(process.pid, "SIGTERM");
5670
+ }
5671
+ process.on("SIGINT", onSigint);
5672
+ process.on("SIGTERM", onSigterm);
5673
+ function promoteActiveTool(clearActive, targetState) {
5674
+ const s = targetState ?? state;
5675
+ if (!s.activeTool) return;
5676
+ const entry = {
5677
+ name: s.activeTool.name,
5678
+ args: s.activeToolArgs,
5679
+ driver: s.activeDriverName ?? void 0
5680
+ };
5681
+ const updated = [...s.completedTools, entry];
5682
+ s.completedTools = updated.length > MAX_COMPLETED_TOOLS ? updated.slice(updated.length - MAX_COMPLETED_TOOLS) : updated;
5683
+ if (clearActive) {
5684
+ s.activeTool = null;
5685
+ s.activeToolArgs = "";
5686
+ s.activeDriverName = null;
5687
+ }
5688
+ }
5689
+ function copyLaneToDisplay(laneId) {
5690
+ const ls = laneStates.get(laneId);
5691
+ if (!ls) return;
5692
+ state.completedTools = [...ls.completedTools];
5693
+ state.activeTool = ls.activeTool ? { ...ls.activeTool } : null;
5694
+ state.activeToolArgs = ls.activeToolArgs;
5695
+ state.lastThought = ls.lastThought;
5696
+ state.retryInfo = ls.retryInfo ? { ...ls.retryInfo } : null;
5697
+ state.activeDriverName = ls.activeDriverName;
5698
+ state.activeLaneId = laneId;
5699
+ }
5700
+ function getOrCreateLaneState(laneId) {
5701
+ let ls = laneStates.get(laneId);
5702
+ if (!ls) {
5703
+ ls = {
5704
+ completedTools: [],
5705
+ activeTool: null,
5706
+ activeToolArgs: "",
5707
+ lastThought: null,
5708
+ retryInfo: null,
5709
+ activeDriverName: null,
5710
+ status: "active",
5711
+ lastActivityTime: Date.now()
5712
+ };
5713
+ laneStates.set(laneId, ls);
5714
+ }
5715
+ return ls;
5716
+ }
5717
+ function getActiveLaneIds() {
5718
+ const ids = [];
5719
+ for (const [id, ls] of laneStates) {
5720
+ if (ls.status === "active") ids.push(id);
5721
+ }
5722
+ return ids;
5723
+ }
5724
+ function update(event, driverName, laneId) {
5725
+ if (cleaned) return;
5726
+ if (laneId) {
5727
+ const ls = getOrCreateLaneState(laneId);
5728
+ ls.lastActivityTime = Date.now();
5729
+ switch (event.type) {
5730
+ case "tool-start":
5731
+ promoteActiveTool(false, ls);
5732
+ ls.activeTool = { name: event.name };
5733
+ ls.activeToolArgs = "";
5734
+ ls.activeDriverName = driverName ?? null;
5735
+ ls.lastThought = null;
5736
+ ls.retryInfo = null;
5737
+ break;
5738
+ case "tool-input":
5739
+ ls.activeToolArgs += event.partial;
5740
+ return;
5741
+ case "tool-complete":
5742
+ if (ls.activeTool) {
5743
+ if (["Agent", "Skill"].includes(ls.activeTool.name)) break;
5744
+ promoteActiveTool(true, ls);
5745
+ }
5746
+ break;
5747
+ case "text":
5748
+ ls.lastThought = event.text;
5749
+ ls.retryInfo = null;
5750
+ break;
5751
+ case "retry":
5752
+ ls.retryInfo = { attempt: event.attempt, delay: event.delay };
5753
+ break;
5754
+ case "result":
5755
+ if (event.cost > 0 && state.sprintInfo) {
5756
+ state.sprintInfo = {
5757
+ ...state.sprintInfo,
5758
+ totalCost: (state.sprintInfo.totalCost ?? 0) + event.cost
5759
+ };
5760
+ }
5761
+ if (event.cost > 0 && driverName) {
5762
+ state.driverCosts = {
5763
+ ...state.driverCosts,
5764
+ [driverName]: (state.driverCosts[driverName] ?? 0) + event.cost
5765
+ };
5766
+ currentStoryCosts = {
5767
+ ...currentStoryCosts,
5768
+ [driverName]: (currentStoryCosts[driverName] ?? 0) + event.cost
5769
+ };
5770
+ }
5771
+ break;
5772
+ }
5773
+ if (!pinnedLane && state.activeLaneId !== laneId) {
5774
+ state.activeLaneId = laneId;
5775
+ }
5776
+ if (pinnedLane && state.activeLaneId !== laneId) {
5777
+ pinnedLane = false;
5778
+ }
5779
+ if (state.activeLaneId === laneId) {
5780
+ copyLaneToDisplay(laneId);
5781
+ }
5782
+ state.laneCount = laneStates.size;
5783
+ rerender();
5784
+ return;
5785
+ }
5786
+ switch (event.type) {
5787
+ case "tool-start":
5788
+ promoteActiveTool(false);
5789
+ state.activeTool = { name: event.name };
5790
+ state.activeToolArgs = "";
5791
+ state.activeDriverName = driverName ?? null;
5792
+ state.lastThought = null;
5793
+ state.retryInfo = null;
5794
+ break;
5795
+ case "tool-input":
5796
+ state.activeToolArgs += event.partial;
5797
+ return;
5798
+ // Skip rerender
5799
+ case "tool-complete":
5800
+ if (state.activeTool) {
5801
+ if (["Agent", "Skill"].includes(state.activeTool.name)) break;
5802
+ promoteActiveTool(true);
5803
+ }
5804
+ break;
5805
+ case "text":
5806
+ state.lastThought = event.text;
5807
+ state.retryInfo = null;
5808
+ break;
5809
+ case "retry":
5810
+ state.retryInfo = { attempt: event.attempt, delay: event.delay };
5811
+ break;
5812
+ case "result":
5813
+ if (event.cost > 0 && state.sprintInfo) {
5814
+ state.sprintInfo = {
5815
+ ...state.sprintInfo,
5816
+ totalCost: (state.sprintInfo.totalCost ?? 0) + event.cost
5817
+ };
5818
+ }
5819
+ if (event.cost > 0 && driverName) {
5820
+ state.driverCosts = {
5821
+ ...state.driverCosts,
5822
+ [driverName]: (state.driverCosts[driverName] ?? 0) + event.cost
5823
+ };
5824
+ currentStoryCosts = {
5825
+ ...currentStoryCosts,
5826
+ [driverName]: (currentStoryCosts[driverName] ?? 0) + event.cost
5827
+ };
5828
+ }
5829
+ break;
5830
+ }
5831
+ rerender();
5832
+ }
5833
+ function processLaneEvent(event) {
5834
+ if (cleaned) return;
5835
+ switch (event.type) {
5836
+ case "lane-started": {
5837
+ const ls = getOrCreateLaneState(event.epicId);
5838
+ ls.status = "active";
5839
+ ls.lastActivityTime = Date.now();
5840
+ if (state.activeLaneId == null) {
5841
+ state.activeLaneId = event.epicId;
5842
+ copyLaneToDisplay(event.epicId);
5843
+ }
5844
+ state.laneCount = laneStates.size;
5845
+ break;
5846
+ }
5847
+ case "lane-completed": {
5848
+ const ls = laneStates.get(event.epicId);
5849
+ if (ls) {
5850
+ ls.status = "completed";
5851
+ }
5852
+ if (state.summaryBar) {
5853
+ const epicId = event.epicId;
5854
+ const newDone = [...state.summaryBar.doneStories];
5855
+ if (!newDone.includes(epicId)) newDone.push(epicId);
5856
+ const newPending = state.summaryBar.pendingEpics.filter((e) => e !== epicId);
5857
+ state.summaryBar = {
5858
+ ...state.summaryBar,
5859
+ doneStories: newDone,
5860
+ pendingEpics: newPending
5861
+ };
5862
+ }
5863
+ state.laneCount = laneStates.size;
5864
+ break;
5865
+ }
5866
+ case "lane-failed": {
5867
+ const ls = laneStates.get(event.epicId);
5868
+ if (ls) {
5869
+ ls.status = "failed";
5870
+ } else {
5871
+ const newLs = getOrCreateLaneState(event.epicId);
5872
+ newLs.status = "failed";
5873
+ }
5874
+ if (state.activeLaneId === event.epicId) {
5875
+ const activeIds = getActiveLaneIds();
5876
+ if (activeIds.length > 0) {
5877
+ state.activeLaneId = activeIds[0];
5878
+ copyLaneToDisplay(activeIds[0]);
5879
+ }
5880
+ }
5881
+ state.laneCount = laneStates.size;
5882
+ break;
5883
+ }
5884
+ case "epic-queued": {
5885
+ if (state.summaryBar) {
5886
+ if (!state.summaryBar.pendingEpics.includes(event.epicId)) {
5887
+ state.summaryBar = {
5888
+ ...state.summaryBar,
5889
+ pendingEpics: [...state.summaryBar.pendingEpics, event.epicId]
5890
+ };
5891
+ }
5892
+ }
5893
+ break;
5894
+ }
5895
+ }
5896
+ rerender();
5897
+ }
5898
+ function updateMergeState(mergeState) {
5899
+ if (cleaned) return;
5900
+ state.mergeState = mergeState;
5901
+ if (state.summaryBar && !mergeState) {
5902
+ state.summaryBar = { ...state.summaryBar, mergingEpic: null };
5903
+ }
5904
+ if (state.summaryBar && mergeState) {
5905
+ const mergingStatus = mergeState.outcome === "clean" || mergeState.outcome === "resolved" ? "complete" : mergeState.outcome === "escalated" ? "complete" : "in-progress";
5906
+ state.summaryBar = {
5907
+ ...state.summaryBar,
5908
+ mergingEpic: {
5909
+ epicId: mergeState.epicId,
5910
+ status: mergingStatus,
5911
+ conflictCount: mergeState.conflictCount
5912
+ }
5913
+ };
5914
+ }
5915
+ rerender();
5916
+ }
5917
+ function cycleLane() {
5918
+ if (cleaned) return;
5919
+ const activeIds = getActiveLaneIds();
5920
+ if (activeIds.length <= 1) return;
5921
+ const currentIndex = state.activeLaneId ? activeIds.indexOf(state.activeLaneId) : -1;
5922
+ const nextIndex = (currentIndex + 1) % activeIds.length;
5923
+ state.activeLaneId = activeIds[nextIndex];
5924
+ copyLaneToDisplay(activeIds[nextIndex]);
5925
+ pinnedLane = true;
5926
+ rerender();
5927
+ }
5928
+ function cleanupFull() {
5929
+ if (cleaned) return;
5930
+ cleaned = true;
5931
+ try {
5932
+ inkInstance.unmount();
5933
+ } catch {
5934
+ }
5935
+ try {
5936
+ inkInstance.cleanup();
5937
+ } catch {
5938
+ }
5939
+ process.removeListener("SIGINT", onSigint);
5940
+ process.removeListener("SIGTERM", onSigterm);
5941
+ }
5942
+ function updateSprintState(sprintState) {
5943
+ if (cleaned) return;
5944
+ if (sprintState && state.sprintInfo) {
5945
+ state.sprintInfo = {
5946
+ ...sprintState,
5947
+ totalCost: sprintState.totalCost ?? state.sprintInfo.totalCost,
5948
+ acProgress: sprintState.acProgress ?? state.sprintInfo.acProgress,
5949
+ currentCommand: sprintState.currentCommand ?? state.sprintInfo.currentCommand
5950
+ };
5951
+ } else {
5952
+ state.sprintInfo = sprintState ?? null;
5953
+ }
5954
+ const newKey = state.sprintInfo?.storyKey ?? null;
5955
+ if (newKey && lastStoryKey && newKey !== lastStoryKey) {
5956
+ if (Object.keys(currentStoryCosts).length > 0) {
5957
+ pendingStoryCosts.set(lastStoryKey, { ...currentStoryCosts });
5958
+ }
5959
+ currentStoryCosts = {};
5960
+ lastStoryKey = newKey;
5961
+ } else if (newKey && !lastStoryKey) {
5962
+ lastStoryKey = newKey;
5963
+ }
5964
+ rerender();
5965
+ }
5966
+ function updateStories(stories) {
5967
+ if (cleaned) return;
5968
+ const currentKey = state.sprintInfo?.storyKey ?? null;
5969
+ const hasCurrentCosts = Object.keys(currentStoryCosts).length > 0;
5970
+ const updatedStories = stories.map((s) => {
5971
+ if (s.status !== "done" || s.costByDriver) return s;
5972
+ const pending = pendingStoryCosts.get(s.key);
5973
+ if (pending) {
5974
+ pendingStoryCosts.delete(s.key);
5975
+ return { ...s, costByDriver: pending };
5976
+ }
5977
+ if (hasCurrentCosts && s.key === (lastStoryKey ?? currentKey)) {
5978
+ const snap = { ...currentStoryCosts };
5979
+ currentStoryCosts = {};
5980
+ return { ...s, costByDriver: snap };
5981
+ }
5982
+ return s;
5983
+ });
5984
+ if (currentKey && currentKey !== lastStoryKey) {
5985
+ if (lastStoryKey && Object.keys(currentStoryCosts).length > 0) {
5986
+ pendingStoryCosts.set(lastStoryKey, { ...currentStoryCosts });
5987
+ }
5988
+ currentStoryCosts = {};
5989
+ lastStoryKey = currentKey;
5990
+ } else if (currentKey && !lastStoryKey) {
5991
+ lastStoryKey = currentKey;
5992
+ }
5993
+ state.stories = updatedStories;
5994
+ rerender();
5995
+ }
5996
+ function addMessage(msg) {
5997
+ if (cleaned) return;
5998
+ state.messages = [...state.messages, msg];
5999
+ rerender();
6000
+ }
6001
+ function updateWorkflowState(flow, currentTask, taskStates, taskMeta) {
6002
+ if (cleaned) return;
6003
+ state.workflowFlow = flow;
6004
+ state.currentTaskName = currentTask;
6005
+ state.taskStates = { ...taskStates };
6006
+ state.taskMeta = taskMeta ? { ...taskMeta } : state.taskMeta;
6007
+ rerender();
6008
+ }
6009
+ return {
6010
+ update,
6011
+ updateSprintState,
6012
+ updateStories,
6013
+ addMessage,
6014
+ updateWorkflowState,
6015
+ processLaneEvent,
6016
+ updateMergeState,
6017
+ cleanup: cleanupFull,
6018
+ _getState: () => state,
6019
+ _getLaneStates: () => laneStates,
6020
+ _cycleLane: () => cycleLane()
6021
+ };
6022
+ }
6023
+
4860
6024
  // src/commands/run.ts
4861
6025
  function resolvePluginDir() {
4862
6026
  return join15(process.cwd(), ".claude");
@@ -4996,6 +6160,80 @@ function registerRunCommand(program) {
4996
6160
  info("Resuming after circuit breaker \u2014 previous findings preserved", outputOpts);
4997
6161
  }
4998
6162
  }
6163
+ const renderer = startRenderer({
6164
+ quiet: !!options.quiet || isJson,
6165
+ sprintState: {
6166
+ storyKey: "",
6167
+ phase: "executing",
6168
+ done: counts.done,
6169
+ total: counts.total,
6170
+ totalCost: 0
6171
+ }
6172
+ });
6173
+ let totalCostUsd = 0;
6174
+ let storiesDone = counts.done;
6175
+ const taskStates = {};
6176
+ const taskMeta = {};
6177
+ for (const [tn, task] of Object.entries(parsedWorkflow.tasks)) {
6178
+ taskStates[tn] = "pending";
6179
+ taskMeta[tn] = { driver: task.driver ?? "claude-code" };
6180
+ }
6181
+ const storyEntries = [];
6182
+ for (const [key, status] of Object.entries(statuses)) {
6183
+ if (key.startsWith("epic-")) continue;
6184
+ if (status === "done") storyEntries.push({ key, status: "done" });
6185
+ else if (status === "in-progress") storyEntries.push({ key, status: "in-progress" });
6186
+ else if (status === "backlog" || status === "ready-for-dev") storyEntries.push({ key, status: "pending" });
6187
+ else if (status === "failed") storyEntries.push({ key, status: "failed" });
6188
+ }
6189
+ renderer.updateStories(storyEntries);
6190
+ const onEvent = (event) => {
6191
+ if (event.type === "stream-event" && event.streamEvent) {
6192
+ renderer.update(event.streamEvent, event.driverName);
6193
+ }
6194
+ if (event.type === "dispatch-start") {
6195
+ renderer.updateSprintState({
6196
+ storyKey: event.storyKey,
6197
+ phase: event.taskName,
6198
+ done: storiesDone,
6199
+ total: counts.total,
6200
+ totalCost: totalCostUsd
6201
+ });
6202
+ taskStates[event.taskName] = "active";
6203
+ renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
6204
+ const idx = storyEntries.findIndex((s) => s.key === event.storyKey);
6205
+ if (idx >= 0 && storyEntries[idx].status === "pending") {
6206
+ storyEntries[idx] = { ...storyEntries[idx], status: "in-progress" };
6207
+ renderer.updateStories([...storyEntries]);
6208
+ }
6209
+ }
6210
+ if (event.type === "dispatch-end") {
6211
+ totalCostUsd += event.costUsd ?? 0;
6212
+ taskStates[event.taskName] = "done";
6213
+ taskMeta[event.taskName] = {
6214
+ ...taskMeta[event.taskName],
6215
+ costUsd: (taskMeta[event.taskName]?.costUsd ?? 0) + (event.costUsd ?? 0),
6216
+ elapsedMs: (taskMeta[event.taskName]?.elapsedMs ?? 0) + (event.elapsedMs ?? 0)
6217
+ };
6218
+ renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
6219
+ renderer.updateSprintState({
6220
+ storyKey: event.storyKey,
6221
+ phase: event.taskName,
6222
+ done: storiesDone,
6223
+ total: counts.total,
6224
+ totalCost: totalCostUsd
6225
+ });
6226
+ }
6227
+ if (event.type === "dispatch-error") {
6228
+ taskStates[event.taskName] = "failed";
6229
+ renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
6230
+ renderer.addMessage({
6231
+ type: "fail",
6232
+ key: event.storyKey,
6233
+ message: `[${event.taskName}] ${event.error?.message ?? "unknown error"}`
6234
+ });
6235
+ }
6236
+ };
4999
6237
  const config = {
5000
6238
  workflow: parsedWorkflow,
5001
6239
  agents,
@@ -5003,7 +6241,8 @@ function registerRunCommand(program) {
5003
6241
  issuesPath: join15(projectDir, ".codeharness", "issues.yaml"),
5004
6242
  runId: `run-${Date.now()}`,
5005
6243
  projectDir,
5006
- maxIterations
6244
+ maxIterations,
6245
+ onEvent
5007
6246
  };
5008
6247
  const execution = parsedWorkflow.execution;
5009
6248
  const isParallel = execution?.epic_strategy === "parallel";
@@ -5081,6 +6320,7 @@ function registerRunCommand(program) {
5081
6320
  } else {
5082
6321
  try {
5083
6322
  const result = await executeWorkflow(config);
6323
+ renderer.cleanup();
5084
6324
  if (result.success) {
5085
6325
  ok(`Workflow completed \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed in ${formatElapsed(result.durationMs)}`, outputOpts);
5086
6326
  } else {
@@ -5091,6 +6331,7 @@ function registerRunCommand(program) {
5091
6331
  process.exitCode = 1;
5092
6332
  }
5093
6333
  } catch (err) {
6334
+ renderer.cleanup();
5094
6335
  const msg = err instanceof Error ? err.message : String(err);
5095
6336
  fail(`Workflow engine error: ${msg}`, outputOpts);
5096
6337
  process.exitCode = 1;
@@ -9177,7 +10418,7 @@ async function handleDockerCheck(isJson) {
9177
10418
  }
9178
10419
  }
9179
10420
  }
9180
- function formatElapsed2(ms) {
10421
+ function formatElapsed3(ms) {
9181
10422
  const s = Math.floor(ms / 1e3);
9182
10423
  const h = Math.floor(s / 3600);
9183
10424
  const m = Math.floor(s % 3600 / 60);
@@ -9197,7 +10438,7 @@ function printWorkflowState() {
9197
10438
  console.log(` Tasks completed: ${state.tasks_completed.length}`);
9198
10439
  if (state.phase === "executing" && state.started) {
9199
10440
  const elapsed = Date.now() - Date.parse(state.started);
9200
- console.log(` Elapsed: ${formatElapsed2(elapsed)}`);
10441
+ console.log(` Elapsed: ${formatElapsed3(elapsed)}`);
9201
10442
  }
9202
10443
  if (state.evaluator_scores.length > 0) {
9203
10444
  const latest = state.evaluator_scores[state.evaluator_scores.length - 1];
@@ -9222,7 +10463,7 @@ function getWorkflowStateData() {
9222
10463
  };
9223
10464
  if (state.phase === "executing" && state.started) {
9224
10465
  data.elapsed_ms = Date.now() - Date.parse(state.started);
9225
- data.elapsed = formatElapsed2(data.elapsed_ms);
10466
+ data.elapsed = formatElapsed3(data.elapsed_ms);
9226
10467
  }
9227
10468
  return data;
9228
10469
  }
@@ -9874,7 +11115,7 @@ function registerTeardownCommand(program) {
9874
11115
  } else if (otlpMode === "remote-routed") {
9875
11116
  if (!options.keepDocker) {
9876
11117
  try {
9877
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-32GRDQOK.js");
11118
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-QJGQIPTO.js");
9878
11119
  stopCollectorOnly2();
9879
11120
  result.docker.stopped = true;
9880
11121
  if (!isJson) {
@@ -9906,7 +11147,7 @@ function registerTeardownCommand(program) {
9906
11147
  info("Shared stack: kept running (other projects may use it)");
9907
11148
  }
9908
11149
  } else if (isLegacyStack) {
9909
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-32GRDQOK.js");
11150
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-QJGQIPTO.js");
9910
11151
  let stackRunning = false;
9911
11152
  try {
9912
11153
  stackRunning = isStackRunning2(composeFile);
@@ -12784,7 +14025,7 @@ function registerDriversCommand(program) {
12784
14025
  }
12785
14026
 
12786
14027
  // src/index.ts
12787
- var VERSION = true ? "0.29.0" : "0.0.0-dev";
14028
+ var VERSION = true ? "0.29.2" : "0.0.0-dev";
12788
14029
  function createProgram() {
12789
14030
  const program = new Command();
12790
14031
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");