codeharness 0.29.2 → 0.29.4

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.2" : "0.0.0-dev";
2898
+ var HARNESS_VERSION = true ? "0.29.4" : "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-3ZSXMCZV.js";
19
+ } from "./chunk-KVKEUNEB.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-3ZSXMCZV.js";
43
+ } from "./chunk-KVKEUNEB.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,
@@ -5660,13 +5672,14 @@ function startRenderer(options) {
5660
5672
  inkInstance.rerender(/* @__PURE__ */ jsx9(App, { state, onCycleLane: () => cycleLane() }));
5661
5673
  }
5662
5674
  }
5675
+ const heartbeat = setInterval(() => {
5676
+ if (!cleaned) rerender();
5677
+ }, 200);
5663
5678
  function onSigint() {
5664
5679
  cleanupFull();
5665
- process.kill(process.pid, "SIGINT");
5666
5680
  }
5667
5681
  function onSigterm() {
5668
5682
  cleanupFull();
5669
- process.kill(process.pid, "SIGTERM");
5670
5683
  }
5671
5684
  process.on("SIGINT", onSigint);
5672
5685
  process.on("SIGTERM", onSigterm);
@@ -5928,6 +5941,7 @@ function startRenderer(options) {
5928
5941
  function cleanupFull() {
5929
5942
  if (cleaned) return;
5930
5943
  cleaned = true;
5944
+ clearInterval(heartbeat);
5931
5945
  try {
5932
5946
  inkInstance.unmount();
5933
5947
  } catch {
@@ -6160,6 +6174,7 @@ function registerRunCommand(program) {
6160
6174
  info("Resuming after circuit breaker \u2014 previous findings preserved", outputOpts);
6161
6175
  }
6162
6176
  }
6177
+ const abortController = new AbortController();
6163
6178
  const renderer = startRenderer({
6164
6179
  quiet: !!options.quiet || isJson,
6165
6180
  sprintState: {
@@ -6170,8 +6185,34 @@ function registerRunCommand(program) {
6170
6185
  totalCost: 0
6171
6186
  }
6172
6187
  });
6188
+ let interrupted = false;
6189
+ const onInterrupt = () => {
6190
+ if (interrupted) {
6191
+ process.exit(1);
6192
+ }
6193
+ interrupted = true;
6194
+ renderer.cleanup();
6195
+ abortController.abort();
6196
+ info("Interrupted \u2014 waiting for current task to finish...", outputOpts);
6197
+ };
6198
+ process.on("SIGINT", onInterrupt);
6199
+ process.on("SIGTERM", onInterrupt);
6200
+ const sessionStartMs = Date.now();
6173
6201
  let totalCostUsd = 0;
6174
6202
  let storiesDone = counts.done;
6203
+ let currentStoryKey = "";
6204
+ let currentTaskName = "";
6205
+ const headerRefresh = setInterval(() => {
6206
+ if (interrupted) return;
6207
+ renderer.updateSprintState({
6208
+ storyKey: currentStoryKey,
6209
+ phase: currentTaskName,
6210
+ done: storiesDone,
6211
+ total: counts.total,
6212
+ totalCost: totalCostUsd,
6213
+ elapsed: formatElapsed(Date.now() - sessionStartMs)
6214
+ });
6215
+ }, 2e3);
6175
6216
  const taskStates = {};
6176
6217
  const taskMeta = {};
6177
6218
  for (const [tn, task] of Object.entries(parsedWorkflow.tasks)) {
@@ -6192,12 +6233,15 @@ function registerRunCommand(program) {
6192
6233
  renderer.update(event.streamEvent, event.driverName);
6193
6234
  }
6194
6235
  if (event.type === "dispatch-start") {
6236
+ currentStoryKey = event.storyKey;
6237
+ currentTaskName = event.taskName;
6195
6238
  renderer.updateSprintState({
6196
6239
  storyKey: event.storyKey,
6197
6240
  phase: event.taskName,
6198
6241
  done: storiesDone,
6199
6242
  total: counts.total,
6200
- totalCost: totalCostUsd
6243
+ totalCost: totalCostUsd,
6244
+ elapsed: formatElapsed(Date.now() - sessionStartMs)
6201
6245
  });
6202
6246
  taskStates[event.taskName] = "active";
6203
6247
  renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
@@ -6241,6 +6285,7 @@ function registerRunCommand(program) {
6241
6285
  issuesPath: join15(projectDir, ".codeharness", "issues.yaml"),
6242
6286
  runId: `run-${Date.now()}`,
6243
6287
  projectDir,
6288
+ abortSignal: abortController.signal,
6244
6289
  maxIterations,
6245
6290
  onEvent
6246
6291
  };
@@ -6320,8 +6365,14 @@ function registerRunCommand(program) {
6320
6365
  } else {
6321
6366
  try {
6322
6367
  const result = await executeWorkflow(config);
6368
+ clearInterval(headerRefresh);
6369
+ process.removeListener("SIGINT", onInterrupt);
6370
+ process.removeListener("SIGTERM", onInterrupt);
6323
6371
  renderer.cleanup();
6324
- if (result.success) {
6372
+ if (interrupted) {
6373
+ info(`Interrupted \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed. State saved \u2014 run again to resume.`, outputOpts);
6374
+ process.exitCode = 130;
6375
+ } else if (result.success) {
6325
6376
  ok(`Workflow completed \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed in ${formatElapsed(result.durationMs)}`, outputOpts);
6326
6377
  } else {
6327
6378
  fail(`Workflow failed \u2014 ${result.storiesProcessed} stories processed, ${result.tasksCompleted} tasks completed, ${result.errors.length} error(s) in ${formatElapsed(result.durationMs)}`, outputOpts);
@@ -6331,6 +6382,7 @@ function registerRunCommand(program) {
6331
6382
  process.exitCode = 1;
6332
6383
  }
6333
6384
  } catch (err) {
6385
+ clearInterval(headerRefresh);
6334
6386
  renderer.cleanup();
6335
6387
  const msg = err instanceof Error ? err.message : String(err);
6336
6388
  fail(`Workflow engine error: ${msg}`, outputOpts);
@@ -11115,7 +11167,7 @@ function registerTeardownCommand(program) {
11115
11167
  } else if (otlpMode === "remote-routed") {
11116
11168
  if (!options.keepDocker) {
11117
11169
  try {
11118
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-QJGQIPTO.js");
11170
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-PYH5XATT.js");
11119
11171
  stopCollectorOnly2();
11120
11172
  result.docker.stopped = true;
11121
11173
  if (!isJson) {
@@ -11147,7 +11199,7 @@ function registerTeardownCommand(program) {
11147
11199
  info("Shared stack: kept running (other projects may use it)");
11148
11200
  }
11149
11201
  } else if (isLegacyStack) {
11150
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-QJGQIPTO.js");
11202
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-PYH5XATT.js");
11151
11203
  let stackRunning = false;
11152
11204
  try {
11153
11205
  stackRunning = isStackRunning2(composeFile);
@@ -13378,11 +13430,38 @@ function classifyError(err) {
13378
13430
  }
13379
13431
  return "UNKNOWN";
13380
13432
  }
13381
- function mapSdkMessage(message) {
13433
+ function mapSdkMessages(message) {
13382
13434
  const type = message.type;
13435
+ const events = [];
13436
+ if (type === "assistant") {
13437
+ const msg = message.message;
13438
+ if (!msg) return events;
13439
+ const content = msg.content;
13440
+ if (!Array.isArray(content)) return events;
13441
+ for (const block of content) {
13442
+ if (block.type === "tool_use") {
13443
+ const name = block.name;
13444
+ const id = block.id;
13445
+ const input = block.input;
13446
+ if (typeof name === "string") {
13447
+ events.push({ type: "tool-start", name, id: typeof id === "string" ? id : "" });
13448
+ if (input != null) {
13449
+ events.push({ type: "tool-input", partial: typeof input === "string" ? input : JSON.stringify(input) });
13450
+ }
13451
+ events.push({ type: "tool-complete" });
13452
+ }
13453
+ } else if (block.type === "text") {
13454
+ const text = block.text;
13455
+ if (typeof text === "string" && text.length > 0) {
13456
+ events.push({ type: "text", text });
13457
+ }
13458
+ }
13459
+ }
13460
+ return events;
13461
+ }
13383
13462
  if (type === "stream_event") {
13384
13463
  const event = message.event;
13385
- if (!event || typeof event !== "object") return null;
13464
+ if (!event || typeof event !== "object") return events;
13386
13465
  const eventType = event.type;
13387
13466
  if (eventType === "content_block_start") {
13388
13467
  const contentBlock = event.content_block;
@@ -13390,34 +13469,26 @@ function mapSdkMessage(message) {
13390
13469
  const name = contentBlock.name;
13391
13470
  const id = contentBlock.id;
13392
13471
  if (typeof name === "string" && typeof id === "string") {
13393
- return { type: "tool-start", name, id };
13472
+ events.push({ type: "tool-start", name, id });
13394
13473
  }
13395
13474
  }
13396
- return null;
13475
+ return events;
13397
13476
  }
13398
13477
  if (eventType === "content_block_delta") {
13399
13478
  const delta = event.delta;
13400
- if (!delta) return null;
13401
- if (delta.type === "input_json_delta") {
13402
- const partialJson = delta.partial_json;
13403
- if (typeof partialJson === "string") {
13404
- return { type: "tool-input", partial: partialJson };
13405
- }
13406
- return null;
13407
- }
13408
- if (delta.type === "text_delta") {
13409
- const text = delta.text;
13410
- if (typeof text === "string") {
13411
- return { type: "text", text };
13412
- }
13413
- return null;
13479
+ if (!delta) return events;
13480
+ if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
13481
+ events.push({ type: "tool-input", partial: delta.partial_json });
13482
+ } else if (delta.type === "text_delta" && typeof delta.text === "string") {
13483
+ events.push({ type: "text", text: delta.text });
13414
13484
  }
13415
- return null;
13485
+ return events;
13416
13486
  }
13417
13487
  if (eventType === "content_block_stop") {
13418
- return { type: "tool-complete" };
13488
+ events.push({ type: "tool-complete" });
13489
+ return events;
13419
13490
  }
13420
- return null;
13491
+ return events;
13421
13492
  }
13422
13493
  if (type === "system") {
13423
13494
  const subtype = message.subtype;
@@ -13425,12 +13496,12 @@ function mapSdkMessage(message) {
13425
13496
  const attempt = message.attempt;
13426
13497
  const delay = message.retry_delay_ms;
13427
13498
  if (typeof attempt === "number" && typeof delay === "number") {
13428
- return { type: "retry", attempt, delay };
13499
+ events.push({ type: "retry", attempt, delay });
13429
13500
  }
13430
13501
  }
13431
- return null;
13502
+ return events;
13432
13503
  }
13433
- return null;
13504
+ return events;
13434
13505
  }
13435
13506
  var ClaudeCodeDriver = class {
13436
13507
  name = "claude-code";
@@ -13491,8 +13562,7 @@ var ClaudeCodeDriver = class {
13491
13562
  yieldedResult = true;
13492
13563
  continue;
13493
13564
  }
13494
- const streamEvent = mapSdkMessage(msg);
13495
- if (streamEvent) {
13565
+ for (const streamEvent of mapSdkMessages(msg)) {
13496
13566
  yield streamEvent;
13497
13567
  }
13498
13568
  }
@@ -14025,7 +14095,7 @@ function registerDriversCommand(program) {
14025
14095
  }
14026
14096
 
14027
14097
  // src/index.ts
14028
- var VERSION = true ? "0.29.2" : "0.0.0-dev";
14098
+ var VERSION = true ? "0.29.4" : "0.0.0-dev";
14029
14099
  function createProgram() {
14030
14100
  const program = new Command();
14031
14101
  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.2",
3
+ "version": "0.29.4",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {