codeharness 0.29.3 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.3" : "0.0.0-dev";
2898
+ var HARNESS_VERSION = true ? "0.30.0" : "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-4QLSEKNP.js";
19
+ } from "./chunk-S3RKDJMC.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-4QLSEKNP.js";
43
+ } from "./chunk-S3RKDJMC.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" };
@@ -5430,14 +5546,17 @@ function LaneActivityHeader({ activeLaneId, laneCount }) {
5430
5546
  if (laneCount <= 1 || !activeLaneId) return null;
5431
5547
  return /* @__PURE__ */ jsx7(Text7, { children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: `[Lane ${activeLaneId} \u25B8]` }) });
5432
5548
  }
5433
- function App({ state, onCycleLane }) {
5549
+ function App({ state, onCycleLane, onQuit }) {
5434
5550
  const lanes = state.lanes;
5435
5551
  const laneCount = lanes?.length ?? 0;
5436
5552
  const terminalWidth = process.stdout.columns || 80;
5437
- useInput((_input, key) => {
5438
- if (key.ctrl && _input === "l" && onCycleLane && laneCount > 1) {
5553
+ useInput((input, key) => {
5554
+ if (key.ctrl && input === "l" && onCycleLane && laneCount > 1) {
5439
5555
  onCycleLane();
5440
5556
  }
5557
+ if (input === "q" && onQuit) {
5558
+ onQuit();
5559
+ }
5441
5560
  }, { isActive: typeof process.stdin.setRawMode === "function" });
5442
5561
  const activeLaneCount = state.laneCount ?? 0;
5443
5562
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
@@ -5661,7 +5780,8 @@ function startRenderer(options) {
5661
5780
  let lastStoryKey = state.sprintInfo?.storyKey ?? null;
5662
5781
  const pendingStoryCosts = /* @__PURE__ */ new Map();
5663
5782
  let cleaned = false;
5664
- const inkInstance = inkRender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }), {
5783
+ const onQuit = options?.onQuit;
5784
+ const inkInstance = inkRender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane(), onQuit: onQuit ? () => onQuit() : void 0 }), {
5665
5785
  exitOnCtrlC: false,
5666
5786
  patchConsole: !options?._forceTTY,
5667
5787
  maxFps: 15
@@ -5669,16 +5789,17 @@ function startRenderer(options) {
5669
5789
  function rerender() {
5670
5790
  if (!cleaned) {
5671
5791
  state = { ...state };
5672
- inkInstance.rerender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }));
5792
+ inkInstance.rerender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane(), onQuit: onQuit ? () => onQuit() : void 0 }));
5673
5793
  }
5674
5794
  }
5795
+ const heartbeat = setInterval(() => {
5796
+ if (!cleaned) rerender();
5797
+ }, 200);
5675
5798
  function onSigint() {
5676
5799
  cleanupFull();
5677
- process.kill(process.pid, "SIGINT");
5678
5800
  }
5679
5801
  function onSigterm() {
5680
5802
  cleanupFull();
5681
- process.kill(process.pid, "SIGTERM");
5682
5803
  }
5683
5804
  process.on("SIGINT", onSigint);
5684
5805
  process.on("SIGTERM", onSigterm);
@@ -5940,6 +6061,7 @@ function startRenderer(options) {
5940
6061
  function cleanupFull() {
5941
6062
  if (cleaned) return;
5942
6063
  cleaned = true;
6064
+ clearInterval(heartbeat);
5943
6065
  try {
5944
6066
  inkInstance.unmount();
5945
6067
  } catch {
@@ -6173,29 +6295,46 @@ function registerRunCommand(program) {
6173
6295
  }
6174
6296
  }
6175
6297
  const abortController = new AbortController();
6176
- const renderer = startRenderer({
6177
- quiet: !!options.quiet || isJson,
6178
- sprintState: {
6179
- storyKey: "",
6180
- phase: "executing",
6181
- done: counts.done,
6182
- total: counts.total,
6183
- totalCost: 0
6184
- }
6185
- });
6186
6298
  let interrupted = false;
6299
+ let renderer;
6187
6300
  const onInterrupt = () => {
6188
6301
  if (interrupted) {
6189
- renderer.cleanup();
6190
6302
  process.exit(1);
6191
6303
  }
6192
6304
  interrupted = true;
6305
+ renderer.cleanup();
6193
6306
  abortController.abort();
6307
+ info("Interrupted \u2014 waiting for current task to finish...", outputOpts);
6194
6308
  };
6309
+ renderer = startRenderer({
6310
+ quiet: !!options.quiet || isJson,
6311
+ sprintState: {
6312
+ storyKey: "",
6313
+ phase: "executing",
6314
+ done: counts.done,
6315
+ total: counts.total,
6316
+ totalCost: 0
6317
+ },
6318
+ onQuit: () => onInterrupt()
6319
+ });
6195
6320
  process.on("SIGINT", onInterrupt);
6196
6321
  process.on("SIGTERM", onInterrupt);
6322
+ const sessionStartMs = Date.now();
6197
6323
  let totalCostUsd = 0;
6198
6324
  let storiesDone = counts.done;
6325
+ let currentStoryKey = "";
6326
+ let currentTaskName = "";
6327
+ const headerRefresh = setInterval(() => {
6328
+ if (interrupted) return;
6329
+ renderer.updateSprintState({
6330
+ storyKey: currentStoryKey,
6331
+ phase: currentTaskName,
6332
+ done: storiesDone,
6333
+ total: counts.total,
6334
+ totalCost: totalCostUsd,
6335
+ elapsed: formatElapsed(Date.now() - sessionStartMs)
6336
+ });
6337
+ }, 2e3);
6199
6338
  const taskStates = {};
6200
6339
  const taskMeta = {};
6201
6340
  for (const [tn, task] of Object.entries(parsedWorkflow.tasks)) {
@@ -6216,12 +6355,15 @@ function registerRunCommand(program) {
6216
6355
  renderer.update(event.streamEvent, event.driverName);
6217
6356
  }
6218
6357
  if (event.type === "dispatch-start") {
6358
+ currentStoryKey = event.storyKey;
6359
+ currentTaskName = event.taskName;
6219
6360
  renderer.updateSprintState({
6220
6361
  storyKey: event.storyKey,
6221
6362
  phase: event.taskName,
6222
6363
  done: storiesDone,
6223
6364
  total: counts.total,
6224
- totalCost: totalCostUsd
6365
+ totalCost: totalCostUsd,
6366
+ elapsed: formatElapsed(Date.now() - sessionStartMs)
6225
6367
  });
6226
6368
  taskStates[event.taskName] = "active";
6227
6369
  renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
@@ -6266,6 +6408,7 @@ function registerRunCommand(program) {
6266
6408
  runId: `run-${Date.now()}`,
6267
6409
  projectDir,
6268
6410
  abortSignal: abortController.signal,
6411
+ storyPipeline: true,
6269
6412
  maxIterations,
6270
6413
  onEvent
6271
6414
  };
@@ -6345,6 +6488,7 @@ function registerRunCommand(program) {
6345
6488
  } else {
6346
6489
  try {
6347
6490
  const result = await executeWorkflow(config);
6491
+ clearInterval(headerRefresh);
6348
6492
  process.removeListener("SIGINT", onInterrupt);
6349
6493
  process.removeListener("SIGTERM", onInterrupt);
6350
6494
  renderer.cleanup();
@@ -6361,6 +6505,7 @@ function registerRunCommand(program) {
6361
6505
  process.exitCode = 1;
6362
6506
  }
6363
6507
  } catch (err) {
6508
+ clearInterval(headerRefresh);
6364
6509
  renderer.cleanup();
6365
6510
  const msg = err instanceof Error ? err.message : String(err);
6366
6511
  fail(`Workflow engine error: ${msg}`, outputOpts);
@@ -11145,7 +11290,7 @@ function registerTeardownCommand(program) {
11145
11290
  } else if (otlpMode === "remote-routed") {
11146
11291
  if (!options.keepDocker) {
11147
11292
  try {
11148
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-XNAP7B5H.js");
11293
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-7QONH2B5.js");
11149
11294
  stopCollectorOnly2();
11150
11295
  result.docker.stopped = true;
11151
11296
  if (!isJson) {
@@ -11177,7 +11322,7 @@ function registerTeardownCommand(program) {
11177
11322
  info("Shared stack: kept running (other projects may use it)");
11178
11323
  }
11179
11324
  } else if (isLegacyStack) {
11180
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-XNAP7B5H.js");
11325
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-7QONH2B5.js");
11181
11326
  let stackRunning = false;
11182
11327
  try {
11183
11328
  stackRunning = isStackRunning2(composeFile);
@@ -13408,11 +13553,38 @@ function classifyError(err) {
13408
13553
  }
13409
13554
  return "UNKNOWN";
13410
13555
  }
13411
- function mapSdkMessage(message) {
13556
+ function mapSdkMessages(message) {
13412
13557
  const type = message.type;
13558
+ const events = [];
13559
+ if (type === "assistant") {
13560
+ const msg = message.message;
13561
+ if (!msg) return events;
13562
+ const content = msg.content;
13563
+ if (!Array.isArray(content)) return events;
13564
+ for (const block of content) {
13565
+ if (block.type === "tool_use") {
13566
+ const name = block.name;
13567
+ const id = block.id;
13568
+ const input = block.input;
13569
+ if (typeof name === "string") {
13570
+ events.push({ type: "tool-start", name, id: typeof id === "string" ? id : "" });
13571
+ if (input != null) {
13572
+ events.push({ type: "tool-input", partial: typeof input === "string" ? input : JSON.stringify(input) });
13573
+ }
13574
+ events.push({ type: "tool-complete" });
13575
+ }
13576
+ } else if (block.type === "text") {
13577
+ const text = block.text;
13578
+ if (typeof text === "string" && text.length > 0) {
13579
+ events.push({ type: "text", text });
13580
+ }
13581
+ }
13582
+ }
13583
+ return events;
13584
+ }
13413
13585
  if (type === "stream_event") {
13414
13586
  const event = message.event;
13415
- if (!event || typeof event !== "object") return null;
13587
+ if (!event || typeof event !== "object") return events;
13416
13588
  const eventType = event.type;
13417
13589
  if (eventType === "content_block_start") {
13418
13590
  const contentBlock = event.content_block;
@@ -13420,34 +13592,26 @@ function mapSdkMessage(message) {
13420
13592
  const name = contentBlock.name;
13421
13593
  const id = contentBlock.id;
13422
13594
  if (typeof name === "string" && typeof id === "string") {
13423
- return { type: "tool-start", name, id };
13595
+ events.push({ type: "tool-start", name, id });
13424
13596
  }
13425
13597
  }
13426
- return null;
13598
+ return events;
13427
13599
  }
13428
13600
  if (eventType === "content_block_delta") {
13429
13601
  const delta = event.delta;
13430
- if (!delta) return null;
13431
- if (delta.type === "input_json_delta") {
13432
- const partialJson = delta.partial_json;
13433
- if (typeof partialJson === "string") {
13434
- return { type: "tool-input", partial: partialJson };
13435
- }
13436
- return null;
13602
+ if (!delta) return events;
13603
+ if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
13604
+ events.push({ type: "tool-input", partial: delta.partial_json });
13605
+ } else if (delta.type === "text_delta" && typeof delta.text === "string") {
13606
+ events.push({ type: "text", text: delta.text });
13437
13607
  }
13438
- if (delta.type === "text_delta") {
13439
- const text = delta.text;
13440
- if (typeof text === "string") {
13441
- return { type: "text", text };
13442
- }
13443
- return null;
13444
- }
13445
- return null;
13608
+ return events;
13446
13609
  }
13447
13610
  if (eventType === "content_block_stop") {
13448
- return { type: "tool-complete" };
13611
+ events.push({ type: "tool-complete" });
13612
+ return events;
13449
13613
  }
13450
- return null;
13614
+ return events;
13451
13615
  }
13452
13616
  if (type === "system") {
13453
13617
  const subtype = message.subtype;
@@ -13455,12 +13619,12 @@ function mapSdkMessage(message) {
13455
13619
  const attempt = message.attempt;
13456
13620
  const delay = message.retry_delay_ms;
13457
13621
  if (typeof attempt === "number" && typeof delay === "number") {
13458
- return { type: "retry", attempt, delay };
13622
+ events.push({ type: "retry", attempt, delay });
13459
13623
  }
13460
13624
  }
13461
- return null;
13625
+ return events;
13462
13626
  }
13463
- return null;
13627
+ return events;
13464
13628
  }
13465
13629
  var ClaudeCodeDriver = class {
13466
13630
  name = "claude-code";
@@ -13521,8 +13685,7 @@ var ClaudeCodeDriver = class {
13521
13685
  yieldedResult = true;
13522
13686
  continue;
13523
13687
  }
13524
- const streamEvent = mapSdkMessage(msg);
13525
- if (streamEvent) {
13688
+ for (const streamEvent of mapSdkMessages(msg)) {
13526
13689
  yield streamEvent;
13527
13690
  }
13528
13691
  }
@@ -14055,7 +14218,7 @@ function registerDriversCommand(program) {
14055
14218
  }
14056
14219
 
14057
14220
  // src/index.ts
14058
- var VERSION = true ? "0.29.3" : "0.0.0-dev";
14221
+ var VERSION = true ? "0.30.0" : "0.0.0-dev";
14059
14222
  function createProgram() {
14060
14223
  const program = new Command();
14061
14224
  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.3",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {