codeharness 0.29.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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-EJ6GZH4Z.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,48 @@ 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
+ const onEvent = (event) => {
6174
+ if (event.type === "stream-event" && event.streamEvent) {
6175
+ renderer.update(event.streamEvent, event.driverName);
6176
+ }
6177
+ if (event.type === "dispatch-start") {
6178
+ renderer.updateSprintState({
6179
+ storyKey: event.storyKey,
6180
+ phase: `${event.taskName}`,
6181
+ done: counts.done,
6182
+ total: counts.total
6183
+ });
6184
+ renderer.updateWorkflowState(
6185
+ parsedWorkflow.flow,
6186
+ event.taskName,
6187
+ { [event.taskName]: "active" }
6188
+ );
6189
+ }
6190
+ if (event.type === "dispatch-end") {
6191
+ renderer.updateWorkflowState(
6192
+ parsedWorkflow.flow,
6193
+ event.taskName,
6194
+ { [event.taskName]: "done" }
6195
+ );
6196
+ }
6197
+ if (event.type === "dispatch-error") {
6198
+ renderer.addMessage({
6199
+ type: "fail",
6200
+ key: event.storyKey,
6201
+ message: `[${event.taskName}] ${event.error?.message ?? "unknown error"}`
6202
+ });
6203
+ }
6204
+ };
4999
6205
  const config = {
5000
6206
  workflow: parsedWorkflow,
5001
6207
  agents,
@@ -5003,7 +6209,8 @@ function registerRunCommand(program) {
5003
6209
  issuesPath: join15(projectDir, ".codeharness", "issues.yaml"),
5004
6210
  runId: `run-${Date.now()}`,
5005
6211
  projectDir,
5006
- maxIterations
6212
+ maxIterations,
6213
+ onEvent
5007
6214
  };
5008
6215
  const execution = parsedWorkflow.execution;
5009
6216
  const isParallel = execution?.epic_strategy === "parallel";
@@ -5081,6 +6288,7 @@ function registerRunCommand(program) {
5081
6288
  } else {
5082
6289
  try {
5083
6290
  const result = await executeWorkflow(config);
6291
+ renderer.cleanup();
5084
6292
  if (result.success) {
5085
6293
  ok(`Workflow completed \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed in ${formatElapsed(result.durationMs)}`, outputOpts);
5086
6294
  } else {
@@ -5091,6 +6299,7 @@ function registerRunCommand(program) {
5091
6299
  process.exitCode = 1;
5092
6300
  }
5093
6301
  } catch (err) {
6302
+ renderer.cleanup();
5094
6303
  const msg = err instanceof Error ? err.message : String(err);
5095
6304
  fail(`Workflow engine error: ${msg}`, outputOpts);
5096
6305
  process.exitCode = 1;
@@ -9177,7 +10386,7 @@ async function handleDockerCheck(isJson) {
9177
10386
  }
9178
10387
  }
9179
10388
  }
9180
- function formatElapsed2(ms) {
10389
+ function formatElapsed3(ms) {
9181
10390
  const s = Math.floor(ms / 1e3);
9182
10391
  const h = Math.floor(s / 3600);
9183
10392
  const m = Math.floor(s % 3600 / 60);
@@ -9197,7 +10406,7 @@ function printWorkflowState() {
9197
10406
  console.log(` Tasks completed: ${state.tasks_completed.length}`);
9198
10407
  if (state.phase === "executing" && state.started) {
9199
10408
  const elapsed = Date.now() - Date.parse(state.started);
9200
- console.log(` Elapsed: ${formatElapsed2(elapsed)}`);
10409
+ console.log(` Elapsed: ${formatElapsed3(elapsed)}`);
9201
10410
  }
9202
10411
  if (state.evaluator_scores.length > 0) {
9203
10412
  const latest = state.evaluator_scores[state.evaluator_scores.length - 1];
@@ -9222,7 +10431,7 @@ function getWorkflowStateData() {
9222
10431
  };
9223
10432
  if (state.phase === "executing" && state.started) {
9224
10433
  data.elapsed_ms = Date.now() - Date.parse(state.started);
9225
- data.elapsed = formatElapsed2(data.elapsed_ms);
10434
+ data.elapsed = formatElapsed3(data.elapsed_ms);
9226
10435
  }
9227
10436
  return data;
9228
10437
  }
@@ -9874,7 +11083,7 @@ function registerTeardownCommand(program) {
9874
11083
  } else if (otlpMode === "remote-routed") {
9875
11084
  if (!options.keepDocker) {
9876
11085
  try {
9877
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-32GRDQOK.js");
11086
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-JBXHIWZS.js");
9878
11087
  stopCollectorOnly2();
9879
11088
  result.docker.stopped = true;
9880
11089
  if (!isJson) {
@@ -9906,7 +11115,7 @@ function registerTeardownCommand(program) {
9906
11115
  info("Shared stack: kept running (other projects may use it)");
9907
11116
  }
9908
11117
  } else if (isLegacyStack) {
9909
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-32GRDQOK.js");
11118
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-JBXHIWZS.js");
9910
11119
  let stackRunning = false;
9911
11120
  try {
9912
11121
  stackRunning = isStackRunning2(composeFile);
@@ -12784,7 +13993,7 @@ function registerDriversCommand(program) {
12784
13993
  }
12785
13994
 
12786
13995
  // src/index.ts
12787
- var VERSION = true ? "0.29.0" : "0.0.0-dev";
13996
+ var VERSION = true ? "0.29.1" : "0.0.0-dev";
12788
13997
  function createProgram() {
12789
13998
  const program = new Command();
12790
13999
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");