codeharness 0.30.0 → 0.31.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ codeharness is an **npm CLI** + **Claude Code plugin** that packages verificatio
8
8
 
9
9
  1. **Verifies features work** — not just that tests pass. Black-box verification runs the built CLI inside a Docker container with no source code access. If the feature doesn't work from a user's perspective, verification fails.
10
10
  2. **Fixes what it finds** — verification failures with code bugs automatically return to development with specific findings. The dev agent gets told exactly what's broken and why.
11
- 3. **Runs sprints autonomously** — reads your sprint plan, picks the highest-priority story, implements it, reviews it, verifies it, and moves to the next one. Cross-epic prioritization, retry management, and session handoff built in.
11
+ 3. **Runs sprints autonomously** — reads your sprint plan, picks the highest-priority story, implements it, checks it (tests + lint), verifies it (agent evaluation), and moves to the next one. Cross-epic prioritization, retry management, and session handoff built in.
12
12
  4. **Makes agents see runtime** — ephemeral VictoriaMetrics stack (logs, metrics, traces) that agents query programmatically during development. No guessing at what the code does at runtime.
13
13
 
14
14
  ## Installation
@@ -61,7 +61,7 @@ The plugin provides slash commands that orchestrate the CLI within Claude Code s
61
61
 
62
62
  | Command | Purpose |
63
63
  |---------|---------|
64
- | `/harness-run` | Autonomous sprint execution — picks stories by priority, runs create → devreview → verify loop |
64
+ | `/harness-run` | Autonomous sprint execution — picks stories by priority, runs create → implementcheck → verify loop |
65
65
  | `/harness-init` | Interactive project initialization |
66
66
  | `/harness-status` | Quick overview of sprint progress and harness health |
67
67
  | `/harness-onboard` | Scan project and generate onboarding plan |
@@ -84,7 +84,7 @@ codeharness integrates with [BMAD Method](https://github.com/bmadcode/BMAD-METHO
84
84
  ┌─────────────────────────────────────────┐
85
85
  │ Claude Code Session │
86
86
  │ /harness-run picks next story │
87
- │ → create-story → devreview → verify
87
+ │ → create-story → implementcheck → verify
88
88
  └────────────────────┬────────────────────┘
89
89
  │ verify
90
90
 
@@ -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.30.0" : "0.0.0-dev";
2898
+ var HARNESS_VERSION = true ? "0.31.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-S3RKDJMC.js";
19
+ } from "./chunk-ITPLJVAB.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-S3RKDJMC.js";
43
+ } from "./chunk-ITPLJVAB.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { Command } from "commander";
@@ -5046,15 +5046,29 @@ function CompletedTool({ entry }) {
5046
5046
  entry.driver && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${entry.driver})` })
5047
5047
  ] });
5048
5048
  }
5049
- var VISIBLE_COMPLETED_TOOLS = 5;
5050
- function CompletedTools({ tools }) {
5051
- 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);
5052
5053
  const hidden = tools.length - visible.length;
5053
5054
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
5054
5055
  hidden > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${hidden} earlier tools` }),
5055
5056
  visible.map((entry, i) => /* @__PURE__ */ jsx(CompletedTool, { entry }, i))
5056
5057
  ] });
5057
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
+ }
5058
5072
  function ActiveTool({ name, driverName }) {
5059
5073
  return /* @__PURE__ */ jsxs(Box, { children: [
5060
5074
  /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A1 " }),
@@ -5080,13 +5094,6 @@ function RetryNotice({ info: info3 }) {
5080
5094
  "ms)"
5081
5095
  ] });
5082
5096
  }
5083
- function DriverCostSummary({ driverCosts }) {
5084
- if (!driverCosts) return null;
5085
- const entries = Object.entries(driverCosts).sort(([a], [b]) => a.localeCompare(b));
5086
- if (entries.length === 0) return null;
5087
- const parts = entries.map(([driver, cost]) => `${driver} $${cost.toFixed(2)}`).join(", ");
5088
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `Cost: ${parts}` });
5089
- }
5090
5097
 
5091
5098
  // src/lib/ink-app.tsx
5092
5099
  import { Box as Box7, Static, Text as Text7, useInput } from "ink";
@@ -5094,23 +5101,10 @@ import { Box as Box7, Static, Text as Text7, useInput } from "ink";
5094
5101
  // src/lib/ink-workflow.tsx
5095
5102
  import { Text as Text2, Box as Box2 } from "ink";
5096
5103
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5097
- var termWidth = () => Math.min(process.stdout.columns || 60, 80);
5098
5104
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5099
5105
  function isLoopBlock2(step) {
5100
5106
  return typeof step === "object" && step !== null && "loop" in step;
5101
5107
  }
5102
- function formatCost(costUsd) {
5103
- if (costUsd == null) return "...";
5104
- return `$${costUsd.toFixed(2)}`;
5105
- }
5106
- function formatElapsed2(ms) {
5107
- if (ms == null) return "...";
5108
- const seconds = Math.round(ms / 1e3);
5109
- if (seconds >= 60) {
5110
- return `${Math.floor(seconds / 60)}m`;
5111
- }
5112
- return `${seconds}s`;
5113
- }
5114
5108
  function TaskNode({ name, status, spinnerFrame }) {
5115
5109
  const s = status ?? "pending";
5116
5110
  switch (s) {
@@ -5144,17 +5138,6 @@ function loopIteration(tasks, taskStates) {
5144
5138
  });
5145
5139
  return anyStarted ? 1 : 0;
5146
5140
  }
5147
- function collectTaskNames(flow) {
5148
- const names = [];
5149
- for (const step of flow) {
5150
- if (isLoopBlock2(step)) {
5151
- names.push(...step.loop);
5152
- } else {
5153
- names.push(step);
5154
- }
5155
- }
5156
- return names;
5157
- }
5158
5141
  function hasMetaData(taskMeta) {
5159
5142
  if (!taskMeta) return false;
5160
5143
  return Object.keys(taskMeta).length > 0;
@@ -5200,69 +5183,10 @@ function WorkflowGraph({ flow, currentTask, taskStates, taskMeta }) {
5200
5183
  );
5201
5184
  }
5202
5185
  }
5203
- let driverRow = null;
5204
- let costRow = null;
5205
- if (showMeta) {
5206
- const taskNames = collectTaskNames(flow);
5207
- const driverParts = [];
5208
- const costParts = [];
5209
- let hasAnyCost = false;
5210
- for (const name of taskNames) {
5211
- const m = meta[name];
5212
- const driver = m?.driver ?? "";
5213
- driverParts.push(driver);
5214
- const state = taskStates[name];
5215
- if (state === "done") {
5216
- const costStr = formatCost(m?.costUsd);
5217
- const timeStr = formatElapsed2(m?.elapsedMs);
5218
- costParts.push(`${costStr} / ${timeStr}`);
5219
- hasAnyCost = true;
5220
- } else {
5221
- costParts.push("");
5222
- }
5223
- }
5224
- const hasSomeDriver = driverParts.some((d) => d.length > 0);
5225
- if (hasSomeDriver) {
5226
- const driverLabels = [];
5227
- for (let idx = 0; idx < taskNames.length; idx++) {
5228
- if (idx > 0) {
5229
- driverLabels.push(/* @__PURE__ */ jsx2(Text2, { children: " " }, `drv-sep-${idx}`));
5230
- }
5231
- driverLabels.push(
5232
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: driverParts[idx] || " " }, `drv-${idx}`)
5233
- );
5234
- }
5235
- driverRow = /* @__PURE__ */ jsxs2(Text2, { children: [
5236
- " ",
5237
- driverLabels
5238
- ] });
5239
- }
5240
- if (hasAnyCost) {
5241
- const costLabels = [];
5242
- for (let idx = 0; idx < taskNames.length; idx++) {
5243
- if (idx > 0) {
5244
- costLabels.push(/* @__PURE__ */ jsx2(Text2, { children: " " }, `cost-sep-${idx}`));
5245
- }
5246
- costLabels.push(
5247
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: costParts[idx] || " " }, `cost-${idx}`)
5248
- );
5249
- }
5250
- costRow = /* @__PURE__ */ jsxs2(Text2, { children: [
5251
- " ",
5252
- costLabels
5253
- ] });
5254
- }
5255
- }
5256
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
5257
- /* @__PURE__ */ jsx2(Text2, { children: "\u2501".repeat(termWidth()) }),
5258
- /* @__PURE__ */ jsxs2(Text2, { children: [
5259
- " ",
5260
- elements
5261
- ] }),
5262
- driverRow,
5263
- costRow,
5264
- /* @__PURE__ */ jsx2(Text2, { children: "\u2501".repeat(termWidth()) })
5265
- ] });
5186
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { children: [
5187
+ " ",
5188
+ elements
5189
+ ] }) });
5266
5190
  }
5267
5191
 
5268
5192
  // src/lib/ink-lane-container.tsx
@@ -5440,7 +5364,7 @@ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
5440
5364
  function formatConflictText(count) {
5441
5365
  return count === 1 ? "1 conflict" : `${count} conflicts`;
5442
5366
  }
5443
- function formatCost2(cost) {
5367
+ function formatCost(cost) {
5444
5368
  return `$${cost.toFixed(2)}`;
5445
5369
  }
5446
5370
  function SummaryBar({ doneStories, mergingEpic, pendingEpics, completedLanes }) {
@@ -5466,7 +5390,7 @@ function SummaryBar({ doneStories, mergingEpic, pendingEpics, completedLanes })
5466
5390
  /* @__PURE__ */ jsx5(Text5, { children: " \u2502 " }),
5467
5391
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Pending: ${pendingSection}` })
5468
5392
  ] }),
5469
- completedLanes && completedLanes.length > 0 && completedLanes.map((lane) => /* @__PURE__ */ jsx5(Text5, { color: "green", children: `[OK] Lane ${lane.laneIndex}: Epic ${lane.epicId} complete (${lane.storyCount} stories, ${formatCost2(lane.cost)}, ${lane.elapsed})` }, `lane-complete-${lane.laneIndex}`))
5393
+ completedLanes && completedLanes.length > 0 && completedLanes.map((lane) => /* @__PURE__ */ jsx5(Text5, { color: "green", children: `[OK] Lane ${lane.laneIndex}: Epic ${lane.epicId} complete (${lane.storyCount} stories, ${formatCost(lane.cost)}, ${lane.elapsed})` }, `lane-complete-${lane.laneIndex}`))
5470
5394
  ] });
5471
5395
  }
5472
5396
 
@@ -5559,29 +5483,36 @@ function App({ state, onCycleLane, onQuit }) {
5559
5483
  }
5560
5484
  }, { isActive: typeof process.stdin.setRawMode === "function" });
5561
5485
  const activeLaneCount = state.laneCount ?? 0;
5486
+ const termRows = process.stdout.rows || 24;
5487
+ const fixedHeight = 10;
5488
+ const availableHeight = Math.max(3, termRows - fixedHeight);
5562
5489
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
5563
5490
  /* @__PURE__ */ jsx7(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx7(StoryMessageLine, { msg }, i) }),
5564
5491
  /* @__PURE__ */ jsx7(Header, { info: state.sprintInfo, laneCount: laneCount > 1 ? laneCount : void 0 }),
5565
- laneCount > 1 ? /* @__PURE__ */ jsx7(LaneContainer, { lanes, terminalWidth }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
5566
- /* @__PURE__ */ jsx7(WorkflowGraph, { flow: state.workflowFlow, currentTask: state.currentTaskName, taskStates: state.taskStates, taskMeta: state.taskMeta }),
5567
- /* @__PURE__ */ jsx7(StoryBreakdown, { stories: state.stories, sprintInfo: state.sprintInfo }),
5568
- /* @__PURE__ */ jsx7(DriverCostSummary, { driverCosts: state.driverCosts })
5569
- ] }),
5570
- laneCount > 1 && state.summaryBar && /* @__PURE__ */ jsxs7(Fragment, { children: [
5492
+ laneCount > 1 ? /* @__PURE__ */ jsxs7(Fragment, { children: [
5493
+ /* @__PURE__ */ jsx7(LaneContainer, { lanes, terminalWidth }),
5494
+ state.summaryBar && /* @__PURE__ */ jsxs7(Fragment, { children: [
5495
+ /* @__PURE__ */ jsx7(Separator, {}),
5496
+ /* @__PURE__ */ jsx7(SummaryBar, { ...state.summaryBar })
5497
+ ] }),
5498
+ state.mergeState && /* @__PURE__ */ jsxs7(Fragment, { children: [
5499
+ /* @__PURE__ */ jsx7(Separator, {}),
5500
+ /* @__PURE__ */ jsx7(MergeStatus, { mergeState: state.mergeState })
5501
+ ] }),
5571
5502
  /* @__PURE__ */ jsx7(Separator, {}),
5572
- /* @__PURE__ */ jsx7(SummaryBar, { ...state.summaryBar })
5573
- ] }),
5574
- laneCount > 1 && state.mergeState && /* @__PURE__ */ jsxs7(Fragment, { children: [
5503
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 1, children: [
5504
+ /* @__PURE__ */ jsx7(LaneActivityHeader, { activeLaneId: state.activeLaneId ?? null, laneCount: activeLaneCount }),
5505
+ /* @__PURE__ */ jsx7(ActivitySection, { completedTools: state.completedTools, activeTool: state.activeTool, activeDriverName: state.activeDriverName, lastThought: state.lastThought, retryInfo: state.retryInfo, availableHeight })
5506
+ ] })
5507
+ ] }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
5575
5508
  /* @__PURE__ */ jsx7(Separator, {}),
5576
- /* @__PURE__ */ jsx7(MergeStatus, { mergeState: state.mergeState })
5577
- ] }),
5578
- (state.stories.length > 0 || laneCount > 1 && (state.summaryBar || state.mergeState)) && /* @__PURE__ */ jsx7(Separator, {}),
5579
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 1, children: [
5580
- /* @__PURE__ */ jsx7(LaneActivityHeader, { activeLaneId: state.activeLaneId ?? null, laneCount: activeLaneCount }),
5581
- /* @__PURE__ */ jsx7(CompletedTools, { tools: state.completedTools }),
5582
- state.activeTool && /* @__PURE__ */ jsx7(ActiveTool, { name: state.activeTool.name, driverName: state.activeDriverName }),
5583
- state.lastThought && /* @__PURE__ */ jsx7(LastThought, { text: state.lastThought }),
5584
- state.retryInfo && /* @__PURE__ */ jsx7(RetryNotice, { info: state.retryInfo })
5509
+ /* @__PURE__ */ jsx7(ProgressBar, { done: state.sprintInfo?.done ?? 0, total: state.sprintInfo?.total ?? 0 }),
5510
+ /* @__PURE__ */ jsx7(EpicInfo, { info: state.sprintInfo }),
5511
+ /* @__PURE__ */ jsx7(StoryContext, { entries: state.storyContext ?? [] }),
5512
+ /* @__PURE__ */ jsx7(Separator, {}),
5513
+ /* @__PURE__ */ jsx7(WorkflowGraph, { flow: state.workflowFlow, currentTask: state.currentTaskName, taskStates: state.taskStates }),
5514
+ /* @__PURE__ */ jsx7(Separator, {}),
5515
+ /* @__PURE__ */ jsx7(ActivitySection, { completedTools: state.completedTools, activeTool: state.activeTool, activeDriverName: state.activeDriverName, lastThought: state.lastThought, retryInfo: state.retryInfo, availableHeight })
5585
5516
  ] })
5586
5517
  ] });
5587
5518
  }
@@ -5596,139 +5527,54 @@ function shortKey(key) {
5596
5527
  const m = key.match(/^(\d+-\d+)/);
5597
5528
  return m ? m[1] : key;
5598
5529
  }
5599
- function formatCost3(cost) {
5530
+ function formatCost2(cost) {
5600
5531
  return `$${cost.toFixed(2)}`;
5601
5532
  }
5602
5533
  function Header({ info: info3, laneCount }) {
5603
5534
  if (!info3) return null;
5604
5535
  const parts = ["codeharness run"];
5605
- if (laneCount != null && laneCount > 1) {
5606
- parts.push(`${laneCount} lanes`);
5607
- }
5608
- if (info3.iterationCount != null) {
5609
- parts.push(`iteration ${info3.iterationCount}`);
5610
- }
5611
- if (info3.elapsed) {
5612
- parts.push(`${info3.elapsed} elapsed`);
5613
- }
5536
+ if (laneCount != null && laneCount > 1) parts.push(`${laneCount} lanes`);
5537
+ if (info3.elapsed) parts.push(`${info3.elapsed} elapsed`);
5614
5538
  const displayCost = laneCount != null && laneCount > 1 && info3.laneTotalCost != null ? info3.laneTotalCost : info3.totalCost;
5615
- if (displayCost != null) {
5616
- parts.push(`${formatCost3(displayCost)} spent`);
5617
- }
5618
- const headerLine = parts.join(" | ");
5619
- let phaseLine = "";
5620
- if (info3.phase) {
5621
- phaseLine = `Phase: ${info3.phase}`;
5622
- if (info3.acProgress) {
5623
- phaseLine += ` \u2192 AC ${info3.acProgress}`;
5624
- }
5625
- if (info3.currentCommand) {
5626
- phaseLine += ` (${info3.currentCommand})`;
5627
- }
5628
- }
5629
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
5630
- /* @__PURE__ */ jsx8(Text8, { children: headerLine }),
5631
- /* @__PURE__ */ jsx8(Separator, {}),
5632
- /* @__PURE__ */ jsx8(Text8, { children: `Story: ${info3.storyKey || "(waiting)"}` }),
5633
- phaseLine && /* @__PURE__ */ jsx8(Text8, { children: phaseLine })
5539
+ if (displayCost != null) parts.push(`${formatCost2(displayCost)} spent`);
5540
+ const left = parts.join(" | ");
5541
+ const right = "[q to quit]";
5542
+ const width = process.stdout.columns || 80;
5543
+ const pad = Math.max(0, width - left.length - right.length);
5544
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
5545
+ left,
5546
+ " ".repeat(pad),
5547
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: right })
5634
5548
  ] });
5635
5549
  }
5636
- function StoryBreakdown({ stories, sprintInfo }) {
5637
- if (stories.length === 0) return null;
5638
- const done = [];
5639
- const inProgress = [];
5640
- const pending = [];
5641
- const failed = [];
5642
- const blocked = [];
5643
- for (const s of stories) {
5644
- switch (s.status) {
5645
- case "done":
5646
- done.push(s);
5647
- break;
5648
- case "in-progress":
5649
- inProgress.push(s);
5650
- break;
5651
- case "pending":
5652
- pending.push(s);
5653
- break;
5654
- case "failed":
5655
- failed.push(s);
5656
- break;
5657
- case "blocked":
5658
- blocked.push(s);
5659
- break;
5660
- }
5661
- }
5662
- const lines = [];
5663
- if (done.length > 0) {
5664
- const doneItems = done.map((s) => {
5665
- let item = `${shortKey(s.key)} \u2713`;
5666
- if (s.costByDriver && Object.keys(s.costByDriver).length > 0) {
5667
- const costParts = Object.keys(s.costByDriver).sort().map(
5668
- (driver) => `${driver} ${formatCost3(s.costByDriver[driver])}`
5669
- );
5670
- item += ` ${costParts.join(", ")}`;
5671
- }
5672
- return item;
5673
- }).join(" ");
5674
- lines.push(
5675
- /* @__PURE__ */ jsxs8(Text8, { children: [
5676
- /* @__PURE__ */ jsx8(Text8, { color: "green", children: "Done: " }),
5677
- /* @__PURE__ */ jsx8(Text8, { color: "green", children: doneItems })
5678
- ] }, "done")
5679
- );
5680
- }
5681
- if (inProgress.length > 0) {
5682
- for (const s of inProgress) {
5683
- let thisText = `${shortKey(s.key)} \u25C6`;
5684
- if (sprintInfo && sprintInfo.storyKey && shortKey(s.key) === shortKey(sprintInfo.storyKey)) {
5685
- if (sprintInfo.phase) thisText += ` ${sprintInfo.phase}`;
5686
- if (sprintInfo.acProgress) thisText += ` (${sprintInfo.acProgress} ACs)`;
5687
- }
5688
- lines.push(
5689
- /* @__PURE__ */ jsxs8(Text8, { children: [
5690
- /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "This: " }),
5691
- /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: thisText })
5692
- ] }, `this-${s.key}`)
5693
- );
5694
- }
5695
- }
5696
- if (pending.length > 0) {
5697
- lines.push(
5698
- /* @__PURE__ */ jsxs8(Text8, { children: [
5699
- /* @__PURE__ */ jsx8(Text8, { children: "Next: " }),
5700
- /* @__PURE__ */ jsx8(Text8, { children: shortKey(pending[0].key) }),
5701
- pending.length > 1 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` (+${pending.length - 1} more)` })
5702
- ] }, "next")
5703
- );
5704
- }
5705
- if (blocked.length > 0) {
5706
- const blockedItems = blocked.map((s) => {
5707
- let item = `${shortKey(s.key)} \u2715`;
5708
- if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
5709
- return item;
5710
- }).join(" ");
5711
- lines.push(
5712
- /* @__PURE__ */ jsxs8(Text8, { children: [
5713
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Blocked: " }),
5714
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: blockedItems })
5715
- ] }, "blocked")
5716
- );
5717
- }
5718
- if (failed.length > 0) {
5719
- const failedItems = failed.map((s) => {
5720
- let item = `${shortKey(s.key)} \u2717`;
5721
- if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
5722
- return item;
5723
- }).join(" ");
5724
- lines.push(
5725
- /* @__PURE__ */ jsxs8(Text8, { children: [
5726
- /* @__PURE__ */ jsx8(Text8, { color: "red", children: "Failed: " }),
5727
- /* @__PURE__ */ jsx8(Text8, { color: "red", children: failedItems })
5728
- ] }, "failed")
5729
- );
5730
- }
5731
- return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: lines });
5550
+ function ProgressBar({ done, total }) {
5551
+ const width = Math.max(10, (process.stdout.columns || 80) - 30);
5552
+ const pct = total > 0 ? done / total : 0;
5553
+ const filled = Math.round(width * pct);
5554
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
5555
+ const pctStr = total > 0 ? `${Math.round(pct * 100)}%` : "0%";
5556
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
5557
+ "Progress: ",
5558
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: bar }),
5559
+ ` ${done}/${total} stories (${pctStr})`
5560
+ ] });
5561
+ }
5562
+ function EpicInfo({ info: info3 }) {
5563
+ if (!info3?.epicId) return null;
5564
+ const title = info3.epicTitle ?? `Epic ${info3.epicId}`;
5565
+ const progress = info3.epicStoriesTotal ? ` \u2014 ${info3.epicStoriesDone ?? 0}/${info3.epicStoriesTotal} stories done` : "";
5566
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
5567
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: `Epic ${info3.epicId}: ${title}` }),
5568
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: progress })
5569
+ ] });
5570
+ }
5571
+ function StoryContext({ entries }) {
5572
+ if (entries.length === 0) return null;
5573
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: entries.map((e, i) => {
5574
+ if (e.role === "prev") return /* @__PURE__ */ jsx8(Text8, { children: /* @__PURE__ */ jsx8(Text8, { color: "green", children: ` Prev: ${shortKey(e.key)} \u2713` }) }, i);
5575
+ if (e.role === "current") return /* @__PURE__ */ jsx8(Text8, { children: /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: ` This: ${shortKey(e.key)} \u25C6 ${e.task ?? ""}` }) }, i);
5576
+ return /* @__PURE__ */ jsx8(Text8, { children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` Next: ${shortKey(e.key)}` }) }, i);
5577
+ }) });
5732
5578
  }
5733
5579
 
5734
5580
  // src/lib/ink-renderer.tsx
@@ -5771,6 +5617,7 @@ function startRenderer(options) {
5771
5617
  taskMeta: {},
5772
5618
  activeDriverName: null,
5773
5619
  driverCosts: {},
5620
+ storyContext: [],
5774
5621
  activeLaneId: null,
5775
5622
  laneCount: 0
5776
5623
  };
@@ -6125,6 +5972,24 @@ function startRenderer(options) {
6125
5972
  lastStoryKey = currentKey;
6126
5973
  }
6127
5974
  state.stories = updatedStories;
5975
+ const ctx = [];
5976
+ const currentStory = currentKey ?? "";
5977
+ const currentTask = state.currentTaskName ?? "";
5978
+ let foundCurrent = false;
5979
+ let prevKey = null;
5980
+ for (const s of updatedStories) {
5981
+ if (s.key === currentStory) {
5982
+ if (prevKey) ctx.push({ key: prevKey, role: "prev" });
5983
+ ctx.push({ key: s.key, role: "current", task: currentTask });
5984
+ foundCurrent = true;
5985
+ } else if (foundCurrent && s.status === "pending") {
5986
+ ctx.push({ key: s.key, role: "next" });
5987
+ break;
5988
+ } else if (s.status === "done") {
5989
+ prevKey = s.key;
5990
+ }
5991
+ }
5992
+ state.storyContext = ctx;
6128
5993
  rerender();
6129
5994
  }
6130
5995
  function addMessage(msg) {
@@ -6326,15 +6191,28 @@ function registerRunCommand(program) {
6326
6191
  let currentTaskName = "";
6327
6192
  const headerRefresh = setInterval(() => {
6328
6193
  if (interrupted) return;
6194
+ const epicId = currentStoryKey ? extractEpicId2(currentStoryKey) : "";
6195
+ const epic = epicId ? epicData[epicId] : void 0;
6329
6196
  renderer.updateSprintState({
6330
6197
  storyKey: currentStoryKey,
6331
6198
  phase: currentTaskName,
6332
6199
  done: storiesDone,
6333
6200
  total: counts.total,
6334
6201
  totalCost: totalCostUsd,
6335
- elapsed: formatElapsed(Date.now() - sessionStartMs)
6202
+ elapsed: formatElapsed(Date.now() - sessionStartMs),
6203
+ epicId: epicId || void 0,
6204
+ epicStoriesDone: epic?.storiesDone,
6205
+ epicStoriesTotal: epic?.storiesTotal
6336
6206
  });
6337
6207
  }, 2e3);
6208
+ const epicData = {};
6209
+ const sprintStateResult = getSprintState2();
6210
+ if (sprintStateResult.success) {
6211
+ for (const [epicKey, epic] of Object.entries(sprintStateResult.data.epics ?? {})) {
6212
+ const epicId = epicKey.replace("epic-", "");
6213
+ epicData[epicId] = { storiesDone: epic.storiesDone ?? 0, storiesTotal: epic.storiesTotal ?? 0 };
6214
+ }
6215
+ }
6338
6216
  const taskStates = {};
6339
6217
  const taskMeta = {};
6340
6218
  for (const [tn, task] of Object.entries(parsedWorkflow.tasks)) {
@@ -6357,13 +6235,18 @@ function registerRunCommand(program) {
6357
6235
  if (event.type === "dispatch-start") {
6358
6236
  currentStoryKey = event.storyKey;
6359
6237
  currentTaskName = event.taskName;
6238
+ const epicId = extractEpicId2(event.storyKey);
6239
+ const epic = epicData[epicId];
6360
6240
  renderer.updateSprintState({
6361
6241
  storyKey: event.storyKey,
6362
6242
  phase: event.taskName,
6363
6243
  done: storiesDone,
6364
6244
  total: counts.total,
6365
6245
  totalCost: totalCostUsd,
6366
- elapsed: formatElapsed(Date.now() - sessionStartMs)
6246
+ elapsed: formatElapsed(Date.now() - sessionStartMs),
6247
+ epicId,
6248
+ epicStoriesDone: epic?.storiesDone ?? 0,
6249
+ epicStoriesTotal: epic?.storiesTotal ?? 0
6367
6250
  });
6368
6251
  taskStates[event.taskName] = "active";
6369
6252
  renderer.updateWorkflowState(parsedWorkflow.flow, event.taskName, { ...taskStates }, { ...taskMeta });
@@ -10593,7 +10476,7 @@ async function handleDockerCheck(isJson) {
10593
10476
  }
10594
10477
  }
10595
10478
  }
10596
- function formatElapsed3(ms) {
10479
+ function formatElapsed2(ms) {
10597
10480
  const s = Math.floor(ms / 1e3);
10598
10481
  const h = Math.floor(s / 3600);
10599
10482
  const m = Math.floor(s % 3600 / 60);
@@ -10613,7 +10496,7 @@ function printWorkflowState() {
10613
10496
  console.log(` Tasks completed: ${state.tasks_completed.length}`);
10614
10497
  if (state.phase === "executing" && state.started) {
10615
10498
  const elapsed = Date.now() - Date.parse(state.started);
10616
- console.log(` Elapsed: ${formatElapsed3(elapsed)}`);
10499
+ console.log(` Elapsed: ${formatElapsed2(elapsed)}`);
10617
10500
  }
10618
10501
  if (state.evaluator_scores.length > 0) {
10619
10502
  const latest = state.evaluator_scores[state.evaluator_scores.length - 1];
@@ -10638,7 +10521,7 @@ function getWorkflowStateData() {
10638
10521
  };
10639
10522
  if (state.phase === "executing" && state.started) {
10640
10523
  data.elapsed_ms = Date.now() - Date.parse(state.started);
10641
- data.elapsed = formatElapsed3(data.elapsed_ms);
10524
+ data.elapsed = formatElapsed2(data.elapsed_ms);
10642
10525
  }
10643
10526
  return data;
10644
10527
  }
@@ -11290,7 +11173,7 @@ function registerTeardownCommand(program) {
11290
11173
  } else if (otlpMode === "remote-routed") {
11291
11174
  if (!options.keepDocker) {
11292
11175
  try {
11293
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-7QONH2B5.js");
11176
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-TANMGEDO.js");
11294
11177
  stopCollectorOnly2();
11295
11178
  result.docker.stopped = true;
11296
11179
  if (!isJson) {
@@ -11322,7 +11205,7 @@ function registerTeardownCommand(program) {
11322
11205
  info("Shared stack: kept running (other projects may use it)");
11323
11206
  }
11324
11207
  } else if (isLegacyStack) {
11325
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-7QONH2B5.js");
11208
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-TANMGEDO.js");
11326
11209
  let stackRunning = false;
11327
11210
  try {
11328
11211
  stackRunning = isStackRunning2(composeFile);
@@ -13863,12 +13746,12 @@ var CodexDriver = class {
13863
13746
  opts.plugins
13864
13747
  );
13865
13748
  }
13866
- const args = [];
13749
+ const args = ["exec"];
13867
13750
  if (opts.model) {
13868
13751
  args.push("--model", opts.model);
13869
13752
  }
13870
13753
  if (opts.cwd) {
13871
- args.push("--cwd", opts.cwd);
13754
+ args.push("--cd", opts.cwd);
13872
13755
  }
13873
13756
  args.push(opts.prompt);
13874
13757
  let yieldedResult = false;
@@ -14218,7 +14101,7 @@ function registerDriversCommand(program) {
14218
14101
  }
14219
14102
 
14220
14103
  // src/index.ts
14221
- var VERSION = true ? "0.30.0" : "0.0.0-dev";
14104
+ var VERSION = true ? "0.31.0" : "0.0.0-dev";
14222
14105
  function createProgram() {
14223
14106
  const program = new Command();
14224
14107
  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.30.0",
3
+ "version": "0.31.0",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
@@ -0,0 +1,65 @@
1
+ name: checker
2
+ role:
3
+ title: Automated Checker
4
+ purpose: Run tests, linter, and coverage checks — report pass/fail objectively
5
+ persona:
6
+ identity: |
7
+ CI bot that runs the project's test suite, linter, and coverage tool.
8
+ Reports results objectively — no interpretation, no fixes, just facts.
9
+ communication_style: "Machine-like. Commands run, output captured, pass/fail reported."
10
+ principles:
11
+ - Run the project's actual test command (npm test, pytest, cargo test, etc.)
12
+ - Run the project's linter if configured (eslint, ruff, clippy, etc.)
13
+ - Check coverage against target if configured
14
+ - Report exact command, exit code, and output for each check
15
+ - Never fix code — only report results
16
+ prompt_template: |
17
+ ## Role
18
+
19
+ You are running automated checks on the implementation. Run tests, linter, and coverage. Report results.
20
+
21
+ ## Process
22
+
23
+ 1. **Detect check commands** from the project (package.json scripts, pyproject.toml, Makefile, etc.)
24
+ 2. **Run tests**: execute the test command, capture output and exit code
25
+ 3. **Run linter**: execute the lint command if available
26
+ 4. **Check coverage**: if a coverage target exists, verify it's met
27
+
28
+ ## Output Format
29
+
30
+ Output a single JSON object:
31
+
32
+ ```json
33
+ {
34
+ "verdict": "pass" | "fail",
35
+ "checks": [
36
+ {
37
+ "name": "tests",
38
+ "command": "npm test",
39
+ "exit_code": 0,
40
+ "passed": true,
41
+ "summary": "42 tests passed"
42
+ },
43
+ {
44
+ "name": "lint",
45
+ "command": "npm run lint",
46
+ "exit_code": 0,
47
+ "passed": true,
48
+ "summary": "no issues"
49
+ },
50
+ {
51
+ "name": "coverage",
52
+ "command": "npm run coverage",
53
+ "exit_code": 0,
54
+ "passed": true,
55
+ "summary": "98% (target: 100%)"
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ Verdict is "pass" only if ALL checks pass.
62
+
63
+ ## Output Location
64
+
65
+ Write results to ./verdict/check.json
@@ -11,6 +11,12 @@ tasks:
11
11
  session: fresh
12
12
  source_access: true
13
13
  model: claude-sonnet-4-6
14
+ check:
15
+ agent: checker
16
+ scope: per-story
17
+ session: fresh
18
+ source_access: true
19
+ driver: codex
14
20
  review:
15
21
  agent: reviewer
16
22
  scope: per-story
@@ -39,10 +45,12 @@ tasks:
39
45
  flow:
40
46
  - create-story
41
47
  - implement
48
+ - check
42
49
  - review
43
50
  - verify
44
51
  - loop:
45
52
  - retry
53
+ - check
46
54
  - review
47
55
  - verify
48
56
  - retro