codeharness 0.26.3 → 0.26.5
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/dist/{chunk-NYZZCLQG.js → chunk-F6L7CXLK.js} +24 -19
- package/dist/{docker-GG765ZJT.js → docker-VHOP56YP.js} +1 -1
- package/dist/index.js +193 -103
- package/package.json +1 -1
- package/patches/AGENTS.md +1 -1
- package/patches/dev/enforcement.md +16 -7
- package/patches/retro/enforcement.md +2 -2
- package/patches/review/enforcement.md +24 -3
- package/patches/verify/story-verification.md +25 -11
- package/ralph/ralph.sh +13 -8
|
@@ -1096,25 +1096,33 @@ RUN cargo build --release
|
|
|
1096
1096
|
// ── Task 16: getProjectName ───────────────────────────────────────────
|
|
1097
1097
|
// ── getVerifyDockerfileSection ──────────────────────────────────────
|
|
1098
1098
|
getVerifyDockerfileSection(projectDir) {
|
|
1099
|
+
let needsBevy = false;
|
|
1100
|
+
const cargoContent = readTextSafe(join6(projectDir, "Cargo.toml"));
|
|
1101
|
+
if (cargoContent) {
|
|
1102
|
+
const depsSection = getCargoDepsSection(cargoContent);
|
|
1103
|
+
needsBevy = hasCargoDep(depsSection, "bevy");
|
|
1104
|
+
}
|
|
1105
|
+
const aptPackages = ["build-essential", "pkg-config", "libssl-dev"];
|
|
1106
|
+
if (needsBevy) {
|
|
1107
|
+
aptPackages.push(
|
|
1108
|
+
"libudev-dev",
|
|
1109
|
+
"libasound2-dev",
|
|
1110
|
+
"libwayland-dev",
|
|
1111
|
+
"libxkbcommon-dev",
|
|
1112
|
+
"libfontconfig1-dev",
|
|
1113
|
+
"libx11-dev"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1099
1116
|
const lines = [
|
|
1100
1117
|
"# --- Rust tooling ---",
|
|
1118
|
+
"RUN apt-get update && apt-get install -y --no-install-recommends \\",
|
|
1119
|
+
` ${aptPackages.join(" ")} \\`,
|
|
1120
|
+
" && rm -rf /var/lib/apt/lists/*",
|
|
1101
1121
|
'RUN curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable',
|
|
1102
1122
|
'ENV PATH="/root/.cargo/bin:$PATH"',
|
|
1103
1123
|
"RUN rustup component add clippy",
|
|
1104
1124
|
"RUN cargo install cargo-tarpaulin"
|
|
1105
1125
|
];
|
|
1106
|
-
const cargoContent = readTextSafe(join6(projectDir, "Cargo.toml"));
|
|
1107
|
-
if (cargoContent) {
|
|
1108
|
-
const depsSection = getCargoDepsSection(cargoContent);
|
|
1109
|
-
if (hasCargoDep(depsSection, "bevy")) {
|
|
1110
|
-
lines.push(
|
|
1111
|
-
"RUN apt-get update && apt-get install -y --no-install-recommends \\",
|
|
1112
|
-
" libudev-dev libasound2-dev libwayland-dev libxkbcommon-dev \\",
|
|
1113
|
-
" libfontconfig1-dev libx11-dev \\",
|
|
1114
|
-
" && rm -rf /var/lib/apt/lists/*"
|
|
1115
|
-
);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
1126
|
return lines.join("\n");
|
|
1119
1127
|
}
|
|
1120
1128
|
getProjectName(dir) {
|
|
@@ -1337,9 +1345,8 @@ function parseValue(raw) {
|
|
|
1337
1345
|
if (raw === "true") return true;
|
|
1338
1346
|
if (raw === "false") return false;
|
|
1339
1347
|
if (raw === "null") return null;
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
return raw;
|
|
1348
|
+
const n = Number(raw);
|
|
1349
|
+
return !Number.isNaN(n) && raw.trim() !== "" ? n : raw;
|
|
1343
1350
|
}
|
|
1344
1351
|
|
|
1345
1352
|
// src/lib/observability/instrument.ts
|
|
@@ -1932,9 +1939,7 @@ function handleLocalShared(opts, state) {
|
|
|
1932
1939
|
}
|
|
1933
1940
|
if (!opts.isJson) {
|
|
1934
1941
|
fail("Observability stack: failed to start");
|
|
1935
|
-
if (startResult.error) {
|
|
1936
|
-
info(`Error: ${startResult.error}`);
|
|
1937
|
-
}
|
|
1942
|
+
if (startResult.error) info(`Error: ${startResult.error}`);
|
|
1938
1943
|
}
|
|
1939
1944
|
const docker = {
|
|
1940
1945
|
compose_file: sharedComposeFile,
|
|
@@ -3207,7 +3212,7 @@ function generateDockerfileTemplate(projectDir, stackOrDetections) {
|
|
|
3207
3212
|
}
|
|
3208
3213
|
|
|
3209
3214
|
// src/modules/infra/init-project.ts
|
|
3210
|
-
var HARNESS_VERSION = true ? "0.26.
|
|
3215
|
+
var HARNESS_VERSION = true ? "0.26.5" : "0.0.0-dev";
|
|
3211
3216
|
function failResult(opts, error) {
|
|
3212
3217
|
return {
|
|
3213
3218
|
status: "fail",
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
validateDockerfile,
|
|
52
52
|
warn,
|
|
53
53
|
writeState
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-F6L7CXLK.js";
|
|
55
55
|
|
|
56
56
|
// src/index.ts
|
|
57
57
|
import { Command } from "commander";
|
|
@@ -805,16 +805,6 @@ function defaultState() {
|
|
|
805
805
|
actionItems: []
|
|
806
806
|
};
|
|
807
807
|
}
|
|
808
|
-
function defaultStoryState() {
|
|
809
|
-
return {
|
|
810
|
-
status: "backlog",
|
|
811
|
-
attempts: 0,
|
|
812
|
-
lastAttempt: null,
|
|
813
|
-
lastError: null,
|
|
814
|
-
proofPath: null,
|
|
815
|
-
acResults: null
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
808
|
function getStoryStatusesFromState(state) {
|
|
819
809
|
const result = {};
|
|
820
810
|
for (const [key, story] of Object.entries(state.stories)) {
|
|
@@ -982,7 +972,10 @@ function updateStoryStatus(key, status, detail) {
|
|
|
982
972
|
return fail2(stateResult.error);
|
|
983
973
|
}
|
|
984
974
|
const current = stateResult.data;
|
|
985
|
-
const existingStory = current.stories[key]
|
|
975
|
+
const existingStory = current.stories[key];
|
|
976
|
+
if (!existingStory) {
|
|
977
|
+
return fail2(`Story '${key}' does not exist in sprint state \u2014 refusing to create phantom entry`);
|
|
978
|
+
}
|
|
986
979
|
const isNewAttempt = status === "in-progress";
|
|
987
980
|
const updatedStory = {
|
|
988
981
|
...existingStory,
|
|
@@ -1130,17 +1123,30 @@ function reconcileState() {
|
|
|
1130
1123
|
}
|
|
1131
1124
|
const updatedEpics = { ...state.epics };
|
|
1132
1125
|
for (const [epicKey, storyKeys] of epicStories) {
|
|
1126
|
+
const total = storyKeys.length;
|
|
1127
|
+
const doneCount = storyKeys.filter((k) => state.stories[k].status === "done").length;
|
|
1128
|
+
const failedCount = storyKeys.filter((k) => state.stories[k].status === "failed").length;
|
|
1129
|
+
const computedStatus = doneCount === total ? "done" : doneCount + failedCount === total ? "done" : "in-progress";
|
|
1133
1130
|
if (!(epicKey in updatedEpics)) {
|
|
1134
|
-
const total = storyKeys.length;
|
|
1135
|
-
const doneCount = storyKeys.filter((k) => state.stories[k].status === "done").length;
|
|
1136
|
-
const epicStatus = doneCount === total ? "done" : "in-progress";
|
|
1137
1131
|
updatedEpics[epicKey] = {
|
|
1138
|
-
status:
|
|
1132
|
+
status: computedStatus,
|
|
1139
1133
|
storiesTotal: total,
|
|
1140
1134
|
storiesDone: doneCount
|
|
1141
1135
|
};
|
|
1142
1136
|
changed = true;
|
|
1143
1137
|
corrections.push(`created missing epic entry: ${epicKey}`);
|
|
1138
|
+
} else {
|
|
1139
|
+
const existing = updatedEpics[epicKey];
|
|
1140
|
+
if (existing.storiesTotal !== total || existing.storiesDone !== doneCount || existing.status !== computedStatus) {
|
|
1141
|
+
updatedEpics[epicKey] = {
|
|
1142
|
+
...existing,
|
|
1143
|
+
status: computedStatus,
|
|
1144
|
+
storiesTotal: total,
|
|
1145
|
+
storiesDone: doneCount
|
|
1146
|
+
};
|
|
1147
|
+
changed = true;
|
|
1148
|
+
corrections.push(`fixed epic ${epicKey}: ${existing.status}\u2192${computedStatus} (${doneCount}/${total} done, ${failedCount} failed)`);
|
|
1149
|
+
}
|
|
1144
1150
|
}
|
|
1145
1151
|
}
|
|
1146
1152
|
state.epics = updatedEpics;
|
|
@@ -2317,6 +2323,7 @@ var ERROR_LINE = /\[ERROR\]\s+(.+)/;
|
|
|
2317
2323
|
function parseRalphMessage(rawLine) {
|
|
2318
2324
|
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
2319
2325
|
if (clean.length === 0) return null;
|
|
2326
|
+
if (clean.startsWith("{")) return null;
|
|
2320
2327
|
const success = SUCCESS_STORY.exec(clean);
|
|
2321
2328
|
if (success) {
|
|
2322
2329
|
const key = success[1];
|
|
@@ -2559,10 +2566,6 @@ function handleAgentEvent(event, rendererHandle, state) {
|
|
|
2559
2566
|
rendererHandle.update(event);
|
|
2560
2567
|
break;
|
|
2561
2568
|
case "story-complete": {
|
|
2562
|
-
const completeResult = updateStoryStatus2(event.key, "review");
|
|
2563
|
-
if (!completeResult.success) {
|
|
2564
|
-
info(`[WARN] Failed to update status for ${event.key}: ${completeResult.error}`);
|
|
2565
|
-
}
|
|
2566
2569
|
rendererHandle.addMessage({ type: "ok", key: event.key, message: event.details });
|
|
2567
2570
|
break;
|
|
2568
2571
|
}
|
|
@@ -2799,13 +2802,29 @@ function registerRunCommand(program) {
|
|
|
2799
2802
|
|
|
2800
2803
|
// src/commands/verify.ts
|
|
2801
2804
|
import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
|
|
2802
|
-
import { join as
|
|
2805
|
+
import { join as join21 } from "path";
|
|
2803
2806
|
|
|
2804
2807
|
// src/modules/verify/index.ts
|
|
2805
2808
|
import { readFileSync as readFileSync18 } from "fs";
|
|
2806
2809
|
|
|
2807
2810
|
// src/modules/verify/proof.ts
|
|
2808
2811
|
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
2812
|
+
|
|
2813
|
+
// src/modules/verify/types.ts
|
|
2814
|
+
var TIER_HIERARCHY = [
|
|
2815
|
+
"test-provable",
|
|
2816
|
+
"runtime-provable",
|
|
2817
|
+
"environment-provable",
|
|
2818
|
+
"escalate"
|
|
2819
|
+
];
|
|
2820
|
+
var LEGACY_TIER_MAP = {
|
|
2821
|
+
"cli-verifiable": "test-provable",
|
|
2822
|
+
"integration-required": "environment-provable",
|
|
2823
|
+
"unit-testable": "test-provable",
|
|
2824
|
+
"black-box": "environment-provable"
|
|
2825
|
+
};
|
|
2826
|
+
|
|
2827
|
+
// src/modules/verify/proof.ts
|
|
2809
2828
|
function classifyEvidenceCommands(proofContent) {
|
|
2810
2829
|
const results = [];
|
|
2811
2830
|
const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
|
|
@@ -2895,9 +2914,15 @@ function validateProofQuality(proofPath) {
|
|
|
2895
2914
|
return emptyResult;
|
|
2896
2915
|
}
|
|
2897
2916
|
const content = readFileSync9(proofPath, "utf-8");
|
|
2898
|
-
const
|
|
2899
|
-
const
|
|
2900
|
-
const
|
|
2917
|
+
const allTierNames = [...TIER_HIERARCHY, ...Object.keys(LEGACY_TIER_MAP)];
|
|
2918
|
+
const uniqueTierNames = [...new Set(allTierNames)];
|
|
2919
|
+
const tierPattern = new RegExp(`\\*\\*Tier:\\*\\*\\s*(${uniqueTierNames.join("|")})`, "i");
|
|
2920
|
+
const bbTierMatch = tierPattern.exec(content);
|
|
2921
|
+
const rawTierValue = bbTierMatch ? bbTierMatch[1].toLowerCase() : null;
|
|
2922
|
+
const normalizedTier = rawTierValue ? LEGACY_TIER_MAP[rawTierValue] ?? (TIER_HIERARCHY.includes(rawTierValue) ? rawTierValue : null) : null;
|
|
2923
|
+
const skipDockerEnforcement = normalizedTier !== null && normalizedTier !== "environment-provable";
|
|
2924
|
+
const bbRawEnforcement = checkBlackBoxEnforcement(content);
|
|
2925
|
+
const bbEnforcement = skipDockerEnforcement ? { ...bbRawEnforcement, blackBoxPass: true } : bbRawEnforcement;
|
|
2901
2926
|
function buildResult(base) {
|
|
2902
2927
|
const basePassed = base.pending === 0 && base.verified > 0;
|
|
2903
2928
|
return {
|
|
@@ -3716,6 +3741,8 @@ function closeBeadsIssue(storyId, dir) {
|
|
|
3716
3741
|
|
|
3717
3742
|
// src/modules/verify/parser.ts
|
|
3718
3743
|
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
|
|
3744
|
+
|
|
3745
|
+
// src/modules/verify/parser-keywords.ts
|
|
3719
3746
|
var UI_KEYWORDS = [
|
|
3720
3747
|
"agent-browser",
|
|
3721
3748
|
"screenshot",
|
|
@@ -3752,6 +3779,40 @@ var ESCALATE_KEYWORDS = [
|
|
|
3752
3779
|
"visual inspection by human",
|
|
3753
3780
|
"paid external service"
|
|
3754
3781
|
];
|
|
3782
|
+
var RUNTIME_PROVABLE_KEYWORDS = [
|
|
3783
|
+
"cli command",
|
|
3784
|
+
"api endpoint",
|
|
3785
|
+
"http",
|
|
3786
|
+
"server",
|
|
3787
|
+
"output shows",
|
|
3788
|
+
"exit code",
|
|
3789
|
+
"binary",
|
|
3790
|
+
"runs and produces",
|
|
3791
|
+
"cli outputs",
|
|
3792
|
+
"when run"
|
|
3793
|
+
];
|
|
3794
|
+
var ENVIRONMENT_PROVABLE_KEYWORDS = [
|
|
3795
|
+
"docker",
|
|
3796
|
+
"container",
|
|
3797
|
+
"observability",
|
|
3798
|
+
"telemetry",
|
|
3799
|
+
"database",
|
|
3800
|
+
"queue",
|
|
3801
|
+
"distributed",
|
|
3802
|
+
"multi-service",
|
|
3803
|
+
"end-to-end",
|
|
3804
|
+
"victorialogs"
|
|
3805
|
+
];
|
|
3806
|
+
var ESCALATE_TIER_KEYWORDS = [
|
|
3807
|
+
"physical hardware",
|
|
3808
|
+
"human visual",
|
|
3809
|
+
"paid service",
|
|
3810
|
+
"gpu",
|
|
3811
|
+
"manual inspection",
|
|
3812
|
+
"physical display"
|
|
3813
|
+
];
|
|
3814
|
+
|
|
3815
|
+
// src/modules/verify/parser.ts
|
|
3755
3816
|
function classifyVerifiability(description) {
|
|
3756
3817
|
const lower = description.toLowerCase();
|
|
3757
3818
|
for (const kw of INTEGRATION_KEYWORDS) {
|
|
@@ -3766,10 +3827,27 @@ function classifyStrategy(description) {
|
|
|
3766
3827
|
}
|
|
3767
3828
|
return "docker";
|
|
3768
3829
|
}
|
|
3769
|
-
|
|
3830
|
+
function classifyTier(description) {
|
|
3831
|
+
const lower = description.toLowerCase();
|
|
3832
|
+
for (const kw of ESCALATE_TIER_KEYWORDS) {
|
|
3833
|
+
if (lower.includes(kw)) return "escalate";
|
|
3834
|
+
}
|
|
3835
|
+
for (const kw of ENVIRONMENT_PROVABLE_KEYWORDS) {
|
|
3836
|
+
if (lower.includes(kw)) return "environment-provable";
|
|
3837
|
+
}
|
|
3838
|
+
for (const kw of RUNTIME_PROVABLE_KEYWORDS) {
|
|
3839
|
+
if (lower.includes(kw)) return "runtime-provable";
|
|
3840
|
+
}
|
|
3841
|
+
return "test-provable";
|
|
3842
|
+
}
|
|
3843
|
+
var VERIFICATION_TAG_PATTERN = /<!--\s*verification:\s*(cli-verifiable|integration-required|unit-testable|black-box|test-provable|runtime-provable|environment-provable|escalate)\s*-->/;
|
|
3770
3844
|
function parseVerificationTag(text) {
|
|
3771
3845
|
const match = VERIFICATION_TAG_PATTERN.exec(text);
|
|
3772
|
-
|
|
3846
|
+
if (!match) return null;
|
|
3847
|
+
const raw = match[1];
|
|
3848
|
+
const mapped = LEGACY_TIER_MAP[raw] ?? raw;
|
|
3849
|
+
if (!TIER_HIERARCHY.includes(mapped)) return null;
|
|
3850
|
+
return mapped;
|
|
3773
3851
|
}
|
|
3774
3852
|
function classifyAC(description) {
|
|
3775
3853
|
const lower = description.toLowerCase();
|
|
@@ -3821,14 +3899,16 @@ function parseStoryACs(storyFilePath) {
|
|
|
3821
3899
|
const description = currentDesc.join(" ").trim();
|
|
3822
3900
|
if (description) {
|
|
3823
3901
|
const tag = parseVerificationTag(description);
|
|
3824
|
-
const
|
|
3902
|
+
const tier = tag ?? classifyTier(description);
|
|
3903
|
+
const verifiability = classifyVerifiability(description);
|
|
3825
3904
|
const strategy = classifyStrategy(description);
|
|
3826
3905
|
acs.push({
|
|
3827
3906
|
id: currentId,
|
|
3828
3907
|
description,
|
|
3829
3908
|
type: classifyAC(description),
|
|
3830
3909
|
verifiability,
|
|
3831
|
-
strategy
|
|
3910
|
+
strategy,
|
|
3911
|
+
tier
|
|
3832
3912
|
});
|
|
3833
3913
|
} else {
|
|
3834
3914
|
warn(`Skipping malformed AC #${currentId}: empty description`);
|
|
@@ -5418,14 +5498,16 @@ function runValidationCycle() {
|
|
|
5418
5498
|
// src/modules/verify/env.ts
|
|
5419
5499
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
5420
5500
|
import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync12, cpSync, rmSync, statSync as statSync5 } from "fs";
|
|
5421
|
-
import { join as
|
|
5501
|
+
import { join as join20, basename } from "path";
|
|
5422
5502
|
import { createHash } from "crypto";
|
|
5423
5503
|
|
|
5424
5504
|
// src/modules/verify/dockerfile-generator.ts
|
|
5505
|
+
import { join as join19 } from "path";
|
|
5425
5506
|
function generateVerifyDockerfile(projectDir) {
|
|
5426
5507
|
const detections = detectStacks(projectDir);
|
|
5427
5508
|
const sections = [];
|
|
5428
5509
|
sections.push("FROM ubuntu:22.04");
|
|
5510
|
+
sections.push("ENV DEBIAN_FRONTEND=noninteractive");
|
|
5429
5511
|
sections.push("");
|
|
5430
5512
|
sections.push("# Common tools");
|
|
5431
5513
|
sections.push(
|
|
@@ -5440,7 +5522,8 @@ function generateVerifyDockerfile(projectDir) {
|
|
|
5440
5522
|
for (const detection of detections) {
|
|
5441
5523
|
const provider = getStackProvider(detection.stack);
|
|
5442
5524
|
if (!provider) continue;
|
|
5443
|
-
const
|
|
5525
|
+
const resolvedDir = detection.dir === "." ? projectDir : join19(projectDir, detection.dir);
|
|
5526
|
+
const section = provider.getVerifyDockerfileSection(resolvedDir);
|
|
5444
5527
|
if (section) {
|
|
5445
5528
|
sections.push(section);
|
|
5446
5529
|
sections.push("");
|
|
@@ -5468,7 +5551,7 @@ function isValidStoryKey(storyKey) {
|
|
|
5468
5551
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
5469
5552
|
}
|
|
5470
5553
|
function computeDistHash(projectDir) {
|
|
5471
|
-
const distDir =
|
|
5554
|
+
const distDir = join20(projectDir, "dist");
|
|
5472
5555
|
if (!existsSync21(distDir)) return null;
|
|
5473
5556
|
const hash = createHash("sha256");
|
|
5474
5557
|
const files = collectFiles(distDir).sort();
|
|
@@ -5481,7 +5564,7 @@ function computeDistHash(projectDir) {
|
|
|
5481
5564
|
function collectFiles(dir) {
|
|
5482
5565
|
const results = [];
|
|
5483
5566
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
5484
|
-
const fullPath =
|
|
5567
|
+
const fullPath = join20(dir, entry.name);
|
|
5485
5568
|
if (entry.isDirectory()) {
|
|
5486
5569
|
results.push(...collectFiles(fullPath));
|
|
5487
5570
|
} else {
|
|
@@ -5517,7 +5600,7 @@ function detectProjectType(projectDir) {
|
|
|
5517
5600
|
const rootDetection = allStacks.find((s) => s.dir === ".");
|
|
5518
5601
|
const stack = rootDetection ? rootDetection.stack : null;
|
|
5519
5602
|
if (stack && STACK_TO_PROJECT_TYPE[stack]) return STACK_TO_PROJECT_TYPE[stack];
|
|
5520
|
-
if (existsSync21(
|
|
5603
|
+
if (existsSync21(join20(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
5521
5604
|
return "generic";
|
|
5522
5605
|
}
|
|
5523
5606
|
function buildVerifyImage(options = {}) {
|
|
@@ -5561,18 +5644,18 @@ function buildNodeImage(projectDir) {
|
|
|
5561
5644
|
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
5562
5645
|
if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
5563
5646
|
const tarballName = basename(lastLine);
|
|
5564
|
-
const tarballPath =
|
|
5565
|
-
const buildContext =
|
|
5647
|
+
const tarballPath = join20("/tmp", tarballName);
|
|
5648
|
+
const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
5566
5649
|
mkdirSync6(buildContext, { recursive: true });
|
|
5567
5650
|
try {
|
|
5568
|
-
cpSync(tarballPath,
|
|
5651
|
+
cpSync(tarballPath, join20(buildContext, tarballName));
|
|
5569
5652
|
const dockerfile = generateVerifyDockerfile(projectDir) + `
|
|
5570
5653
|
# Install project from tarball
|
|
5571
5654
|
ARG TARBALL=package.tgz
|
|
5572
5655
|
COPY \${TARBALL} /tmp/\${TARBALL}
|
|
5573
5656
|
RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
|
|
5574
5657
|
`;
|
|
5575
|
-
writeFileSync12(
|
|
5658
|
+
writeFileSync12(join20(buildContext, "Dockerfile"), dockerfile);
|
|
5576
5659
|
execFileSync5("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
|
|
5577
5660
|
cwd: buildContext,
|
|
5578
5661
|
stdio: "pipe",
|
|
@@ -5584,22 +5667,22 @@ RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
|
|
|
5584
5667
|
}
|
|
5585
5668
|
}
|
|
5586
5669
|
function buildPythonImage(projectDir) {
|
|
5587
|
-
const distDir =
|
|
5670
|
+
const distDir = join20(projectDir, "dist");
|
|
5588
5671
|
const distFiles = readdirSync6(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
|
|
5589
5672
|
if (distFiles.length === 0) {
|
|
5590
5673
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
5591
5674
|
}
|
|
5592
5675
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
5593
|
-
const buildContext =
|
|
5676
|
+
const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
5594
5677
|
mkdirSync6(buildContext, { recursive: true });
|
|
5595
5678
|
try {
|
|
5596
|
-
cpSync(
|
|
5679
|
+
cpSync(join20(distDir, distFile), join20(buildContext, distFile));
|
|
5597
5680
|
const dockerfile = generateVerifyDockerfile(projectDir) + `
|
|
5598
5681
|
# Install project from distribution
|
|
5599
5682
|
COPY ${distFile} /tmp/${distFile}
|
|
5600
5683
|
RUN pip install --break-system-packages /tmp/${distFile} && rm /tmp/${distFile}
|
|
5601
5684
|
`;
|
|
5602
|
-
writeFileSync12(
|
|
5685
|
+
writeFileSync12(join20(buildContext, "Dockerfile"), dockerfile);
|
|
5603
5686
|
execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
5604
5687
|
cwd: buildContext,
|
|
5605
5688
|
stdio: "pipe",
|
|
@@ -5614,19 +5697,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
5614
5697
|
if (!isValidStoryKey(storyKey)) {
|
|
5615
5698
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
5616
5699
|
}
|
|
5617
|
-
const storyFile =
|
|
5700
|
+
const storyFile = join20(root, STORY_DIR, `${storyKey}.md`);
|
|
5618
5701
|
if (!existsSync21(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
|
|
5619
5702
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
5620
5703
|
if (existsSync21(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
5621
5704
|
mkdirSync6(workspace, { recursive: true });
|
|
5622
|
-
cpSync(storyFile,
|
|
5623
|
-
const readmePath =
|
|
5624
|
-
if (existsSync21(readmePath)) cpSync(readmePath,
|
|
5625
|
-
const docsDir =
|
|
5705
|
+
cpSync(storyFile, join20(workspace, "story.md"));
|
|
5706
|
+
const readmePath = join20(root, "README.md");
|
|
5707
|
+
if (existsSync21(readmePath)) cpSync(readmePath, join20(workspace, "README.md"));
|
|
5708
|
+
const docsDir = join20(root, "docs");
|
|
5626
5709
|
if (existsSync21(docsDir) && statSync5(docsDir).isDirectory()) {
|
|
5627
|
-
cpSync(docsDir,
|
|
5710
|
+
cpSync(docsDir, join20(workspace, "docs"), { recursive: true });
|
|
5628
5711
|
}
|
|
5629
|
-
mkdirSync6(
|
|
5712
|
+
mkdirSync6(join20(workspace, "verification"), { recursive: true });
|
|
5630
5713
|
return workspace;
|
|
5631
5714
|
}
|
|
5632
5715
|
function checkVerifyEnv() {
|
|
@@ -5679,18 +5762,18 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
5679
5762
|
}
|
|
5680
5763
|
}
|
|
5681
5764
|
function buildPluginImage(projectDir) {
|
|
5682
|
-
const buildContext =
|
|
5765
|
+
const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
5683
5766
|
mkdirSync6(buildContext, { recursive: true });
|
|
5684
5767
|
try {
|
|
5685
|
-
const pluginDir =
|
|
5686
|
-
cpSync(pluginDir,
|
|
5768
|
+
const pluginDir = join20(projectDir, ".claude-plugin");
|
|
5769
|
+
cpSync(pluginDir, join20(buildContext, ".claude-plugin"), { recursive: true });
|
|
5687
5770
|
for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
|
|
5688
|
-
const src =
|
|
5771
|
+
const src = join20(projectDir, dir);
|
|
5689
5772
|
if (existsSync21(src) && statSync5(src).isDirectory()) {
|
|
5690
|
-
cpSync(src,
|
|
5773
|
+
cpSync(src, join20(buildContext, dir), { recursive: true });
|
|
5691
5774
|
}
|
|
5692
5775
|
}
|
|
5693
|
-
writeFileSync12(
|
|
5776
|
+
writeFileSync12(join20(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
|
|
5694
5777
|
execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
5695
5778
|
cwd: buildContext,
|
|
5696
5779
|
stdio: "pipe",
|
|
@@ -5701,10 +5784,10 @@ function buildPluginImage(projectDir) {
|
|
|
5701
5784
|
}
|
|
5702
5785
|
}
|
|
5703
5786
|
function buildSimpleImage(projectDir, timeout = 12e4) {
|
|
5704
|
-
const buildContext =
|
|
5787
|
+
const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
5705
5788
|
mkdirSync6(buildContext, { recursive: true });
|
|
5706
5789
|
try {
|
|
5707
|
-
writeFileSync12(
|
|
5790
|
+
writeFileSync12(join20(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
|
|
5708
5791
|
execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
5709
5792
|
cwd: buildContext,
|
|
5710
5793
|
stdio: "pipe",
|
|
@@ -5786,7 +5869,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
5786
5869
|
return;
|
|
5787
5870
|
}
|
|
5788
5871
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
5789
|
-
const retroPath =
|
|
5872
|
+
const retroPath = join21(root, STORY_DIR2, retroFile);
|
|
5790
5873
|
if (!existsSync22(retroPath)) {
|
|
5791
5874
|
if (isJson) {
|
|
5792
5875
|
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
@@ -5804,7 +5887,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
5804
5887
|
warn(`Failed to update sprint status: ${message}`);
|
|
5805
5888
|
}
|
|
5806
5889
|
if (isJson) {
|
|
5807
|
-
jsonOutput({ status: "ok", epic: epicNum, retroFile:
|
|
5890
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join21(STORY_DIR2, retroFile) });
|
|
5808
5891
|
} else {
|
|
5809
5892
|
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
5810
5893
|
}
|
|
@@ -5815,7 +5898,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
5815
5898
|
process.exitCode = 1;
|
|
5816
5899
|
return;
|
|
5817
5900
|
}
|
|
5818
|
-
const readmePath =
|
|
5901
|
+
const readmePath = join21(root, "README.md");
|
|
5819
5902
|
if (!existsSync22(readmePath)) {
|
|
5820
5903
|
if (isJson) {
|
|
5821
5904
|
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
@@ -5825,7 +5908,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
5825
5908
|
process.exitCode = 1;
|
|
5826
5909
|
return;
|
|
5827
5910
|
}
|
|
5828
|
-
const storyFilePath =
|
|
5911
|
+
const storyFilePath = join21(root, STORY_DIR2, `${storyId}.md`);
|
|
5829
5912
|
if (!existsSync22(storyFilePath)) {
|
|
5830
5913
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
5831
5914
|
process.exitCode = 1;
|
|
@@ -5866,7 +5949,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
5866
5949
|
return;
|
|
5867
5950
|
}
|
|
5868
5951
|
const storyTitle = extractStoryTitle(storyFilePath);
|
|
5869
|
-
const expectedProofPath =
|
|
5952
|
+
const expectedProofPath = join21(root, "verification", `${storyId}-proof.md`);
|
|
5870
5953
|
const proofPath = existsSync22(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
|
|
5871
5954
|
const proofQuality = validateProofQuality(proofPath);
|
|
5872
5955
|
if (!proofQuality.passed) {
|
|
@@ -5996,8 +6079,15 @@ var ELK_ENDPOINTS = {
|
|
|
5996
6079
|
function getDefaultEndpointsForBackend(backend) {
|
|
5997
6080
|
return backend === "elk" ? ELK_ENDPOINTS : DEFAULT_ENDPOINTS;
|
|
5998
6081
|
}
|
|
5999
|
-
function buildScopedEndpoints(endpoints, serviceName) {
|
|
6082
|
+
function buildScopedEndpoints(endpoints, serviceName, backend) {
|
|
6000
6083
|
const encoded = encodeURIComponent(serviceName);
|
|
6084
|
+
if (backend === "elk") {
|
|
6085
|
+
return {
|
|
6086
|
+
logs: `${endpoints.logs}/_search?q=${encodeURIComponent(`service_name:${serviceName}`)}&size=100`,
|
|
6087
|
+
metrics: `${endpoints.metrics}/_search?q=${encodeURIComponent(`service_name:${serviceName}`)}&size=100`,
|
|
6088
|
+
traces: `${endpoints.traces}/_search?q=${encodeURIComponent(`trace_id:* AND service_name:${serviceName}`)}&size=20`
|
|
6089
|
+
};
|
|
6090
|
+
}
|
|
6001
6091
|
return {
|
|
6002
6092
|
logs: `${endpoints.logs}/select/logsql/query?query=${encodeURIComponent(`service_name:${serviceName}`)}`,
|
|
6003
6093
|
metrics: `${endpoints.metrics}/api/v1/query?query=${encodeURIComponent(`{service_name="${serviceName}"}`)}`,
|
|
@@ -6031,12 +6121,12 @@ function resolveEndpoints(state) {
|
|
|
6031
6121
|
|
|
6032
6122
|
// src/lib/onboard-checks.ts
|
|
6033
6123
|
import { existsSync as existsSync26 } from "fs";
|
|
6034
|
-
import { join as
|
|
6124
|
+
import { join as join24, dirname as dirname5 } from "path";
|
|
6035
6125
|
import { fileURLToPath } from "url";
|
|
6036
6126
|
|
|
6037
6127
|
// src/lib/coverage/parser.ts
|
|
6038
6128
|
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
|
|
6039
|
-
import { join as
|
|
6129
|
+
import { join as join22 } from "path";
|
|
6040
6130
|
function parseTestCounts(output) {
|
|
6041
6131
|
const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
|
|
6042
6132
|
if (vitestMatch) {
|
|
@@ -6100,7 +6190,7 @@ function parseVitestCoverage(dir) {
|
|
|
6100
6190
|
}
|
|
6101
6191
|
}
|
|
6102
6192
|
function parsePythonCoverage(dir) {
|
|
6103
|
-
const reportPath =
|
|
6193
|
+
const reportPath = join22(dir, "coverage.json");
|
|
6104
6194
|
if (!existsSync23(reportPath)) {
|
|
6105
6195
|
warn("Coverage report not found at coverage.json");
|
|
6106
6196
|
return 0;
|
|
@@ -6114,7 +6204,7 @@ function parsePythonCoverage(dir) {
|
|
|
6114
6204
|
}
|
|
6115
6205
|
}
|
|
6116
6206
|
function parseTarpaulinCoverage(dir) {
|
|
6117
|
-
const reportPath =
|
|
6207
|
+
const reportPath = join22(dir, "coverage", "tarpaulin-report.json");
|
|
6118
6208
|
if (!existsSync23(reportPath)) {
|
|
6119
6209
|
warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
|
|
6120
6210
|
return 0;
|
|
@@ -6129,8 +6219,8 @@ function parseTarpaulinCoverage(dir) {
|
|
|
6129
6219
|
}
|
|
6130
6220
|
function findCoverageSummary(dir) {
|
|
6131
6221
|
const candidates = [
|
|
6132
|
-
|
|
6133
|
-
|
|
6222
|
+
join22(dir, "coverage", "coverage-summary.json"),
|
|
6223
|
+
join22(dir, "src", "coverage", "coverage-summary.json")
|
|
6134
6224
|
];
|
|
6135
6225
|
for (const p of candidates) {
|
|
6136
6226
|
if (existsSync23(p)) return p;
|
|
@@ -6141,7 +6231,7 @@ function findCoverageSummary(dir) {
|
|
|
6141
6231
|
// src/lib/coverage/runner.ts
|
|
6142
6232
|
import { execSync as execSync6 } from "child_process";
|
|
6143
6233
|
import { existsSync as existsSync24, readFileSync as readFileSync21 } from "fs";
|
|
6144
|
-
import { join as
|
|
6234
|
+
import { join as join23 } from "path";
|
|
6145
6235
|
function detectCoverageTool(dir) {
|
|
6146
6236
|
const baseDir = dir ?? process.cwd();
|
|
6147
6237
|
const stateHint = getStateToolHint(baseDir);
|
|
@@ -6174,7 +6264,7 @@ function detectRustCoverageTool(dir) {
|
|
|
6174
6264
|
warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
|
|
6175
6265
|
return { tool: "unknown", runCommand: "", reportFormat: "" };
|
|
6176
6266
|
}
|
|
6177
|
-
const cargoPath =
|
|
6267
|
+
const cargoPath = join23(dir, "Cargo.toml");
|
|
6178
6268
|
let isWorkspace = false;
|
|
6179
6269
|
try {
|
|
6180
6270
|
const cargoContent = readFileSync21(cargoPath, "utf-8");
|
|
@@ -6197,8 +6287,8 @@ function getStateToolHint(dir) {
|
|
|
6197
6287
|
}
|
|
6198
6288
|
}
|
|
6199
6289
|
function detectNodeCoverageTool(dir, stateHint) {
|
|
6200
|
-
const hasVitestConfig = existsSync24(
|
|
6201
|
-
const pkgPath =
|
|
6290
|
+
const hasVitestConfig = existsSync24(join23(dir, "vitest.config.ts")) || existsSync24(join23(dir, "vitest.config.js"));
|
|
6291
|
+
const pkgPath = join23(dir, "package.json");
|
|
6202
6292
|
let hasVitestCoverageV8 = false;
|
|
6203
6293
|
let hasVitestCoverageIstanbul = false;
|
|
6204
6294
|
let hasC8 = false;
|
|
@@ -6259,7 +6349,7 @@ function getNodeTestCommand(scripts, runner) {
|
|
|
6259
6349
|
return "npm test";
|
|
6260
6350
|
}
|
|
6261
6351
|
function detectPythonCoverageTool(dir) {
|
|
6262
|
-
const reqPath =
|
|
6352
|
+
const reqPath = join23(dir, "requirements.txt");
|
|
6263
6353
|
if (existsSync24(reqPath)) {
|
|
6264
6354
|
try {
|
|
6265
6355
|
const content = readFileSync21(reqPath, "utf-8");
|
|
@@ -6273,7 +6363,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
6273
6363
|
} catch {
|
|
6274
6364
|
}
|
|
6275
6365
|
}
|
|
6276
|
-
const pyprojectPath =
|
|
6366
|
+
const pyprojectPath = join23(dir, "pyproject.toml");
|
|
6277
6367
|
if (existsSync24(pyprojectPath)) {
|
|
6278
6368
|
try {
|
|
6279
6369
|
const content = readFileSync21(pyprojectPath, "utf-8");
|
|
@@ -6472,7 +6562,7 @@ function checkBmadInstalled(dir) {
|
|
|
6472
6562
|
function checkHooksRegistered(dir) {
|
|
6473
6563
|
const __filename = fileURLToPath(import.meta.url);
|
|
6474
6564
|
const __dirname = dirname5(__filename);
|
|
6475
|
-
const hooksPath =
|
|
6565
|
+
const hooksPath = join24(__dirname, "..", "..", "hooks", "hooks.json");
|
|
6476
6566
|
return { ok: existsSync26(hooksPath) };
|
|
6477
6567
|
}
|
|
6478
6568
|
function runPreconditions(dir) {
|
|
@@ -6603,7 +6693,7 @@ function handleFullStatus(isJson) {
|
|
|
6603
6693
|
const serviceName = state.otlp?.service_name;
|
|
6604
6694
|
if (serviceName) {
|
|
6605
6695
|
const endpoints = resolveEndpoints(state);
|
|
6606
|
-
const scoped = buildScopedEndpoints(endpoints, serviceName);
|
|
6696
|
+
const scoped = buildScopedEndpoints(endpoints, serviceName, state.otlp?.backend);
|
|
6607
6697
|
console.log(` Scoped: logs=${scoped.logs} metrics=${scoped.metrics} traces=${scoped.traces}`);
|
|
6608
6698
|
}
|
|
6609
6699
|
printBeadsSummary();
|
|
@@ -6671,7 +6761,7 @@ function handleFullStatusJson(state) {
|
|
|
6671
6761
|
}
|
|
6672
6762
|
const endpoints = resolveEndpoints(state);
|
|
6673
6763
|
const serviceName = state.otlp?.service_name;
|
|
6674
|
-
const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName) : void 0;
|
|
6764
|
+
const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName, state.otlp?.backend) : void 0;
|
|
6675
6765
|
const beads = getBeadsData();
|
|
6676
6766
|
const onboarding = getOnboardingProgressData();
|
|
6677
6767
|
const sprint = getSprintReportData();
|
|
@@ -7112,7 +7202,7 @@ function registerStatusCommand(program) {
|
|
|
7112
7202
|
|
|
7113
7203
|
// src/modules/audit/dimensions.ts
|
|
7114
7204
|
import { existsSync as existsSync27, readdirSync as readdirSync7 } from "fs";
|
|
7115
|
-
import { join as
|
|
7205
|
+
import { join as join25 } from "path";
|
|
7116
7206
|
function gap(dimension, description, suggestedFix) {
|
|
7117
7207
|
return { dimension, description, suggestedFix };
|
|
7118
7208
|
}
|
|
@@ -7224,15 +7314,15 @@ function checkDocumentation(projectDir) {
|
|
|
7224
7314
|
function checkVerification(projectDir) {
|
|
7225
7315
|
try {
|
|
7226
7316
|
const gaps = [];
|
|
7227
|
-
const sprintPath =
|
|
7317
|
+
const sprintPath = join25(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
7228
7318
|
if (!existsSync27(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
7229
|
-
const vDir =
|
|
7319
|
+
const vDir = join25(projectDir, "verification");
|
|
7230
7320
|
let proofCount = 0, totalChecked = 0;
|
|
7231
7321
|
if (existsSync27(vDir)) {
|
|
7232
7322
|
for (const file of readdirSafe(vDir)) {
|
|
7233
7323
|
if (!file.endsWith("-proof.md")) continue;
|
|
7234
7324
|
totalChecked++;
|
|
7235
|
-
const r = parseProof(
|
|
7325
|
+
const r = parseProof(join25(vDir, file));
|
|
7236
7326
|
if (isOk(r) && r.data.passed) {
|
|
7237
7327
|
proofCount++;
|
|
7238
7328
|
} else {
|
|
@@ -7310,7 +7400,7 @@ function formatAuditJson(result) {
|
|
|
7310
7400
|
|
|
7311
7401
|
// src/modules/audit/fix-generator.ts
|
|
7312
7402
|
import { existsSync as existsSync28, writeFileSync as writeFileSync13, mkdirSync as mkdirSync7 } from "fs";
|
|
7313
|
-
import { join as
|
|
7403
|
+
import { join as join26, dirname as dirname6 } from "path";
|
|
7314
7404
|
function buildStoryKey(gap2, index) {
|
|
7315
7405
|
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
7316
7406
|
return `audit-fix-${safeDimension}-${index}`;
|
|
@@ -7342,7 +7432,7 @@ function generateFixStories(auditResult) {
|
|
|
7342
7432
|
const stories = [];
|
|
7343
7433
|
let created = 0;
|
|
7344
7434
|
let skipped = 0;
|
|
7345
|
-
const artifactsDir =
|
|
7435
|
+
const artifactsDir = join26(
|
|
7346
7436
|
process.cwd(),
|
|
7347
7437
|
"_bmad-output",
|
|
7348
7438
|
"implementation-artifacts"
|
|
@@ -7351,7 +7441,7 @@ function generateFixStories(auditResult) {
|
|
|
7351
7441
|
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
7352
7442
|
const gap2 = dimension.gaps[i];
|
|
7353
7443
|
const key = buildStoryKey(gap2, i + 1);
|
|
7354
|
-
const filePath =
|
|
7444
|
+
const filePath = join26(artifactsDir, `${key}.md`);
|
|
7355
7445
|
if (existsSync28(filePath)) {
|
|
7356
7446
|
stories.push({
|
|
7357
7447
|
key,
|
|
@@ -7542,7 +7632,7 @@ function registerOnboardCommand(program) {
|
|
|
7542
7632
|
|
|
7543
7633
|
// src/commands/teardown.ts
|
|
7544
7634
|
import { existsSync as existsSync29, unlinkSync as unlinkSync3, readFileSync as readFileSync23, writeFileSync as writeFileSync14, rmSync as rmSync2 } from "fs";
|
|
7545
|
-
import { join as
|
|
7635
|
+
import { join as join27 } from "path";
|
|
7546
7636
|
function buildDefaultResult() {
|
|
7547
7637
|
return {
|
|
7548
7638
|
status: "ok",
|
|
@@ -7588,7 +7678,7 @@ function registerTeardownCommand(program) {
|
|
|
7588
7678
|
} else if (otlpMode === "remote-routed") {
|
|
7589
7679
|
if (!options.keepDocker) {
|
|
7590
7680
|
try {
|
|
7591
|
-
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-
|
|
7681
|
+
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-VHOP56YP.js");
|
|
7592
7682
|
stopCollectorOnly2();
|
|
7593
7683
|
result.docker.stopped = true;
|
|
7594
7684
|
if (!isJson) {
|
|
@@ -7620,7 +7710,7 @@ function registerTeardownCommand(program) {
|
|
|
7620
7710
|
info("Shared stack: kept running (other projects may use it)");
|
|
7621
7711
|
}
|
|
7622
7712
|
} else if (isLegacyStack) {
|
|
7623
|
-
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-
|
|
7713
|
+
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-VHOP56YP.js");
|
|
7624
7714
|
let stackRunning = false;
|
|
7625
7715
|
try {
|
|
7626
7716
|
stackRunning = isStackRunning2(composeFile);
|
|
@@ -7645,7 +7735,7 @@ function registerTeardownCommand(program) {
|
|
|
7645
7735
|
info("Docker stack: not running, skipping");
|
|
7646
7736
|
}
|
|
7647
7737
|
}
|
|
7648
|
-
const composeFilePath =
|
|
7738
|
+
const composeFilePath = join27(projectDir, composeFile);
|
|
7649
7739
|
if (existsSync29(composeFilePath)) {
|
|
7650
7740
|
unlinkSync3(composeFilePath);
|
|
7651
7741
|
result.removed.push(composeFile);
|
|
@@ -7653,7 +7743,7 @@ function registerTeardownCommand(program) {
|
|
|
7653
7743
|
ok(`Removed: ${composeFile}`);
|
|
7654
7744
|
}
|
|
7655
7745
|
}
|
|
7656
|
-
const otelConfigPath =
|
|
7746
|
+
const otelConfigPath = join27(projectDir, "otel-collector-config.yaml");
|
|
7657
7747
|
if (existsSync29(otelConfigPath)) {
|
|
7658
7748
|
unlinkSync3(otelConfigPath);
|
|
7659
7749
|
result.removed.push("otel-collector-config.yaml");
|
|
@@ -7664,7 +7754,7 @@ function registerTeardownCommand(program) {
|
|
|
7664
7754
|
}
|
|
7665
7755
|
let patchesRemoved = 0;
|
|
7666
7756
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
7667
|
-
const filePath =
|
|
7757
|
+
const filePath = join27(projectDir, "_bmad", relativePath);
|
|
7668
7758
|
if (!existsSync29(filePath)) {
|
|
7669
7759
|
continue;
|
|
7670
7760
|
}
|
|
@@ -7686,7 +7776,7 @@ function registerTeardownCommand(program) {
|
|
|
7686
7776
|
}
|
|
7687
7777
|
const stacks = state.stacks ?? (state.stack ? [state.stack] : []);
|
|
7688
7778
|
if (state.otlp?.enabled && stacks.includes("nodejs")) {
|
|
7689
|
-
const pkgPath =
|
|
7779
|
+
const pkgPath = join27(projectDir, "package.json");
|
|
7690
7780
|
if (existsSync29(pkgPath)) {
|
|
7691
7781
|
try {
|
|
7692
7782
|
const raw = readFileSync23(pkgPath, "utf-8");
|
|
@@ -7729,7 +7819,7 @@ function registerTeardownCommand(program) {
|
|
|
7729
7819
|
}
|
|
7730
7820
|
}
|
|
7731
7821
|
}
|
|
7732
|
-
const harnessDir =
|
|
7822
|
+
const harnessDir = join27(projectDir, ".harness");
|
|
7733
7823
|
if (existsSync29(harnessDir)) {
|
|
7734
7824
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
7735
7825
|
result.removed.push(".harness/");
|
|
@@ -8488,7 +8578,7 @@ function registerQueryCommand(program) {
|
|
|
8488
8578
|
|
|
8489
8579
|
// src/commands/retro-import.ts
|
|
8490
8580
|
import { existsSync as existsSync30, readFileSync as readFileSync24 } from "fs";
|
|
8491
|
-
import { join as
|
|
8581
|
+
import { join as join28 } from "path";
|
|
8492
8582
|
|
|
8493
8583
|
// src/lib/retro-parser.ts
|
|
8494
8584
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -8657,7 +8747,7 @@ function registerRetroImportCommand(program) {
|
|
|
8657
8747
|
return;
|
|
8658
8748
|
}
|
|
8659
8749
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
8660
|
-
const retroPath =
|
|
8750
|
+
const retroPath = join28(root, STORY_DIR3, retroFile);
|
|
8661
8751
|
if (!existsSync30(retroPath)) {
|
|
8662
8752
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
8663
8753
|
process.exitCode = 1;
|
|
@@ -9046,10 +9136,10 @@ function registerVerifyEnvCommand(program) {
|
|
|
9046
9136
|
}
|
|
9047
9137
|
|
|
9048
9138
|
// src/commands/retry.ts
|
|
9049
|
-
import { join as
|
|
9139
|
+
import { join as join30 } from "path";
|
|
9050
9140
|
|
|
9051
9141
|
// src/lib/retry-state.ts
|
|
9052
|
-
import { join as
|
|
9142
|
+
import { join as join29 } from "path";
|
|
9053
9143
|
function mutateState(mutator) {
|
|
9054
9144
|
const result = getSprintState2();
|
|
9055
9145
|
if (!result.success) return;
|
|
@@ -9110,7 +9200,7 @@ function registerRetryCommand(program) {
|
|
|
9110
9200
|
program.command("retry").description("Manage retry state for stories").option("--reset", "Clear retry counters and flagged stories").option("--story <key>", "Target a specific story key (used with --reset or --status)").option("--status", "Show retry status for all stories").action((_options, cmd) => {
|
|
9111
9201
|
const opts = cmd.optsWithGlobals();
|
|
9112
9202
|
const isJson = opts.json === true;
|
|
9113
|
-
const dir =
|
|
9203
|
+
const dir = join30(process.cwd(), RALPH_SUBDIR);
|
|
9114
9204
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
9115
9205
|
if (isJson) {
|
|
9116
9206
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -9520,7 +9610,7 @@ function registerAuditCommand(program) {
|
|
|
9520
9610
|
|
|
9521
9611
|
// src/commands/stats.ts
|
|
9522
9612
|
import { existsSync as existsSync31, readdirSync as readdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync15 } from "fs";
|
|
9523
|
-
import { join as
|
|
9613
|
+
import { join as join31 } from "path";
|
|
9524
9614
|
var RATES = {
|
|
9525
9615
|
input: 15,
|
|
9526
9616
|
output: 75,
|
|
@@ -9605,8 +9695,8 @@ function parseLogFile(filePath, report) {
|
|
|
9605
9695
|
}
|
|
9606
9696
|
}
|
|
9607
9697
|
function generateReport3(projectDir) {
|
|
9608
|
-
const logsDir =
|
|
9609
|
-
const logFiles = readdirSync8(logsDir).filter((f) => f.startsWith("claude_output_") && f.endsWith(".log")).sort().map((f) =>
|
|
9698
|
+
const logsDir = join31(projectDir, "ralph", "logs");
|
|
9699
|
+
const logFiles = readdirSync8(logsDir).filter((f) => f.startsWith("claude_output_") && f.endsWith(".log")).sort().map((f) => join31(logsDir, f));
|
|
9610
9700
|
const report = {
|
|
9611
9701
|
byPhase: /* @__PURE__ */ new Map(),
|
|
9612
9702
|
byStory: /* @__PURE__ */ new Map(),
|
|
@@ -9705,7 +9795,7 @@ function registerStatsCommand(program) {
|
|
|
9705
9795
|
const globalOpts = cmd.optsWithGlobals();
|
|
9706
9796
|
const isJson = !!globalOpts.json;
|
|
9707
9797
|
const projectDir = process.cwd();
|
|
9708
|
-
const logsDir =
|
|
9798
|
+
const logsDir = join31(projectDir, "ralph", "logs");
|
|
9709
9799
|
if (!existsSync31(logsDir)) {
|
|
9710
9800
|
fail("No ralph/logs/ directory found \u2014 run codeharness run first");
|
|
9711
9801
|
process.exitCode = 1;
|
|
@@ -9719,7 +9809,7 @@ function registerStatsCommand(program) {
|
|
|
9719
9809
|
const formatted = formatReport2(report);
|
|
9720
9810
|
console.log(formatted);
|
|
9721
9811
|
if (options.save) {
|
|
9722
|
-
const outPath =
|
|
9812
|
+
const outPath = join31(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
|
|
9723
9813
|
writeFileSync15(outPath, formatted, "utf-8");
|
|
9724
9814
|
ok(`Report saved to ${outPath}`);
|
|
9725
9815
|
}
|
|
@@ -9727,7 +9817,7 @@ function registerStatsCommand(program) {
|
|
|
9727
9817
|
}
|
|
9728
9818
|
|
|
9729
9819
|
// src/index.ts
|
|
9730
|
-
var VERSION = true ? "0.26.
|
|
9820
|
+
var VERSION = true ? "0.26.5" : "0.0.0-dev";
|
|
9731
9821
|
function createProgram() {
|
|
9732
9822
|
const program = new Command();
|
|
9733
9823
|
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
package/patches/AGENTS.md
CHANGED
|
@@ -12,7 +12,7 @@ prevent recurrence of observed failures.
|
|
|
12
12
|
patches/
|
|
13
13
|
dev/enforcement.md — Dev agent guardrails
|
|
14
14
|
review/enforcement.md — Review gates (proof quality, coverage)
|
|
15
|
-
verify/story-verification.md —
|
|
15
|
+
verify/story-verification.md — Tier-appropriate proof requirements
|
|
16
16
|
sprint/planning.md — Sprint planning pre-checks
|
|
17
17
|
retro/enforcement.md — Retrospective quality metrics
|
|
18
18
|
```
|
|
@@ -4,7 +4,7 @@ Dev agents repeatedly shipped code without reading module conventions (AGENTS.md
|
|
|
4
4
|
skipped observability checks, and produced features that could not be verified
|
|
5
5
|
from outside the source tree. This patch enforces architecture awareness,
|
|
6
6
|
observability validation, documentation hygiene, test coverage gates, and
|
|
7
|
-
|
|
7
|
+
verification tier awareness — all operational failures observed in prior sprints.
|
|
8
8
|
(FR33, FR34, NFR20)
|
|
9
9
|
|
|
10
10
|
## Codeharness Development Enforcement
|
|
@@ -35,14 +35,23 @@ After running tests, verify telemetry is flowing:
|
|
|
35
35
|
- Coverage gate: 100% of new/changed code
|
|
36
36
|
- Run `npm test` / `pytest` and verify no regressions
|
|
37
37
|
|
|
38
|
-
###
|
|
38
|
+
### Verification Tier Awareness
|
|
39
39
|
|
|
40
|
-
Write code that can be verified
|
|
41
|
-
- Can a user exercise this feature from the CLI alone?
|
|
42
|
-
- Is the behavior documented in README.md?
|
|
43
|
-
- Would a verifier with NO source access be able to tell if this works?
|
|
40
|
+
Write code that can be verified at the appropriate tier. The four verification tiers determine what evidence is needed to prove an AC works:
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
- **`test-provable`** — Code must be testable via `npm test` / `npm run build`. Ensure functions have test coverage, outputs are greppable, and build artifacts are inspectable. No running app required.
|
|
43
|
+
- **`runtime-provable`** — Code must be exercisable via CLI or local server. Ensure the binary/CLI produces verifiable stdout, exit codes, or HTTP responses without needing Docker.
|
|
44
|
+
- **`environment-provable`** — Code must work in a Docker verification environment. Ensure the Dockerfile is current, services start correctly, and `docker exec` can exercise the feature. Observability queries should return expected log/trace events.
|
|
45
|
+
- **`escalate`** — Reserved for ACs that genuinely cannot be automated (physical hardware, paid external APIs). This is rare — exhaust all automated approaches first.
|
|
46
|
+
|
|
47
|
+
Ask yourself:
|
|
48
|
+
- What tier is this story tagged with?
|
|
49
|
+
- Does my implementation produce the evidence that tier requires?
|
|
50
|
+
- If `test-provable`: are my functions testable and my outputs greppable?
|
|
51
|
+
- If `runtime-provable`: can I run the CLI/server and verify output locally?
|
|
52
|
+
- If `environment-provable`: does `docker exec` work? Are logs flowing to the observability stack?
|
|
53
|
+
|
|
54
|
+
If the answer is "no", the feature has a testability gap — fix the code to be verifiable at the appropriate tier.
|
|
46
55
|
|
|
47
56
|
### Dockerfile Maintenance
|
|
48
57
|
|
|
@@ -11,7 +11,7 @@ quality trends, and mandatory concrete action items with owners.
|
|
|
11
11
|
|
|
12
12
|
### Verification Effectiveness
|
|
13
13
|
|
|
14
|
-
- How many ACs were caught by
|
|
14
|
+
- How many ACs were caught by tier-appropriate verification vs slipped through?
|
|
15
15
|
- Were there false positives (proof said PASS but feature was broken)?
|
|
16
16
|
- Were there false negatives (proof said FAIL but feature actually works)?
|
|
17
17
|
- Time spent on verification — is it proportional to value?
|
|
@@ -20,7 +20,7 @@ quality trends, and mandatory concrete action items with owners.
|
|
|
20
20
|
|
|
21
21
|
- Did the verifier hang on permissions? (check for `--allowedTools` issues)
|
|
22
22
|
- Did stories get stuck in verify→dev loops? (check `attempts` counter)
|
|
23
|
-
- Were stories
|
|
23
|
+
- Were stories assigned the wrong verification tier?
|
|
24
24
|
- Did the verify parser correctly detect `[FAIL]` verdicts?
|
|
25
25
|
|
|
26
26
|
### Documentation Health
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
## WHY
|
|
2
2
|
|
|
3
3
|
Review agents approved stories without verifying proof documents existed or
|
|
4
|
-
checking that evidence
|
|
4
|
+
checking that evidence matched the story's verification tier. Stories passed review
|
|
5
5
|
with fabricated output and missing coverage data. This patch enforces proof
|
|
6
|
-
existence,
|
|
6
|
+
existence, tier-appropriate evidence quality, and coverage delta reporting as hard
|
|
7
7
|
gates before a story can leave review.
|
|
8
8
|
(FR33, FR34, NFR20)
|
|
9
9
|
|
|
@@ -18,13 +18,34 @@ gates before a story can leave review.
|
|
|
18
18
|
|
|
19
19
|
### Proof Quality Checks
|
|
20
20
|
|
|
21
|
-
The proof must pass
|
|
21
|
+
The proof must pass tier-appropriate evidence enforcement. The required evidence depends on the story's verification tier:
|
|
22
|
+
|
|
23
|
+
#### `test-provable` stories
|
|
24
|
+
- Evidence comes from build output, test results, and grep/read of code or generated artifacts
|
|
25
|
+
- `npm test` / `npm run build` output is the primary evidence
|
|
26
|
+
- Source-level assertions (grep against `src/`) are acceptable — this IS the verification method for this tier
|
|
27
|
+
- `docker exec` evidence is NOT required
|
|
28
|
+
- Each AC section must show actual test output or build results
|
|
29
|
+
|
|
30
|
+
#### `runtime-provable` stories
|
|
31
|
+
- Evidence comes from running the actual binary, CLI, or server
|
|
32
|
+
- Process execution output (stdout, stderr, exit codes) is the primary evidence
|
|
33
|
+
- HTTP responses from a locally running server are acceptable
|
|
34
|
+
- `docker exec` evidence is NOT required
|
|
35
|
+
- Each AC section must show actual command execution and output
|
|
36
|
+
|
|
37
|
+
#### `environment-provable` stories
|
|
22
38
|
- Commands run via `docker exec` (not direct host access)
|
|
23
39
|
- Less than 50% of evidence commands are `grep` against `src/`
|
|
24
40
|
- Each AC section has at least one `docker exec`, `docker ps/logs`, or observability query
|
|
25
41
|
- `[FAIL]` verdicts outside code blocks cause the proof to fail
|
|
26
42
|
- `[ESCALATE]` is acceptable only when all automated approaches are exhausted
|
|
27
43
|
|
|
44
|
+
#### `escalate` stories
|
|
45
|
+
- Human judgment is required — automated evidence may be partial or absent
|
|
46
|
+
- Proof document must explain why automation is not possible
|
|
47
|
+
- `[ESCALATE]` verdict is expected and acceptable
|
|
48
|
+
|
|
28
49
|
### Observability
|
|
29
50
|
|
|
30
51
|
Run `semgrep scan --config patches/observability/ --config patches/error-handling/ --json` against changed files and report gaps.
|
|
@@ -1,35 +1,49 @@
|
|
|
1
1
|
## WHY
|
|
2
2
|
|
|
3
3
|
Stories were marked "done" with no proof artifact, or with proofs that only
|
|
4
|
-
grepped source code instead of exercising the feature
|
|
5
|
-
|
|
4
|
+
grepped source code instead of exercising the feature at the appropriate
|
|
5
|
+
verification tier. This patch mandates tier-appropriate proof documents,
|
|
6
6
|
verification tags per AC, and test coverage targets — preventing regressions
|
|
7
|
-
from being hidden behind
|
|
7
|
+
from being hidden behind inadequate evidence.
|
|
8
8
|
(FR33, FR36, NFR20)
|
|
9
9
|
|
|
10
10
|
## Verification Requirements
|
|
11
11
|
|
|
12
|
-
Every story must produce a **
|
|
12
|
+
Every story must produce a **proof document** with evidence appropriate to its verification tier.
|
|
13
13
|
|
|
14
14
|
### Proof Standard
|
|
15
15
|
|
|
16
16
|
- Proof document at `verification/<story-key>-proof.md`
|
|
17
|
-
- Each AC gets a `## AC N:` section with
|
|
18
|
-
- Evidence must come from running the installed CLI/tool, not from grepping source
|
|
17
|
+
- Each AC gets a `## AC N:` section with tier-appropriate evidence and captured output
|
|
19
18
|
- `[FAIL]` = AC failed with evidence showing what went wrong
|
|
20
19
|
- `[ESCALATE]` = AC genuinely cannot be automated (last resort — try everything first)
|
|
21
20
|
|
|
21
|
+
**Tier-dependent evidence rules:**
|
|
22
|
+
|
|
23
|
+
- **`test-provable`** — Evidence comes from build + test output + grep/read of code or artifacts. Run `npm test` or `npm run build`, capture results. Source-level assertions are the primary verification method. No running app or Docker required.
|
|
24
|
+
- **`runtime-provable`** — Evidence comes from running the actual binary/server and interacting with it. Start the process, make requests or run commands, capture stdout/stderr/exit codes. No Docker stack required.
|
|
25
|
+
- **`environment-provable`** — Evidence comes from `docker exec` commands and observability queries. Full Docker verification environment required. Each AC section needs at least one `docker exec`, `docker ps/logs`, or observability query. Evidence must come from running the installed CLI/tool in Docker, not from grepping source.
|
|
26
|
+
- **`escalate`** — Human judgment required. Document why automation is not possible. `[ESCALATE]` verdict is expected.
|
|
27
|
+
|
|
22
28
|
### Verification Tags
|
|
23
29
|
|
|
24
|
-
For each AC, append a tag indicating verification
|
|
25
|
-
- `<!-- verification:
|
|
26
|
-
- `<!-- verification:
|
|
30
|
+
For each AC, append a tag indicating its verification tier:
|
|
31
|
+
- `<!-- verification: test-provable -->` — Can be verified by building and running tests. Evidence: build output, test results, grep/read of code. No running app needed.
|
|
32
|
+
- `<!-- verification: runtime-provable -->` — Requires running the actual binary/CLI/server. Evidence: process output, HTTP responses, exit codes. No Docker stack needed.
|
|
33
|
+
- `<!-- verification: environment-provable -->` — Requires full Docker environment with observability. Evidence: `docker exec` commands, VictoriaLogs queries, multi-service interaction.
|
|
34
|
+
- `<!-- verification: escalate -->` — Cannot be automated. Requires human judgment, physical hardware, or paid external services.
|
|
35
|
+
|
|
36
|
+
**Decision criteria:**
|
|
37
|
+
1. Can you prove it with `npm test` or `npm run build` alone? → `test-provable`
|
|
38
|
+
2. Do you need to run the actual binary/server locally? → `runtime-provable`
|
|
39
|
+
3. Do you need Docker, external services, or observability? → `environment-provable`
|
|
40
|
+
4. Have you exhausted all automated approaches? → `escalate`
|
|
27
41
|
|
|
28
|
-
**Do not over-tag.**
|
|
42
|
+
**Do not over-tag.** Most stories are `test-provable` or `runtime-provable`. Only use `environment-provable` when Docker infrastructure is genuinely needed. Only use `escalate` as a last resort.
|
|
29
43
|
|
|
30
44
|
### Observability Evidence
|
|
31
45
|
|
|
32
|
-
After each `docker exec` command, query the observability backend for log events from the last 30 seconds.
|
|
46
|
+
After each `docker exec` command (applicable to `environment-provable` stories), query the observability backend for log events from the last 30 seconds.
|
|
33
47
|
Use the configured VictoriaLogs endpoint (default: `http://localhost:9428`):
|
|
34
48
|
|
|
35
49
|
```bash
|
package/ralph/ralph.sh
CHANGED
|
@@ -279,13 +279,10 @@ check_sprint_complete() {
|
|
|
279
279
|
local done_count=0
|
|
280
280
|
local flagged_count=0
|
|
281
281
|
|
|
282
|
-
# Load flagged stories for
|
|
283
|
-
local
|
|
282
|
+
# Load flagged stories into a newline-separated string for lookup
|
|
283
|
+
local flagged_list=""
|
|
284
284
|
if [[ -f "$FLAGGED_STORIES_FILE" ]]; then
|
|
285
|
-
|
|
286
|
-
flagged_key=$(echo "$flagged_key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
287
|
-
[[ -n "$flagged_key" ]] && flagged_map["$flagged_key"]=1
|
|
288
|
-
done < "$FLAGGED_STORIES_FILE"
|
|
285
|
+
flagged_list=$(sed 's/^[[:space:]]*//;s/[[:space:]]*$//' "$FLAGGED_STORIES_FILE" | grep -v '^$')
|
|
289
286
|
fi
|
|
290
287
|
|
|
291
288
|
while IFS=: read -r key value; do
|
|
@@ -301,7 +298,7 @@ check_sprint_complete() {
|
|
|
301
298
|
total=$((total + 1))
|
|
302
299
|
if [[ "$value" == "done" ]]; then
|
|
303
300
|
done_count=$((done_count + 1))
|
|
304
|
-
elif [[ -n "$
|
|
301
|
+
elif [[ -n "$flagged_list" ]] && echo "$flagged_list" | grep -qxF "$key"; then
|
|
305
302
|
# Retry-exhausted/flagged stories count as "effectively done"
|
|
306
303
|
# — no autonomous work can be done on them
|
|
307
304
|
flagged_count=$((flagged_count + 1))
|
|
@@ -341,7 +338,7 @@ get_task_counts() {
|
|
|
341
338
|
|
|
342
339
|
if [[ "$key" =~ ^[0-9]+-[0-9]+- ]]; then
|
|
343
340
|
total=$((total + 1))
|
|
344
|
-
if [[ "$value" == "done" ]]; then
|
|
341
|
+
if [[ "$value" == "done" || "$value" == "failed" ]]; then
|
|
345
342
|
completed=$((completed + 1))
|
|
346
343
|
fi
|
|
347
344
|
fi
|
|
@@ -442,6 +439,14 @@ flag_story() {
|
|
|
442
439
|
if ! is_story_flagged "$story_key"; then
|
|
443
440
|
echo "$story_key" >> "$FLAGGED_STORIES_FILE"
|
|
444
441
|
fi
|
|
442
|
+
|
|
443
|
+
# Also update sprint-status.yaml to 'failed' so reconciliation picks it up
|
|
444
|
+
# and sprint-state.json stays consistent (prevents flagged stories stuck at 'review')
|
|
445
|
+
local sprint_yaml="${SPRINT_STATUS_FILE:-}"
|
|
446
|
+
if [[ -n "$sprint_yaml" && -f "$sprint_yaml" ]]; then
|
|
447
|
+
sed -i.bak "s/^ ${story_key}: .*/ ${story_key}: failed/" "$sprint_yaml" 2>/dev/null
|
|
448
|
+
rm -f "${sprint_yaml}.bak" 2>/dev/null
|
|
449
|
+
fi
|
|
445
450
|
}
|
|
446
451
|
|
|
447
452
|
# Get list of flagged stories (newline-separated).
|