codeharness 0.29.1 → 0.29.3

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.1" : "0.0.0-dev";
2898
+ var HARNESS_VERSION = true ? "0.29.3" : "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-EJ6GZH4Z.js";
19
+ } from "./chunk-4QLSEKNP.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-EJ6GZH4Z.js";
43
+ } from "./chunk-4QLSEKNP.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { Command } from "commander";
@@ -3884,6 +3884,13 @@ async function executeWorkflow(config) {
3884
3884
  let accumulatedCostUsd = 0;
3885
3885
  for (const step of config.workflow.storyFlow) {
3886
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
+ }
3887
3894
  if (isLoopBlock(step)) {
3888
3895
  const loopResult = await executeLoopBlock(step, state, config, workItems, lastOutputContract);
3889
3896
  state = loopResult.state;
@@ -4004,6 +4011,10 @@ async function executeWorkflow(config) {
4004
4011
  }
4005
4012
  } else {
4006
4013
  for (const item of workItems) {
4014
+ if (config.abortSignal?.aborted) {
4015
+ halted = true;
4016
+ break;
4017
+ }
4007
4018
  processedStories.add(item.key);
4008
4019
  if (isTaskCompleted(state, taskName, item.key)) {
4009
4020
  warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
@@ -4044,13 +4055,14 @@ async function executeWorkflow(config) {
4044
4055
  }
4045
4056
  }
4046
4057
  }
4047
- if (errors.length === 0 && state.phase !== "max-iterations" && state.phase !== "circuit-breaker") {
4058
+ if (state.phase === "interrupted") {
4059
+ } else if (errors.length === 0 && state.phase !== "max-iterations" && state.phase !== "circuit-breaker") {
4048
4060
  state = { ...state, phase: "completed" };
4049
4061
  writeWorkflowState(state, projectDir);
4050
4062
  }
4051
4063
  const loopTerminated = state.phase === "max-iterations" || state.phase === "circuit-breaker";
4052
4064
  return {
4053
- success: errors.length === 0 && !loopTerminated,
4065
+ success: errors.length === 0 && !loopTerminated && state.phase !== "interrupted",
4054
4066
  tasksCompleted,
4055
4067
  storiesProcessed: processedStories.size,
4056
4068
  errors,
@@ -6160,6 +6172,7 @@ function registerRunCommand(program) {
6160
6172
  info("Resuming after circuit breaker \u2014 previous findings preserved", outputOpts);
6161
6173
  }
6162
6174
  }
6175
+ const abortController = new AbortController();
6163
6176
  const renderer = startRenderer({
6164
6177
  quiet: !!options.quiet || isJson,
6165
6178
  sprintState: {
@@ -6170,6 +6183,34 @@ function registerRunCommand(program) {
6170
6183
  totalCost: 0
6171
6184
  }
6172
6185
  });
6186
+ let interrupted = false;
6187
+ const onInterrupt = () => {
6188
+ if (interrupted) {
6189
+ renderer.cleanup();
6190
+ process.exit(1);
6191
+ }
6192
+ interrupted = true;
6193
+ abortController.abort();
6194
+ };
6195
+ process.on("SIGINT", onInterrupt);
6196
+ process.on("SIGTERM", onInterrupt);
6197
+ let totalCostUsd = 0;
6198
+ let storiesDone = counts.done;
6199
+ const taskStates = {};
6200
+ const taskMeta = {};
6201
+ for (const [tn, task] of Object.entries(parsedWorkflow.tasks)) {
6202
+ taskStates[tn] = "pending";
6203
+ taskMeta[tn] = { driver: task.driver ?? "claude-code" };
6204
+ }
6205
+ const storyEntries = [];
6206
+ for (const [key, status] of Object.entries(statuses)) {
6207
+ if (key.startsWith("epic-")) continue;
6208
+ if (status === "done") storyEntries.push({ key, status: "done" });
6209
+ else if (status === "in-progress") storyEntries.push({ key, status: "in-progress" });
6210
+ else if (status === "backlog" || status === "ready-for-dev") storyEntries.push({ key, status: "pending" });
6211
+ else if (status === "failed") storyEntries.push({ key, status: "failed" });
6212
+ }
6213
+ renderer.updateStories(storyEntries);
6173
6214
  const onEvent = (event) => {
6174
6215
  if (event.type === "stream-event" && event.streamEvent) {
6175
6216
  renderer.update(event.streamEvent, event.driverName);
@@ -6177,24 +6218,39 @@ function registerRunCommand(program) {
6177
6218
  if (event.type === "dispatch-start") {
6178
6219
  renderer.updateSprintState({
6179
6220
  storyKey: event.storyKey,
6180
- phase: `${event.taskName}`,
6181
- done: counts.done,
6182
- total: counts.total
6221
+ phase: event.taskName,
6222
+ done: storiesDone,
6223
+ total: counts.total,
6224
+ totalCost: totalCostUsd
6183
6225
  });
6184
- renderer.updateWorkflowState(
6185
- parsedWorkflow.flow,
6186
- event.taskName,
6187
- { [event.taskName]: "active" }
6188
- );
6226
+ taskStates[event.taskName] = "active";
6227
+ renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
6228
+ const idx = storyEntries.findIndex((s) => s.key === event.storyKey);
6229
+ if (idx >= 0 && storyEntries[idx].status === "pending") {
6230
+ storyEntries[idx] = { ...storyEntries[idx], status: "in-progress" };
6231
+ renderer.updateStories([...storyEntries]);
6232
+ }
6189
6233
  }
6190
6234
  if (event.type === "dispatch-end") {
6191
- renderer.updateWorkflowState(
6192
- parsedWorkflow.flow,
6193
- event.taskName,
6194
- { [event.taskName]: "done" }
6195
- );
6235
+ totalCostUsd += event.costUsd ?? 0;
6236
+ taskStates[event.taskName] = "done";
6237
+ taskMeta[event.taskName] = {
6238
+ ...taskMeta[event.taskName],
6239
+ costUsd: (taskMeta[event.taskName]?.costUsd ?? 0) + (event.costUsd ?? 0),
6240
+ elapsedMs: (taskMeta[event.taskName]?.elapsedMs ?? 0) + (event.elapsedMs ?? 0)
6241
+ };
6242
+ renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
6243
+ renderer.updateSprintState({
6244
+ storyKey: event.storyKey,
6245
+ phase: event.taskName,
6246
+ done: storiesDone,
6247
+ total: counts.total,
6248
+ totalCost: totalCostUsd
6249
+ });
6196
6250
  }
6197
6251
  if (event.type === "dispatch-error") {
6252
+ taskStates[event.taskName] = "failed";
6253
+ renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
6198
6254
  renderer.addMessage({
6199
6255
  type: "fail",
6200
6256
  key: event.storyKey,
@@ -6209,6 +6265,7 @@ function registerRunCommand(program) {
6209
6265
  issuesPath: join15(projectDir, ".codeharness", "issues.yaml"),
6210
6266
  runId: `run-${Date.now()}`,
6211
6267
  projectDir,
6268
+ abortSignal: abortController.signal,
6212
6269
  maxIterations,
6213
6270
  onEvent
6214
6271
  };
@@ -6288,8 +6345,13 @@ function registerRunCommand(program) {
6288
6345
  } else {
6289
6346
  try {
6290
6347
  const result = await executeWorkflow(config);
6348
+ process.removeListener("SIGINT", onInterrupt);
6349
+ process.removeListener("SIGTERM", onInterrupt);
6291
6350
  renderer.cleanup();
6292
- if (result.success) {
6351
+ if (interrupted) {
6352
+ info(`Interrupted \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed. State saved \u2014 run again to resume.`, outputOpts);
6353
+ process.exitCode = 130;
6354
+ } else if (result.success) {
6293
6355
  ok(`Workflow completed \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed in ${formatElapsed(result.durationMs)}`, outputOpts);
6294
6356
  } else {
6295
6357
  fail(`Workflow failed \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed, ${result.errors.length} error(s) in ${formatElapsed(result.durationMs)}`, outputOpts);
@@ -11083,7 +11145,7 @@ function registerTeardownCommand(program) {
11083
11145
  } else if (otlpMode === "remote-routed") {
11084
11146
  if (!options.keepDocker) {
11085
11147
  try {
11086
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-JBXHIWZS.js");
11148
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-XNAP7B5H.js");
11087
11149
  stopCollectorOnly2();
11088
11150
  result.docker.stopped = true;
11089
11151
  if (!isJson) {
@@ -11115,7 +11177,7 @@ function registerTeardownCommand(program) {
11115
11177
  info("Shared stack: kept running (other projects may use it)");
11116
11178
  }
11117
11179
  } else if (isLegacyStack) {
11118
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-JBXHIWZS.js");
11180
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-XNAP7B5H.js");
11119
11181
  let stackRunning = false;
11120
11182
  try {
11121
11183
  stackRunning = isStackRunning2(composeFile);
@@ -13993,7 +14055,7 @@ function registerDriversCommand(program) {
13993
14055
  }
13994
14056
 
13995
14057
  // src/index.ts
13996
- var VERSION = true ? "0.29.1" : "0.0.0-dev";
14058
+ var VERSION = true ? "0.29.3" : "0.0.0-dev";
13997
14059
  function createProgram() {
13998
14060
  const program = new Command();
13999
14061
  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.1",
3
+ "version": "0.29.3",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {