codeharness 0.29.4 → 0.30.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.
@@ -2895,7 +2895,7 @@ function generateDockerfileTemplate(projectDir, stackOrDetections) {
2895
2895
  }
2896
2896
 
2897
2897
  // src/modules/infra/init-project.ts
2898
- var HARNESS_VERSION = true ? "0.29.4" : "0.0.0-dev";
2898
+ var HARNESS_VERSION = true ? "0.30.1" : "0.0.0-dev";
2899
2899
  function failResult(opts, error) {
2900
2900
  return {
2901
2901
  status: "fail",
@@ -16,7 +16,7 @@ import {
16
16
  stopCollectorOnly,
17
17
  stopSharedStack,
18
18
  stopStack
19
- } from "./chunk-KVKEUNEB.js";
19
+ } from "./chunk-QLY7NJIB.js";
20
20
  export {
21
21
  checkRemoteEndpoint,
22
22
  cleanupOrphanedContainers,
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  validateDockerfile,
41
41
  warn,
42
42
  writeState
43
- } from "./chunk-KVKEUNEB.js";
43
+ } from "./chunk-QLY7NJIB.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { Command } from "commander";
@@ -3882,75 +3882,165 @@ async function executeWorkflow(config) {
3882
3882
  let halted = false;
3883
3883
  let lastOutputContract = null;
3884
3884
  let accumulatedCostUsd = 0;
3885
- for (const step of config.workflow.storyFlow) {
3886
- if (halted) break;
3887
- if (config.abortSignal?.aborted) {
3888
- info("Execution interrupted \u2014 saving state");
3889
- state = { ...state, phase: "interrupted" };
3890
- writeWorkflowState(state, projectDir);
3891
- halted = true;
3892
- break;
3893
- }
3894
- if (isLoopBlock(step)) {
3895
- const loopResult = await executeLoopBlock(step, state, config, workItems, lastOutputContract);
3896
- state = loopResult.state;
3897
- errors.push(...loopResult.errors);
3898
- tasksCompleted += loopResult.tasksCompleted;
3899
- lastOutputContract = loopResult.lastContract;
3900
- for (const item of workItems) {
3901
- processedStories.add(item.key);
3885
+ if (config.storyPipeline) {
3886
+ const preTasks = [];
3887
+ let loopIdx = -1;
3888
+ for (let i = 0; i < config.workflow.storyFlow.length; i++) {
3889
+ const s = config.workflow.storyFlow[i];
3890
+ if (isLoopBlock(s)) {
3891
+ loopIdx = i;
3892
+ break;
3902
3893
  }
3903
- if (loopResult.halted) {
3894
+ if (typeof s === "string") preTasks.push(s);
3895
+ }
3896
+ for (const item of workItems) {
3897
+ if (halted || config.abortSignal?.aborted) {
3898
+ if (config.abortSignal?.aborted) {
3899
+ info("Execution interrupted \u2014 saving state");
3900
+ state = { ...state, phase: "interrupted" };
3901
+ writeWorkflowState(state, projectDir);
3902
+ }
3904
3903
  halted = true;
3904
+ break;
3905
3905
  }
3906
- if (state.phase === "max-iterations" || state.phase === "circuit-breaker") {
3907
- halted = true;
3906
+ processedStories.add(item.key);
3907
+ for (const taskName of preTasks) {
3908
+ if (halted || config.abortSignal?.aborted) {
3909
+ halted = true;
3910
+ break;
3911
+ }
3912
+ const task = config.workflow.tasks[taskName];
3913
+ if (!task || task.agent === null) continue;
3914
+ const definition = config.agents[task.agent];
3915
+ if (!definition) {
3916
+ warn(`workflow-engine: agent "${task.agent}" not found for "${taskName}"`);
3917
+ continue;
3918
+ }
3919
+ if (isTaskCompleted(state, taskName, item.key)) continue;
3920
+ try {
3921
+ const dr = await dispatchTaskWithResult(task, taskName, item.key, definition, state, config, void 0, lastOutputContract ?? void 0);
3922
+ state = dr.updatedState;
3923
+ lastOutputContract = dr.contract;
3924
+ propagateVerifyFlags(taskName, dr.contract, projectDir);
3925
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3926
+ tasksCompleted++;
3927
+ } catch (err) {
3928
+ const engineError = handleDispatchError(err, taskName, item.key);
3929
+ errors.push(engineError);
3930
+ if (config.onEvent) {
3931
+ config.onEvent({ type: "dispatch-error", taskName, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
3932
+ } else {
3933
+ warn(`[${taskName}] ${item.key} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
3934
+ }
3935
+ state = recordErrorInState(state, taskName, item.key, engineError);
3936
+ writeWorkflowState(state, projectDir);
3937
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
3938
+ halted = true;
3939
+ }
3940
+ break;
3941
+ }
3908
3942
  }
3909
- continue;
3910
3943
  }
3911
- const taskName = step;
3912
- const task = config.workflow.tasks[taskName];
3913
- if (!task) {
3914
- warn(`workflow-engine: task "${taskName}" not found in workflow tasks, skipping`);
3915
- continue;
3916
- }
3917
- if (task.agent === null) {
3918
- if (task.scope === "per-run") {
3919
- if (isTaskCompleted(state, taskName, PER_RUN_SENTINEL)) {
3920
- warn(`workflow-engine: skipping completed task ${taskName} for ${PER_RUN_SENTINEL}`);
3921
- continue;
3944
+ const remaining = loopIdx >= 0 ? config.workflow.storyFlow.slice(loopIdx) : [];
3945
+ for (const step of remaining) {
3946
+ if (halted || config.abortSignal?.aborted) {
3947
+ halted = true;
3948
+ break;
3949
+ }
3950
+ if (isLoopBlock(step)) {
3951
+ const loopResult = await executeLoopBlock(step, state, config, workItems, lastOutputContract);
3952
+ state = loopResult.state;
3953
+ errors.push(...loopResult.errors);
3954
+ tasksCompleted += loopResult.tasksCompleted;
3955
+ lastOutputContract = loopResult.lastContract;
3956
+ for (const item of workItems) processedStories.add(item.key);
3957
+ if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") {
3958
+ halted = true;
3922
3959
  }
3960
+ continue;
3961
+ }
3962
+ const taskName = step;
3963
+ const task = config.workflow.tasks[taskName];
3964
+ if (!task) continue;
3965
+ if (task.agent === null) continue;
3966
+ const definition = config.agents[task.agent];
3967
+ if (!definition) continue;
3968
+ if (task.scope === "per-run" || task.scope === "per-epic") {
3969
+ if (isTaskCompleted(state, taskName, PER_RUN_SENTINEL)) continue;
3923
3970
  try {
3924
- const nullResult = await executeNullTask(
3925
- task,
3926
- taskName,
3927
- PER_RUN_SENTINEL,
3928
- state,
3929
- config,
3930
- lastOutputContract ?? void 0,
3931
- accumulatedCostUsd
3932
- );
3933
- state = nullResult.updatedState;
3934
- lastOutputContract = nullResult.contract;
3971
+ const dr = await dispatchTaskWithResult(task, taskName, PER_RUN_SENTINEL, definition, state, config, void 0, lastOutputContract ?? void 0);
3972
+ state = dr.updatedState;
3973
+ lastOutputContract = dr.contract;
3935
3974
  tasksCompleted++;
3936
3975
  } catch (err) {
3937
- const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3976
+ const engineError = handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3938
3977
  errors.push(engineError);
3939
3978
  state = recordErrorInState(state, taskName, PER_RUN_SENTINEL, engineError);
3940
3979
  writeWorkflowState(state, projectDir);
3941
3980
  }
3942
3981
  } else {
3982
+ for (const item of workItems) {
3983
+ if (halted || config.abortSignal?.aborted) break;
3984
+ if (isTaskCompleted(state, taskName, item.key)) continue;
3985
+ try {
3986
+ const dr = await dispatchTaskWithResult(task, taskName, item.key, definition, state, config, void 0, lastOutputContract ?? void 0);
3987
+ state = dr.updatedState;
3988
+ lastOutputContract = dr.contract;
3989
+ tasksCompleted++;
3990
+ } catch (err) {
3991
+ const engineError = handleDispatchError(err, taskName, item.key);
3992
+ errors.push(engineError);
3993
+ state = recordErrorInState(state, taskName, item.key, engineError);
3994
+ writeWorkflowState(state, projectDir);
3995
+ }
3996
+ }
3997
+ }
3998
+ }
3999
+ }
4000
+ if (!config.storyPipeline)
4001
+ for (const step of config.workflow.storyFlow) {
4002
+ if (halted) break;
4003
+ if (config.abortSignal?.aborted) {
4004
+ info("Execution interrupted \u2014 saving state");
4005
+ state = { ...state, phase: "interrupted" };
4006
+ writeWorkflowState(state, projectDir);
4007
+ halted = true;
4008
+ break;
4009
+ }
4010
+ if (isLoopBlock(step)) {
4011
+ const loopResult = await executeLoopBlock(step, state, config, workItems, lastOutputContract);
4012
+ state = loopResult.state;
4013
+ errors.push(...loopResult.errors);
4014
+ tasksCompleted += loopResult.tasksCompleted;
4015
+ lastOutputContract = loopResult.lastContract;
3943
4016
  for (const item of workItems) {
3944
4017
  processedStories.add(item.key);
3945
- if (isTaskCompleted(state, taskName, item.key)) {
3946
- warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
4018
+ }
4019
+ if (loopResult.halted) {
4020
+ halted = true;
4021
+ }
4022
+ if (state.phase === "max-iterations" || state.phase === "circuit-breaker") {
4023
+ halted = true;
4024
+ }
4025
+ continue;
4026
+ }
4027
+ const taskName = step;
4028
+ const task = config.workflow.tasks[taskName];
4029
+ if (!task) {
4030
+ warn(`workflow-engine: task "${taskName}" not found in workflow tasks, skipping`);
4031
+ continue;
4032
+ }
4033
+ if (task.agent === null) {
4034
+ if (task.scope === "per-run") {
4035
+ if (isTaskCompleted(state, taskName, PER_RUN_SENTINEL)) {
4036
+ warn(`workflow-engine: skipping completed task ${taskName} for ${PER_RUN_SENTINEL}`);
3947
4037
  continue;
3948
4038
  }
3949
4039
  try {
3950
4040
  const nullResult = await executeNullTask(
3951
4041
  task,
3952
4042
  taskName,
3953
- item.key,
4043
+ PER_RUN_SENTINEL,
3954
4044
  state,
3955
4045
  config,
3956
4046
  lastOutputContract ?? void 0,
@@ -3960,71 +4050,56 @@ async function executeWorkflow(config) {
3960
4050
  lastOutputContract = nullResult.contract;
3961
4051
  tasksCompleted++;
3962
4052
  } catch (err) {
3963
- const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
4053
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3964
4054
  errors.push(engineError);
3965
- state = recordErrorInState(state, taskName, item.key, engineError);
4055
+ state = recordErrorInState(state, taskName, PER_RUN_SENTINEL, engineError);
3966
4056
  writeWorkflowState(state, projectDir);
3967
4057
  }
4058
+ } else {
4059
+ for (const item of workItems) {
4060
+ processedStories.add(item.key);
4061
+ if (isTaskCompleted(state, taskName, item.key)) {
4062
+ warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
4063
+ continue;
4064
+ }
4065
+ try {
4066
+ const nullResult = await executeNullTask(
4067
+ task,
4068
+ taskName,
4069
+ item.key,
4070
+ state,
4071
+ config,
4072
+ lastOutputContract ?? void 0,
4073
+ accumulatedCostUsd
4074
+ );
4075
+ state = nullResult.updatedState;
4076
+ lastOutputContract = nullResult.contract;
4077
+ tasksCompleted++;
4078
+ } catch (err) {
4079
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
4080
+ errors.push(engineError);
4081
+ state = recordErrorInState(state, taskName, item.key, engineError);
4082
+ writeWorkflowState(state, projectDir);
4083
+ }
4084
+ }
3968
4085
  }
3969
- }
3970
- continue;
3971
- }
3972
- const definition = config.agents[task.agent];
3973
- if (!definition) {
3974
- warn(`workflow-engine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3975
- continue;
3976
- }
3977
- if (task.scope === "per-run") {
3978
- if (isTaskCompleted(state, taskName, PER_RUN_SENTINEL)) {
3979
- warn(`workflow-engine: skipping completed task ${taskName} for ${PER_RUN_SENTINEL}`);
3980
4086
  continue;
3981
4087
  }
3982
- try {
3983
- const dispatchResult = await dispatchTaskWithResult(
3984
- task,
3985
- taskName,
3986
- PER_RUN_SENTINEL,
3987
- definition,
3988
- state,
3989
- config,
3990
- void 0,
3991
- lastOutputContract ?? void 0
3992
- );
3993
- state = dispatchResult.updatedState;
3994
- lastOutputContract = dispatchResult.contract;
3995
- propagateVerifyFlags(taskName, dispatchResult.contract, projectDir);
3996
- accumulatedCostUsd += dispatchResult.contract?.cost_usd ?? 0;
3997
- tasksCompleted++;
3998
- } catch (err) {
3999
- const engineError = handleDispatchError(err, taskName, PER_RUN_SENTINEL);
4000
- errors.push(engineError);
4001
- if (config.onEvent) {
4002
- config.onEvent({ type: "dispatch-error", taskName, storyKey: PER_RUN_SENTINEL, error: { code: engineError.code, message: engineError.message } });
4003
- } else {
4004
- warn(`[${taskName}] ${PER_RUN_SENTINEL} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4005
- }
4006
- state = recordErrorInState(state, taskName, PER_RUN_SENTINEL, engineError);
4007
- writeWorkflowState(state, projectDir);
4008
- if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
4009
- halted = true;
4010
- }
4088
+ const definition = config.agents[task.agent];
4089
+ if (!definition) {
4090
+ warn(`workflow-engine: agent "${task.agent}" not found for task "${taskName}", skipping`);
4091
+ continue;
4011
4092
  }
4012
- } else {
4013
- for (const item of workItems) {
4014
- if (config.abortSignal?.aborted) {
4015
- halted = true;
4016
- break;
4017
- }
4018
- processedStories.add(item.key);
4019
- if (isTaskCompleted(state, taskName, item.key)) {
4020
- warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
4093
+ if (task.scope === "per-run") {
4094
+ if (isTaskCompleted(state, taskName, PER_RUN_SENTINEL)) {
4095
+ warn(`workflow-engine: skipping completed task ${taskName} for ${PER_RUN_SENTINEL}`);
4021
4096
  continue;
4022
4097
  }
4023
4098
  try {
4024
4099
  const dispatchResult = await dispatchTaskWithResult(
4025
4100
  task,
4026
4101
  taskName,
4027
- item.key,
4102
+ PER_RUN_SENTINEL,
4028
4103
  definition,
4029
4104
  state,
4030
4105
  config,
@@ -4037,24 +4112,65 @@ async function executeWorkflow(config) {
4037
4112
  accumulatedCostUsd += dispatchResult.contract?.cost_usd ?? 0;
4038
4113
  tasksCompleted++;
4039
4114
  } catch (err) {
4040
- const engineError = handleDispatchError(err, taskName, item.key);
4115
+ const engineError = handleDispatchError(err, taskName, PER_RUN_SENTINEL);
4041
4116
  errors.push(engineError);
4042
4117
  if (config.onEvent) {
4043
- config.onEvent({ type: "dispatch-error", taskName, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
4118
+ config.onEvent({ type: "dispatch-error", taskName, storyKey: PER_RUN_SENTINEL, error: { code: engineError.code, message: engineError.message } });
4044
4119
  } else {
4045
- warn(`[${taskName}] ${item.key} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4120
+ warn(`[${taskName}] ${PER_RUN_SENTINEL} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4046
4121
  }
4047
- state = recordErrorInState(state, taskName, item.key, engineError);
4122
+ state = recordErrorInState(state, taskName, PER_RUN_SENTINEL, engineError);
4048
4123
  writeWorkflowState(state, projectDir);
4049
4124
  if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
4050
4125
  halted = true;
4126
+ }
4127
+ }
4128
+ } else {
4129
+ for (const item of workItems) {
4130
+ if (config.abortSignal?.aborted) {
4131
+ halted = true;
4051
4132
  break;
4052
4133
  }
4053
- continue;
4134
+ processedStories.add(item.key);
4135
+ if (isTaskCompleted(state, taskName, item.key)) {
4136
+ warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
4137
+ continue;
4138
+ }
4139
+ try {
4140
+ const dispatchResult = await dispatchTaskWithResult(
4141
+ task,
4142
+ taskName,
4143
+ item.key,
4144
+ definition,
4145
+ state,
4146
+ config,
4147
+ void 0,
4148
+ lastOutputContract ?? void 0
4149
+ );
4150
+ state = dispatchResult.updatedState;
4151
+ lastOutputContract = dispatchResult.contract;
4152
+ propagateVerifyFlags(taskName, dispatchResult.contract, projectDir);
4153
+ accumulatedCostUsd += dispatchResult.contract?.cost_usd ?? 0;
4154
+ tasksCompleted++;
4155
+ } catch (err) {
4156
+ const engineError = handleDispatchError(err, taskName, item.key);
4157
+ errors.push(engineError);
4158
+ if (config.onEvent) {
4159
+ config.onEvent({ type: "dispatch-error", taskName, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
4160
+ } else {
4161
+ warn(`[${taskName}] ${item.key} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4162
+ }
4163
+ state = recordErrorInState(state, taskName, item.key, engineError);
4164
+ writeWorkflowState(state, projectDir);
4165
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
4166
+ halted = true;
4167
+ break;
4168
+ }
4169
+ continue;
4170
+ }
4054
4171
  }
4055
4172
  }
4056
4173
  }
4057
- }
4058
4174
  if (state.phase === "interrupted") {
4059
4175
  } else if (errors.length === 0 && state.phase !== "max-iterations" && state.phase !== "circuit-breaker") {
4060
4176
  state = { ...state, phase: "completed" };
@@ -4930,15 +5046,29 @@ function CompletedTool({ entry }) {
4930
5046
  entry.driver && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${entry.driver})` })
4931
5047
  ] });
4932
5048
  }
4933
- var VISIBLE_COMPLETED_TOOLS = 5;
4934
- function CompletedTools({ tools }) {
4935
- const visible = tools.slice(-VISIBLE_COMPLETED_TOOLS);
5049
+ var DEFAULT_VISIBLE_TOOLS = 5;
5050
+ function CompletedTools({ tools, maxVisible }) {
5051
+ const limit = maxVisible ?? DEFAULT_VISIBLE_TOOLS;
5052
+ const visible = tools.slice(-limit);
4936
5053
  const hidden = tools.length - visible.length;
4937
5054
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
4938
5055
  hidden > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${hidden} earlier tools` }),
4939
5056
  visible.map((entry, i) => /* @__PURE__ */ jsx(CompletedTool, { entry }, i))
4940
5057
  ] });
4941
5058
  }
5059
+ function ActivitySection({ completedTools, activeTool, activeDriverName, lastThought, retryInfo, availableHeight }) {
5060
+ let reserved = 0;
5061
+ if (activeTool) reserved++;
5062
+ if (lastThought) reserved++;
5063
+ if (retryInfo) reserved++;
5064
+ const toolsHeight = Math.max(1, availableHeight - reserved - 1);
5065
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [
5066
+ /* @__PURE__ */ jsx(CompletedTools, { tools: completedTools, maxVisible: toolsHeight }),
5067
+ activeTool && /* @__PURE__ */ jsx(ActiveTool, { name: activeTool.name, driverName: activeDriverName }),
5068
+ lastThought && /* @__PURE__ */ jsx(LastThought, { text: lastThought }),
5069
+ retryInfo && /* @__PURE__ */ jsx(RetryNotice, { info: retryInfo })
5070
+ ] });
5071
+ }
4942
5072
  function ActiveTool({ name, driverName }) {
4943
5073
  return /* @__PURE__ */ jsxs(Box, { children: [
4944
5074
  /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A1 " }),
@@ -4964,13 +5094,6 @@ function RetryNotice({ info: info3 }) {
4964
5094
  "ms)"
4965
5095
  ] });
4966
5096
  }
4967
- function DriverCostSummary({ driverCosts }) {
4968
- if (!driverCosts) return null;
4969
- const entries = Object.entries(driverCosts).sort(([a], [b]) => a.localeCompare(b));
4970
- if (entries.length === 0) return null;
4971
- const parts = entries.map(([driver, cost]) => `${driver} $${cost.toFixed(2)}`).join(", ");
4972
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `Cost: ${parts}` });
4973
- }
4974
5097
 
4975
5098
  // src/lib/ink-app.tsx
4976
5099
  import { Box as Box7, Static, Text as Text7, useInput } from "ink";
@@ -5430,39 +5553,49 @@ function LaneActivityHeader({ activeLaneId, laneCount }) {
5430
5553
  if (laneCount <= 1 || !activeLaneId) return null;
5431
5554
  return /* @__PURE__ */ jsx7(Text7, { children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: `[Lane ${activeLaneId} \u25B8]` }) });
5432
5555
  }
5433
- function App({ state, onCycleLane }) {
5556
+ function App({ state, onCycleLane, onQuit }) {
5434
5557
  const lanes = state.lanes;
5435
5558
  const laneCount = lanes?.length ?? 0;
5436
5559
  const terminalWidth = process.stdout.columns || 80;
5437
- useInput((_input, key) => {
5438
- if (key.ctrl && _input === "l" && onCycleLane && laneCount > 1) {
5560
+ useInput((input, key) => {
5561
+ if (key.ctrl && input === "l" && onCycleLane && laneCount > 1) {
5439
5562
  onCycleLane();
5440
5563
  }
5564
+ if (input === "q" && onQuit) {
5565
+ onQuit();
5566
+ }
5441
5567
  }, { isActive: typeof process.stdin.setRawMode === "function" });
5442
5568
  const activeLaneCount = state.laneCount ?? 0;
5569
+ const termRows = process.stdout.rows || 24;
5570
+ const fixedHeight = 10;
5571
+ const availableHeight = Math.max(3, termRows - fixedHeight);
5443
5572
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
5444
5573
  /* @__PURE__ */ jsx7(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx7(StoryMessageLine, { msg }, i) }),
5445
5574
  /* @__PURE__ */ jsx7(Header, { info: state.sprintInfo, laneCount: laneCount > 1 ? laneCount : void 0 }),
5446
- laneCount > 1 ? /* @__PURE__ */ jsx7(LaneContainer, { lanes, terminalWidth }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
5447
- /* @__PURE__ */ jsx7(WorkflowGraph, { flow: state.workflowFlow, currentTask: state.currentTaskName, taskStates: state.taskStates, taskMeta: state.taskMeta }),
5448
- /* @__PURE__ */ jsx7(StoryBreakdown, { stories: state.stories, sprintInfo: state.sprintInfo }),
5449
- /* @__PURE__ */ jsx7(DriverCostSummary, { driverCosts: state.driverCosts })
5450
- ] }),
5451
- laneCount > 1 && state.summaryBar && /* @__PURE__ */ jsxs7(Fragment, { children: [
5575
+ laneCount > 1 ? /* @__PURE__ */ jsxs7(Fragment, { children: [
5576
+ /* @__PURE__ */ jsx7(LaneContainer, { lanes, terminalWidth }),
5577
+ state.summaryBar && /* @__PURE__ */ jsxs7(Fragment, { children: [
5578
+ /* @__PURE__ */ jsx7(Separator, {}),
5579
+ /* @__PURE__ */ jsx7(SummaryBar, { ...state.summaryBar })
5580
+ ] }),
5581
+ state.mergeState && /* @__PURE__ */ jsxs7(Fragment, { children: [
5582
+ /* @__PURE__ */ jsx7(Separator, {}),
5583
+ /* @__PURE__ */ jsx7(MergeStatus, { mergeState: state.mergeState })
5584
+ ] }),
5452
5585
  /* @__PURE__ */ jsx7(Separator, {}),
5453
- /* @__PURE__ */ jsx7(SummaryBar, { ...state.summaryBar })
5454
- ] }),
5455
- laneCount > 1 && state.mergeState && /* @__PURE__ */ jsxs7(Fragment, { children: [
5586
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 1, children: [
5587
+ /* @__PURE__ */ jsx7(LaneActivityHeader, { activeLaneId: state.activeLaneId ?? null, laneCount: activeLaneCount }),
5588
+ /* @__PURE__ */ jsx7(ActivitySection, { completedTools: state.completedTools, activeTool: state.activeTool, activeDriverName: state.activeDriverName, lastThought: state.lastThought, retryInfo: state.retryInfo, availableHeight })
5589
+ ] })
5590
+ ] }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
5456
5591
  /* @__PURE__ */ jsx7(Separator, {}),
5457
- /* @__PURE__ */ jsx7(MergeStatus, { mergeState: state.mergeState })
5458
- ] }),
5459
- (state.stories.length > 0 || laneCount > 1 && (state.summaryBar || state.mergeState)) && /* @__PURE__ */ jsx7(Separator, {}),
5460
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 1, children: [
5461
- /* @__PURE__ */ jsx7(LaneActivityHeader, { activeLaneId: state.activeLaneId ?? null, laneCount: activeLaneCount }),
5462
- /* @__PURE__ */ jsx7(CompletedTools, { tools: state.completedTools }),
5463
- state.activeTool && /* @__PURE__ */ jsx7(ActiveTool, { name: state.activeTool.name, driverName: state.activeDriverName }),
5464
- state.lastThought && /* @__PURE__ */ jsx7(LastThought, { text: state.lastThought }),
5465
- state.retryInfo && /* @__PURE__ */ jsx7(RetryNotice, { info: state.retryInfo })
5592
+ /* @__PURE__ */ jsx7(ProgressBar, { done: state.sprintInfo?.done ?? 0, total: state.sprintInfo?.total ?? 0 }),
5593
+ /* @__PURE__ */ jsx7(EpicInfo, { info: state.sprintInfo }),
5594
+ /* @__PURE__ */ jsx7(StoryContext, { entries: state.storyContext ?? [] }),
5595
+ /* @__PURE__ */ jsx7(Separator, {}),
5596
+ /* @__PURE__ */ jsx7(WorkflowGraph, { flow: state.workflowFlow, currentTask: state.currentTaskName, taskStates: state.taskStates }),
5597
+ /* @__PURE__ */ jsx7(Separator, {}),
5598
+ /* @__PURE__ */ jsx7(ActivitySection, { completedTools: state.completedTools, activeTool: state.activeTool, activeDriverName: state.activeDriverName, lastThought: state.lastThought, retryInfo: state.retryInfo, availableHeight })
5466
5599
  ] })
5467
5600
  ] });
5468
5601
  }
@@ -5483,133 +5616,48 @@ function formatCost3(cost) {
5483
5616
  function Header({ info: info3, laneCount }) {
5484
5617
  if (!info3) return null;
5485
5618
  const parts = ["codeharness run"];
5486
- if (laneCount != null && laneCount > 1) {
5487
- parts.push(`${laneCount} lanes`);
5488
- }
5489
- if (info3.iterationCount != null) {
5490
- parts.push(`iteration ${info3.iterationCount}`);
5491
- }
5492
- if (info3.elapsed) {
5493
- parts.push(`${info3.elapsed} elapsed`);
5494
- }
5619
+ if (laneCount != null && laneCount > 1) parts.push(`${laneCount} lanes`);
5620
+ if (info3.elapsed) parts.push(`${info3.elapsed} elapsed`);
5495
5621
  const displayCost = laneCount != null && laneCount > 1 && info3.laneTotalCost != null ? info3.laneTotalCost : info3.totalCost;
5496
- if (displayCost != null) {
5497
- parts.push(`${formatCost3(displayCost)} spent`);
5498
- }
5499
- const headerLine = parts.join(" | ");
5500
- let phaseLine = "";
5501
- if (info3.phase) {
5502
- phaseLine = `Phase: ${info3.phase}`;
5503
- if (info3.acProgress) {
5504
- phaseLine += ` \u2192 AC ${info3.acProgress}`;
5505
- }
5506
- if (info3.currentCommand) {
5507
- phaseLine += ` (${info3.currentCommand})`;
5508
- }
5509
- }
5510
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
5511
- /* @__PURE__ */ jsx8(Text8, { children: headerLine }),
5512
- /* @__PURE__ */ jsx8(Separator, {}),
5513
- /* @__PURE__ */ jsx8(Text8, { children: `Story: ${info3.storyKey || "(waiting)"}` }),
5514
- phaseLine && /* @__PURE__ */ jsx8(Text8, { children: phaseLine })
5622
+ if (displayCost != null) parts.push(`${formatCost3(displayCost)} spent`);
5623
+ const left = parts.join(" | ");
5624
+ const right = "[q to quit]";
5625
+ const width = process.stdout.columns || 80;
5626
+ const pad = Math.max(0, width - left.length - right.length);
5627
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
5628
+ left,
5629
+ " ".repeat(pad),
5630
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: right })
5515
5631
  ] });
5516
5632
  }
5517
- function StoryBreakdown({ stories, sprintInfo }) {
5518
- if (stories.length === 0) return null;
5519
- const done = [];
5520
- const inProgress = [];
5521
- const pending = [];
5522
- const failed = [];
5523
- const blocked = [];
5524
- for (const s of stories) {
5525
- switch (s.status) {
5526
- case "done":
5527
- done.push(s);
5528
- break;
5529
- case "in-progress":
5530
- inProgress.push(s);
5531
- break;
5532
- case "pending":
5533
- pending.push(s);
5534
- break;
5535
- case "failed":
5536
- failed.push(s);
5537
- break;
5538
- case "blocked":
5539
- blocked.push(s);
5540
- break;
5541
- }
5542
- }
5543
- const lines = [];
5544
- if (done.length > 0) {
5545
- const doneItems = done.map((s) => {
5546
- let item = `${shortKey(s.key)} \u2713`;
5547
- if (s.costByDriver && Object.keys(s.costByDriver).length > 0) {
5548
- const costParts = Object.keys(s.costByDriver).sort().map(
5549
- (driver) => `${driver} ${formatCost3(s.costByDriver[driver])}`
5550
- );
5551
- item += ` ${costParts.join(", ")}`;
5552
- }
5553
- return item;
5554
- }).join(" ");
5555
- lines.push(
5556
- /* @__PURE__ */ jsxs8(Text8, { children: [
5557
- /* @__PURE__ */ jsx8(Text8, { color: "green", children: "Done: " }),
5558
- /* @__PURE__ */ jsx8(Text8, { color: "green", children: doneItems })
5559
- ] }, "done")
5560
- );
5561
- }
5562
- if (inProgress.length > 0) {
5563
- for (const s of inProgress) {
5564
- let thisText = `${shortKey(s.key)} \u25C6`;
5565
- if (sprintInfo && sprintInfo.storyKey && shortKey(s.key) === shortKey(sprintInfo.storyKey)) {
5566
- if (sprintInfo.phase) thisText += ` ${sprintInfo.phase}`;
5567
- if (sprintInfo.acProgress) thisText += ` (${sprintInfo.acProgress} ACs)`;
5568
- }
5569
- lines.push(
5570
- /* @__PURE__ */ jsxs8(Text8, { children: [
5571
- /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "This: " }),
5572
- /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: thisText })
5573
- ] }, `this-${s.key}`)
5574
- );
5575
- }
5576
- }
5577
- if (pending.length > 0) {
5578
- lines.push(
5579
- /* @__PURE__ */ jsxs8(Text8, { children: [
5580
- /* @__PURE__ */ jsx8(Text8, { children: "Next: " }),
5581
- /* @__PURE__ */ jsx8(Text8, { children: shortKey(pending[0].key) }),
5582
- pending.length > 1 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` (+${pending.length - 1} more)` })
5583
- ] }, "next")
5584
- );
5585
- }
5586
- if (blocked.length > 0) {
5587
- const blockedItems = blocked.map((s) => {
5588
- let item = `${shortKey(s.key)} \u2715`;
5589
- if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
5590
- return item;
5591
- }).join(" ");
5592
- lines.push(
5593
- /* @__PURE__ */ jsxs8(Text8, { children: [
5594
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Blocked: " }),
5595
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: blockedItems })
5596
- ] }, "blocked")
5597
- );
5598
- }
5599
- if (failed.length > 0) {
5600
- const failedItems = failed.map((s) => {
5601
- let item = `${shortKey(s.key)} \u2717`;
5602
- if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
5603
- return item;
5604
- }).join(" ");
5605
- lines.push(
5606
- /* @__PURE__ */ jsxs8(Text8, { children: [
5607
- /* @__PURE__ */ jsx8(Text8, { color: "red", children: "Failed: " }),
5608
- /* @__PURE__ */ jsx8(Text8, { color: "red", children: failedItems })
5609
- ] }, "failed")
5610
- );
5611
- }
5612
- return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: lines });
5633
+ function ProgressBar({ done, total }) {
5634
+ const width = Math.max(10, (process.stdout.columns || 80) - 30);
5635
+ const pct = total > 0 ? done / total : 0;
5636
+ const filled = Math.round(width * pct);
5637
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
5638
+ const pctStr = total > 0 ? `${Math.round(pct * 100)}%` : "0%";
5639
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
5640
+ "Progress: ",
5641
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: bar }),
5642
+ ` ${done}/${total} stories (${pctStr})`
5643
+ ] });
5644
+ }
5645
+ function EpicInfo({ info: info3 }) {
5646
+ if (!info3?.epicId) return null;
5647
+ const title = info3.epicTitle ?? `Epic ${info3.epicId}`;
5648
+ const progress = info3.epicStoriesTotal ? ` \u2014 ${info3.epicStoriesDone ?? 0}/${info3.epicStoriesTotal} stories done` : "";
5649
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
5650
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: `Epic ${info3.epicId}: ${title}` }),
5651
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: progress })
5652
+ ] });
5653
+ }
5654
+ function StoryContext({ entries }) {
5655
+ if (entries.length === 0) return null;
5656
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: entries.map((e, i) => {
5657
+ if (e.role === "prev") return /* @__PURE__ */ jsx8(Text8, { children: /* @__PURE__ */ jsx8(Text8, { color: "green", children: ` Prev: ${shortKey(e.key)} \u2713` }) }, i);
5658
+ if (e.role === "current") return /* @__PURE__ */ jsx8(Text8, { children: /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: ` This: ${shortKey(e.key)} \u25C6 ${e.task ?? ""}` }) }, i);
5659
+ return /* @__PURE__ */ jsx8(Text8, { children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` Next: ${shortKey(e.key)}` }) }, i);
5660
+ }) });
5613
5661
  }
5614
5662
 
5615
5663
  // src/lib/ink-renderer.tsx
@@ -5652,6 +5700,7 @@ function startRenderer(options) {
5652
5700
  taskMeta: {},
5653
5701
  activeDriverName: null,
5654
5702
  driverCosts: {},
5703
+ storyContext: [],
5655
5704
  activeLaneId: null,
5656
5705
  laneCount: 0
5657
5706
  };
@@ -5661,7 +5710,8 @@ function startRenderer(options) {
5661
5710
  let lastStoryKey = state.sprintInfo?.storyKey ?? null;
5662
5711
  const pendingStoryCosts = /* @__PURE__ */ new Map();
5663
5712
  let cleaned = false;
5664
- const inkInstance = inkRender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }), {
5713
+ const onQuit = options?.onQuit;
5714
+ const inkInstance = inkRender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane(), onQuit: onQuit ? () => onQuit() : void 0 }), {
5665
5715
  exitOnCtrlC: false,
5666
5716
  patchConsole: !options?._forceTTY,
5667
5717
  maxFps: 15
@@ -5669,7 +5719,7 @@ function startRenderer(options) {
5669
5719
  function rerender() {
5670
5720
  if (!cleaned) {
5671
5721
  state = { ...state };
5672
- inkInstance.rerender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }));
5722
+ inkInstance.rerender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane(), onQuit: onQuit ? () => onQuit() : void 0 }));
5673
5723
  }
5674
5724
  }
5675
5725
  const heartbeat = setInterval(() => {
@@ -6005,6 +6055,24 @@ function startRenderer(options) {
6005
6055
  lastStoryKey = currentKey;
6006
6056
  }
6007
6057
  state.stories = updatedStories;
6058
+ const ctx = [];
6059
+ const currentStory = currentKey ?? "";
6060
+ const currentTask = state.currentTaskName ?? "";
6061
+ let foundCurrent = false;
6062
+ let prevKey = null;
6063
+ for (const s of updatedStories) {
6064
+ if (s.key === currentStory) {
6065
+ if (prevKey) ctx.push({ key: prevKey, role: "prev" });
6066
+ ctx.push({ key: s.key, role: "current", task: currentTask });
6067
+ foundCurrent = true;
6068
+ } else if (foundCurrent && s.status === "pending") {
6069
+ ctx.push({ key: s.key, role: "next" });
6070
+ break;
6071
+ } else if (s.status === "done") {
6072
+ prevKey = s.key;
6073
+ }
6074
+ }
6075
+ state.storyContext = ctx;
6008
6076
  rerender();
6009
6077
  }
6010
6078
  function addMessage(msg) {
@@ -6175,17 +6243,8 @@ function registerRunCommand(program) {
6175
6243
  }
6176
6244
  }
6177
6245
  const abortController = new AbortController();
6178
- const renderer = startRenderer({
6179
- quiet: !!options.quiet || isJson,
6180
- sprintState: {
6181
- storyKey: "",
6182
- phase: "executing",
6183
- done: counts.done,
6184
- total: counts.total,
6185
- totalCost: 0
6186
- }
6187
- });
6188
6246
  let interrupted = false;
6247
+ let renderer;
6189
6248
  const onInterrupt = () => {
6190
6249
  if (interrupted) {
6191
6250
  process.exit(1);
@@ -6195,6 +6254,17 @@ function registerRunCommand(program) {
6195
6254
  abortController.abort();
6196
6255
  info("Interrupted \u2014 waiting for current task to finish...", outputOpts);
6197
6256
  };
6257
+ renderer = startRenderer({
6258
+ quiet: !!options.quiet || isJson,
6259
+ sprintState: {
6260
+ storyKey: "",
6261
+ phase: "executing",
6262
+ done: counts.done,
6263
+ total: counts.total,
6264
+ totalCost: 0
6265
+ },
6266
+ onQuit: () => onInterrupt()
6267
+ });
6198
6268
  process.on("SIGINT", onInterrupt);
6199
6269
  process.on("SIGTERM", onInterrupt);
6200
6270
  const sessionStartMs = Date.now();
@@ -6204,15 +6274,28 @@ function registerRunCommand(program) {
6204
6274
  let currentTaskName = "";
6205
6275
  const headerRefresh = setInterval(() => {
6206
6276
  if (interrupted) return;
6277
+ const epicId = currentStoryKey ? extractEpicId2(currentStoryKey) : "";
6278
+ const epic = epicId ? epicData[epicId] : void 0;
6207
6279
  renderer.updateSprintState({
6208
6280
  storyKey: currentStoryKey,
6209
6281
  phase: currentTaskName,
6210
6282
  done: storiesDone,
6211
6283
  total: counts.total,
6212
6284
  totalCost: totalCostUsd,
6213
- elapsed: formatElapsed(Date.now() - sessionStartMs)
6285
+ elapsed: formatElapsed(Date.now() - sessionStartMs),
6286
+ epicId: epicId || void 0,
6287
+ epicStoriesDone: epic?.storiesDone,
6288
+ epicStoriesTotal: epic?.storiesTotal
6214
6289
  });
6215
6290
  }, 2e3);
6291
+ const epicData = {};
6292
+ const sprintStateResult = getSprintState2();
6293
+ if (sprintStateResult.success) {
6294
+ for (const [epicKey, epic] of Object.entries(sprintStateResult.data.epics ?? {})) {
6295
+ const epicId = epicKey.replace("epic-", "");
6296
+ epicData[epicId] = { storiesDone: epic.storiesDone ?? 0, storiesTotal: epic.storiesTotal ?? 0 };
6297
+ }
6298
+ }
6216
6299
  const taskStates = {};
6217
6300
  const taskMeta = {};
6218
6301
  for (const [tn, task] of Object.entries(parsedWorkflow.tasks)) {
@@ -6235,13 +6318,18 @@ function registerRunCommand(program) {
6235
6318
  if (event.type === "dispatch-start") {
6236
6319
  currentStoryKey = event.storyKey;
6237
6320
  currentTaskName = event.taskName;
6321
+ const epicId = extractEpicId2(event.storyKey);
6322
+ const epic = epicData[epicId];
6238
6323
  renderer.updateSprintState({
6239
6324
  storyKey: event.storyKey,
6240
6325
  phase: event.taskName,
6241
6326
  done: storiesDone,
6242
6327
  total: counts.total,
6243
6328
  totalCost: totalCostUsd,
6244
- elapsed: formatElapsed(Date.now() - sessionStartMs)
6329
+ elapsed: formatElapsed(Date.now() - sessionStartMs),
6330
+ epicId,
6331
+ epicStoriesDone: epic?.storiesDone ?? 0,
6332
+ epicStoriesTotal: epic?.storiesTotal ?? 0
6245
6333
  });
6246
6334
  taskStates[event.taskName] = "active";
6247
6335
  renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
@@ -6286,6 +6374,7 @@ function registerRunCommand(program) {
6286
6374
  runId: `run-${Date.now()}`,
6287
6375
  projectDir,
6288
6376
  abortSignal: abortController.signal,
6377
+ storyPipeline: true,
6289
6378
  maxIterations,
6290
6379
  onEvent
6291
6380
  };
@@ -11167,7 +11256,7 @@ function registerTeardownCommand(program) {
11167
11256
  } else if (otlpMode === "remote-routed") {
11168
11257
  if (!options.keepDocker) {
11169
11258
  try {
11170
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-PYH5XATT.js");
11259
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-P65B7Z3S.js");
11171
11260
  stopCollectorOnly2();
11172
11261
  result.docker.stopped = true;
11173
11262
  if (!isJson) {
@@ -11199,7 +11288,7 @@ function registerTeardownCommand(program) {
11199
11288
  info("Shared stack: kept running (other projects may use it)");
11200
11289
  }
11201
11290
  } else if (isLegacyStack) {
11202
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-PYH5XATT.js");
11291
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-P65B7Z3S.js");
11203
11292
  let stackRunning = false;
11204
11293
  try {
11205
11294
  stackRunning = isStackRunning2(composeFile);
@@ -14095,7 +14184,7 @@ function registerDriversCommand(program) {
14095
14184
  }
14096
14185
 
14097
14186
  // src/index.ts
14098
- var VERSION = true ? "0.29.4" : "0.0.0-dev";
14187
+ var VERSION = true ? "0.30.1" : "0.0.0-dev";
14099
14188
  function createProgram() {
14100
14189
  const program = new Command();
14101
14190
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.29.4",
3
+ "version": "0.30.1",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {