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,
|
|
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 →
|
|
64
|
+
| `/harness-run` | Autonomous sprint execution — picks stories by priority, runs create → implement → check → 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 →
|
|
87
|
+
│ → create-story → implement → check → 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.
|
|
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",
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
validateDockerfile,
|
|
41
41
|
warn,
|
|
42
42
|
writeState
|
|
43
|
-
} from "./chunk-
|
|
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
|
|
5050
|
-
function CompletedTools({ tools }) {
|
|
5051
|
-
const
|
|
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
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
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
|
|
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, ${
|
|
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__ */
|
|
5566
|
-
/* @__PURE__ */ jsx7(
|
|
5567
|
-
/* @__PURE__ */
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
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__ */
|
|
5573
|
-
|
|
5574
|
-
|
|
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(
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
/* @__PURE__ */ jsx7(
|
|
5581
|
-
/* @__PURE__ */ jsx7(
|
|
5582
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5617
|
-
|
|
5618
|
-
const
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
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
|
|
5637
|
-
|
|
5638
|
-
const
|
|
5639
|
-
const
|
|
5640
|
-
const
|
|
5641
|
-
const
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
}
|
|
5661
|
-
}
|
|
5662
|
-
|
|
5663
|
-
|
|
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
|
|
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: ${
|
|
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 =
|
|
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-
|
|
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-
|
|
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("--
|
|
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.
|
|
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
|
@@ -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
|