codeharness 0.23.0 → 0.24.1
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/index.js +1389 -544
- package/package.json +3 -1
- package/patches/dev/enforcement.md +10 -0
- package/patches/infra/dockerfile-rules.md +62 -0
- package/patches/observability/AGENTS.md +20 -0
- package/patches/observability/rust-catch-without-tracing.rs +65 -0
- package/patches/observability/rust-catch-without-tracing.yaml +19 -0
- package/patches/observability/rust-error-path-no-tracing.rs +79 -0
- package/patches/observability/rust-error-path-no-tracing.yaml +62 -0
- package/patches/observability/rust-function-no-tracing.rs +119 -0
- package/patches/observability/rust-function-no-tracing.yaml +282 -0
- package/patches/review/enforcement.md +9 -0
- package/patches/verify/story-verification.md +23 -0
- package/templates/Dockerfile.verify.generic +27 -0
- package/templates/Dockerfile.verify.rust +39 -0
package/dist/index.js
CHANGED
|
@@ -51,17 +51,65 @@ function jsonOutput(data) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// src/modules/infra/init-project.ts
|
|
54
|
-
import { existsSync as
|
|
55
|
-
import { basename as basename3 } from "path";
|
|
54
|
+
import { existsSync as existsSync9 } from "fs";
|
|
55
|
+
import { basename as basename3, join as join9 } from "path";
|
|
56
56
|
|
|
57
57
|
// src/lib/stack-detect.ts
|
|
58
|
-
import { existsSync, readFileSync } from "fs";
|
|
58
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
59
59
|
import { join } from "path";
|
|
60
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
61
|
+
"node_modules",
|
|
62
|
+
".git",
|
|
63
|
+
"target",
|
|
64
|
+
"__pycache__",
|
|
65
|
+
"dist",
|
|
66
|
+
"build",
|
|
67
|
+
"coverage",
|
|
68
|
+
".venv",
|
|
69
|
+
"venv",
|
|
70
|
+
".tox",
|
|
71
|
+
".mypy_cache",
|
|
72
|
+
".cache"
|
|
73
|
+
]);
|
|
74
|
+
var STACK_MARKERS = [
|
|
75
|
+
{ stack: "nodejs", files: ["package.json"] },
|
|
76
|
+
{ stack: "python", files: ["requirements.txt", "pyproject.toml", "setup.py"] },
|
|
77
|
+
{ stack: "rust", files: ["Cargo.toml"] }
|
|
78
|
+
];
|
|
79
|
+
function detectStacks(dir = process.cwd()) {
|
|
80
|
+
const results = [];
|
|
81
|
+
for (const marker of STACK_MARKERS) {
|
|
82
|
+
for (const file of marker.files) {
|
|
83
|
+
if (existsSync(join(dir, file))) {
|
|
84
|
+
results.push({ stack: marker.stack, dir: "." });
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
} catch {
|
|
93
|
+
entries = [];
|
|
94
|
+
}
|
|
95
|
+
const subdirs = entries.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name)).map((e) => e.name).sort();
|
|
96
|
+
for (const subdir of subdirs) {
|
|
97
|
+
const subdirPath = join(dir, subdir);
|
|
98
|
+
for (const marker of STACK_MARKERS) {
|
|
99
|
+
for (const file of marker.files) {
|
|
100
|
+
if (existsSync(join(subdirPath, file))) {
|
|
101
|
+
results.push({ stack: marker.stack, dir: subdir });
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
60
109
|
function detectStack(dir = process.cwd()) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
if (existsSync(join(dir, "setup.py"))) return "python";
|
|
110
|
+
const stacks = detectStacks(dir);
|
|
111
|
+
const rootStack = stacks.find((s) => s.dir === ".");
|
|
112
|
+
if (rootStack) return rootStack.stack;
|
|
65
113
|
warn("No recognized stack detected");
|
|
66
114
|
return null;
|
|
67
115
|
}
|
|
@@ -69,6 +117,8 @@ var AGENT_DEPS_NODE = ["anthropic", "@anthropic-ai/sdk", "openai", "langchain",
|
|
|
69
117
|
var AGENT_DEPS_PYTHON = ["anthropic", "openai", "langchain", "llama-index", "traceloop-sdk"];
|
|
70
118
|
var WEB_FRAMEWORK_DEPS = ["react", "vue", "svelte", "angular", "@angular/core", "next", "nuxt", "vite", "webpack"];
|
|
71
119
|
var PYTHON_WEB_FRAMEWORKS = ["flask", "django", "fastapi", "streamlit"];
|
|
120
|
+
var RUST_WEB_FRAMEWORKS = ["actix-web", "axum", "rocket", "tide", "warp"];
|
|
121
|
+
var RUST_AGENT_DEPS = ["async-openai", "anthropic", "llm-chain"];
|
|
72
122
|
function readJsonSafe(path) {
|
|
73
123
|
try {
|
|
74
124
|
if (!existsSync(path)) return null;
|
|
@@ -102,6 +152,18 @@ function hasPythonDep(content, dep) {
|
|
|
102
152
|
const pattern = new RegExp(`(?:^|[\\s"',])${escaped}(?:[\\[>=<~!;\\s"',]|$)`, "m");
|
|
103
153
|
return pattern.test(content);
|
|
104
154
|
}
|
|
155
|
+
function getCargoDepsSection(content) {
|
|
156
|
+
const match = content.match(/^\[dependencies\]\s*$/m);
|
|
157
|
+
if (!match || match.index === void 0) return "";
|
|
158
|
+
const start = match.index + match[0].length;
|
|
159
|
+
const nextSection = content.slice(start).search(/^\[/m);
|
|
160
|
+
return nextSection === -1 ? content.slice(start) : content.slice(start, start + nextSection);
|
|
161
|
+
}
|
|
162
|
+
function hasCargoDep(depsSection, dep) {
|
|
163
|
+
const escaped = dep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
164
|
+
const pattern = new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s*\\{)`, "m");
|
|
165
|
+
return pattern.test(depsSection);
|
|
166
|
+
}
|
|
105
167
|
function getPythonDepsContent(dir) {
|
|
106
168
|
const files = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
107
169
|
const parts = [];
|
|
@@ -155,6 +217,24 @@ function detectAppType(dir, stack) {
|
|
|
155
217
|
}
|
|
156
218
|
return "generic";
|
|
157
219
|
}
|
|
220
|
+
if (stack === "rust") {
|
|
221
|
+
const cargoContent = readTextSafe(join(dir, "Cargo.toml"));
|
|
222
|
+
if (!cargoContent) return "generic";
|
|
223
|
+
const depsSection = getCargoDepsSection(cargoContent);
|
|
224
|
+
if (RUST_AGENT_DEPS.some((d) => hasCargoDep(depsSection, d))) {
|
|
225
|
+
return "agent";
|
|
226
|
+
}
|
|
227
|
+
if (RUST_WEB_FRAMEWORKS.some((d) => hasCargoDep(depsSection, d))) {
|
|
228
|
+
return "server";
|
|
229
|
+
}
|
|
230
|
+
if (/^\[\[bin\]\]\s*$/m.test(cargoContent)) {
|
|
231
|
+
return "cli";
|
|
232
|
+
}
|
|
233
|
+
if (/^\[lib\]\s*$/m.test(cargoContent)) {
|
|
234
|
+
return "generic";
|
|
235
|
+
}
|
|
236
|
+
return "generic";
|
|
237
|
+
}
|
|
158
238
|
return "generic";
|
|
159
239
|
}
|
|
160
240
|
|
|
@@ -162,14 +242,35 @@ function detectAppType(dir, stack) {
|
|
|
162
242
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
163
243
|
import { join as join2 } from "path";
|
|
164
244
|
import { parse, stringify } from "yaml";
|
|
245
|
+
function migrateState(state) {
|
|
246
|
+
const raw = state;
|
|
247
|
+
if (Array.isArray(raw.stacks) && raw.stacks.length > 0) {
|
|
248
|
+
state.stacks = raw.stacks;
|
|
249
|
+
state.stack = state.stacks[0] ?? null;
|
|
250
|
+
return state;
|
|
251
|
+
}
|
|
252
|
+
if (typeof raw.stack === "string" && raw.stack) {
|
|
253
|
+
state.stacks = [raw.stack];
|
|
254
|
+
return state;
|
|
255
|
+
}
|
|
256
|
+
state.stacks = [];
|
|
257
|
+
state.stack = null;
|
|
258
|
+
return state;
|
|
259
|
+
}
|
|
165
260
|
var STATE_DIR = ".claude";
|
|
166
261
|
var STATE_FILE = "codeharness.local.md";
|
|
167
262
|
var DEFAULT_BODY = "\n# Codeharness State\n\nThis file is managed by the codeharness CLI. Do not edit manually.\n";
|
|
263
|
+
function getDefaultCoverageTool(stack) {
|
|
264
|
+
if (stack === "python") return "coverage.py";
|
|
265
|
+
if (stack === "rust") return "cargo-tarpaulin";
|
|
266
|
+
return "c8";
|
|
267
|
+
}
|
|
168
268
|
function getDefaultState(stack) {
|
|
169
269
|
return {
|
|
170
270
|
harness_version: "0.1.0",
|
|
171
271
|
initialized: false,
|
|
172
272
|
stack: stack ?? null,
|
|
273
|
+
stacks: stack ? [stack] : [],
|
|
173
274
|
enforcement: {
|
|
174
275
|
frontend: true,
|
|
175
276
|
database: true,
|
|
@@ -179,7 +280,7 @@ function getDefaultState(stack) {
|
|
|
179
280
|
target: 90,
|
|
180
281
|
baseline: null,
|
|
181
282
|
current: null,
|
|
182
|
-
tool:
|
|
283
|
+
tool: getDefaultCoverageTool(stack)
|
|
183
284
|
},
|
|
184
285
|
session_flags: {
|
|
185
286
|
logs_queried: false,
|
|
@@ -197,7 +298,11 @@ function writeState(state, dir, body) {
|
|
|
197
298
|
const baseDir = dir ?? process.cwd();
|
|
198
299
|
const claudeDir = join2(baseDir, STATE_DIR);
|
|
199
300
|
mkdirSync(claudeDir, { recursive: true });
|
|
200
|
-
const
|
|
301
|
+
const toWrite = { ...state };
|
|
302
|
+
if (toWrite.stacks && toWrite.stacks.length > 0) {
|
|
303
|
+
toWrite.stack = toWrite.stacks[0];
|
|
304
|
+
}
|
|
305
|
+
const yamlContent = stringify(toWrite, { nullStr: "null" });
|
|
201
306
|
const markdownBody = body ?? DEFAULT_BODY;
|
|
202
307
|
const fileContent = `---
|
|
203
308
|
${yamlContent}---
|
|
@@ -220,7 +325,7 @@ function readState(dir) {
|
|
|
220
325
|
if (!isValidState(state)) {
|
|
221
326
|
return recoverCorruptedState(baseDir);
|
|
222
327
|
}
|
|
223
|
-
return state;
|
|
328
|
+
return migrateState(state);
|
|
224
329
|
} catch {
|
|
225
330
|
return recoverCorruptedState(baseDir);
|
|
226
331
|
}
|
|
@@ -244,7 +349,7 @@ function readStateWithBody(dir) {
|
|
|
244
349
|
return { state: recovered, body: DEFAULT_BODY };
|
|
245
350
|
}
|
|
246
351
|
const body = parts.slice(2).join("---");
|
|
247
|
-
return { state, body: body || DEFAULT_BODY };
|
|
352
|
+
return { state: migrateState(state), body: body || DEFAULT_BODY };
|
|
248
353
|
} catch {
|
|
249
354
|
const state = recoverCorruptedState(baseDir);
|
|
250
355
|
return { state, body: DEFAULT_BODY };
|
|
@@ -260,12 +365,20 @@ function isValidState(state) {
|
|
|
260
365
|
if (!s.coverage || typeof s.coverage !== "object") return false;
|
|
261
366
|
if (!s.session_flags || typeof s.session_flags !== "object") return false;
|
|
262
367
|
if (!Array.isArray(s.verification_log)) return false;
|
|
368
|
+
if (s.stacks !== void 0 && !Array.isArray(s.stacks)) return false;
|
|
369
|
+
if (Array.isArray(s.stacks) && s.stacks.some((v) => typeof v !== "string")) return false;
|
|
263
370
|
return true;
|
|
264
371
|
}
|
|
265
372
|
function recoverCorruptedState(dir) {
|
|
266
373
|
warn("State file corrupted \u2014 recreating from detected config");
|
|
267
374
|
const stack = detectStack(dir);
|
|
375
|
+
const allStacks = detectStacks(dir);
|
|
268
376
|
const state = getDefaultState(stack);
|
|
377
|
+
const uniqueStackNames = [...new Set(allStacks.map((s) => s.stack))];
|
|
378
|
+
state.stacks = uniqueStackNames;
|
|
379
|
+
if (state.stack === null && uniqueStackNames.length > 0) {
|
|
380
|
+
state.stack = uniqueStackNames[0];
|
|
381
|
+
}
|
|
269
382
|
writeState(state, dir);
|
|
270
383
|
return state;
|
|
271
384
|
}
|
|
@@ -332,6 +445,12 @@ var PYTHON_OTLP_PACKAGES = [
|
|
|
332
445
|
"opentelemetry-distro",
|
|
333
446
|
"opentelemetry-exporter-otlp"
|
|
334
447
|
];
|
|
448
|
+
var RUST_OTLP_PACKAGES = [
|
|
449
|
+
"opentelemetry",
|
|
450
|
+
"opentelemetry-otlp",
|
|
451
|
+
"tracing-opentelemetry",
|
|
452
|
+
"tracing-subscriber"
|
|
453
|
+
];
|
|
335
454
|
var WEB_OTLP_PACKAGES = [
|
|
336
455
|
"@opentelemetry/sdk-trace-web",
|
|
337
456
|
"@opentelemetry/instrumentation-fetch",
|
|
@@ -423,6 +542,26 @@ function installPythonOtlp(projectDir) {
|
|
|
423
542
|
error: "Failed to install Python OTLP packages"
|
|
424
543
|
};
|
|
425
544
|
}
|
|
545
|
+
function installRustOtlp(projectDir) {
|
|
546
|
+
try {
|
|
547
|
+
execFileSync("cargo", ["add", ...RUST_OTLP_PACKAGES], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
|
|
548
|
+
return {
|
|
549
|
+
status: "configured",
|
|
550
|
+
packages_installed: true,
|
|
551
|
+
start_script_patched: false,
|
|
552
|
+
env_vars_configured: false
|
|
553
|
+
};
|
|
554
|
+
} catch (err) {
|
|
555
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
556
|
+
return {
|
|
557
|
+
status: "failed",
|
|
558
|
+
packages_installed: false,
|
|
559
|
+
start_script_patched: false,
|
|
560
|
+
env_vars_configured: false,
|
|
561
|
+
error: `Failed to install Rust OTLP packages: ${truncateError(message)}`
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
426
565
|
function configureCli(projectDir) {
|
|
427
566
|
const { state, body } = readStateWithBody(projectDir);
|
|
428
567
|
if (!state.otlp) return;
|
|
@@ -496,6 +635,9 @@ function configureAgent(projectDir, stack) {
|
|
|
496
635
|
} catch {
|
|
497
636
|
}
|
|
498
637
|
}
|
|
638
|
+
} else if (stack === "rust") {
|
|
639
|
+
info("Rust agent SDK not yet supported \u2014 skipping agent configuration");
|
|
640
|
+
return;
|
|
499
641
|
}
|
|
500
642
|
const { state, body } = readStateWithBody(projectDir);
|
|
501
643
|
if (state.otlp) {
|
|
@@ -521,6 +663,23 @@ function ensureServiceNameEnvVar(projectDir, serviceName) {
|
|
|
521
663
|
writeFileSync2(envFilePath, envLine + "\n", "utf-8");
|
|
522
664
|
}
|
|
523
665
|
}
|
|
666
|
+
function ensureEndpointEnvVar(projectDir, endpoint) {
|
|
667
|
+
const envFilePath = join3(projectDir, ".env.codeharness");
|
|
668
|
+
const envLine = `OTEL_EXPORTER_OTLP_ENDPOINT=${endpoint}`;
|
|
669
|
+
if (existsSync3(envFilePath)) {
|
|
670
|
+
const content = readFileSync3(envFilePath, "utf-8");
|
|
671
|
+
const lines = content.split("\n").filter((l, i, arr) => i < arr.length - 1 || l.trim() !== "");
|
|
672
|
+
const idx = lines.findIndex((l) => l.startsWith("OTEL_EXPORTER_OTLP_ENDPOINT="));
|
|
673
|
+
if (idx !== -1) {
|
|
674
|
+
lines[idx] = envLine;
|
|
675
|
+
} else {
|
|
676
|
+
lines.push(envLine);
|
|
677
|
+
}
|
|
678
|
+
writeFileSync2(envFilePath, lines.join("\n") + "\n", "utf-8");
|
|
679
|
+
} else {
|
|
680
|
+
writeFileSync2(envFilePath, envLine + "\n", "utf-8");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
524
683
|
function configureOtlpEnvVars(projectDir, stack, opts) {
|
|
525
684
|
const projectName = basename(projectDir);
|
|
526
685
|
const { state, body } = readStateWithBody(projectDir);
|
|
@@ -530,11 +689,15 @@ function configureOtlpEnvVars(projectDir, stack, opts) {
|
|
|
530
689
|
service_name: projectName,
|
|
531
690
|
mode: state.otlp?.mode ?? "local-shared",
|
|
532
691
|
...stack === "nodejs" ? { node_require: NODE_REQUIRE_FLAG } : {},
|
|
533
|
-
...stack === "python" ? { python_wrapper: "opentelemetry-instrument" } : {}
|
|
692
|
+
...stack === "python" ? { python_wrapper: "opentelemetry-instrument" } : {},
|
|
693
|
+
...stack === "rust" ? { rust_env_hint: "OTEL_EXPORTER_OTLP_ENDPOINT" } : {}
|
|
534
694
|
};
|
|
535
695
|
state.otlp.resource_attributes = "service.instance.id=$(hostname)-$$";
|
|
536
696
|
writeState(state, projectDir, body);
|
|
537
697
|
ensureServiceNameEnvVar(projectDir, projectName);
|
|
698
|
+
if (stack === "rust") {
|
|
699
|
+
ensureEndpointEnvVar(projectDir, state.otlp.endpoint);
|
|
700
|
+
}
|
|
538
701
|
}
|
|
539
702
|
function instrumentProject(projectDir, stack, opts) {
|
|
540
703
|
const isJson = opts?.json === true;
|
|
@@ -560,6 +723,11 @@ function instrumentProject(projectDir, stack, opts) {
|
|
|
560
723
|
ok("OTLP: Python packages installed");
|
|
561
724
|
info("OTLP: wrap your command with: opentelemetry-instrument <command>");
|
|
562
725
|
}
|
|
726
|
+
} else if (stack === "rust") {
|
|
727
|
+
result = installRustOtlp(projectDir);
|
|
728
|
+
if (result.status === "configured" && !isJson) {
|
|
729
|
+
ok("OTLP: Rust packages installed");
|
|
730
|
+
}
|
|
563
731
|
} else {
|
|
564
732
|
return {
|
|
565
733
|
status: "skipped",
|
|
@@ -861,6 +1029,33 @@ var DEPENDENCY_REGISTRY = [
|
|
|
861
1029
|
],
|
|
862
1030
|
checkCommand: { cmd: "bd", args: ["--version"] },
|
|
863
1031
|
critical: false
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "semgrep",
|
|
1035
|
+
displayName: "Semgrep",
|
|
1036
|
+
installCommands: [
|
|
1037
|
+
{ cmd: "pipx", args: ["install", "semgrep"] },
|
|
1038
|
+
{ cmd: "pip", args: ["install", "semgrep"] }
|
|
1039
|
+
],
|
|
1040
|
+
checkCommand: { cmd: "semgrep", args: ["--version"] },
|
|
1041
|
+
critical: false
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
name: "bats",
|
|
1045
|
+
displayName: "BATS",
|
|
1046
|
+
installCommands: [
|
|
1047
|
+
{ cmd: "brew", args: ["install", "bats-core"] },
|
|
1048
|
+
{ cmd: "npm", args: ["install", "-g", "bats"] }
|
|
1049
|
+
],
|
|
1050
|
+
checkCommand: { cmd: "bats", args: ["--version"] },
|
|
1051
|
+
critical: false
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: "cargo-tarpaulin",
|
|
1055
|
+
displayName: "cargo-tarpaulin",
|
|
1056
|
+
installCommands: [{ cmd: "cargo", args: ["install", "cargo-tarpaulin"] }],
|
|
1057
|
+
checkCommand: { cmd: "cargo", args: ["tarpaulin", "--version"] },
|
|
1058
|
+
critical: false
|
|
864
1059
|
}
|
|
865
1060
|
];
|
|
866
1061
|
function checkInstalled(spec) {
|
|
@@ -1138,7 +1333,7 @@ var PATCH_TEMPLATES = {
|
|
|
1138
1333
|
|
|
1139
1334
|
// src/lib/beads.ts
|
|
1140
1335
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1141
|
-
import { existsSync as existsSync5, readdirSync } from "fs";
|
|
1336
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
|
|
1142
1337
|
import { join as join5 } from "path";
|
|
1143
1338
|
var BeadsError = class extends Error {
|
|
1144
1339
|
constructor(command, originalMessage) {
|
|
@@ -1228,7 +1423,7 @@ function detectBeadsHooks(dir) {
|
|
|
1228
1423
|
return { hasHooks: false, hookTypes: [] };
|
|
1229
1424
|
}
|
|
1230
1425
|
try {
|
|
1231
|
-
const entries =
|
|
1426
|
+
const entries = readdirSync2(hooksDir);
|
|
1232
1427
|
const hookTypes = entries.filter((e) => !e.startsWith("."));
|
|
1233
1428
|
return {
|
|
1234
1429
|
hasHooks: hookTypes.length > 0,
|
|
@@ -1776,9 +1971,15 @@ function readmeTemplate(config) {
|
|
|
1776
1971
|
return lines.join("\n");
|
|
1777
1972
|
}
|
|
1778
1973
|
function getInstallCommand(stack) {
|
|
1779
|
-
if (stack
|
|
1780
|
-
|
|
1974
|
+
if (Array.isArray(stack)) {
|
|
1975
|
+
const commands = stack.map((s) => getSingleInstallCommand(s));
|
|
1976
|
+
return [...new Set(commands)].join("\n");
|
|
1781
1977
|
}
|
|
1978
|
+
return getSingleInstallCommand(stack);
|
|
1979
|
+
}
|
|
1980
|
+
function getSingleInstallCommand(stack) {
|
|
1981
|
+
if (stack === "python") return "pip install codeharness";
|
|
1982
|
+
if (stack === "rust") return "cargo install codeharness";
|
|
1782
1983
|
return "npm install -g codeharness";
|
|
1783
1984
|
}
|
|
1784
1985
|
|
|
@@ -1795,20 +1996,46 @@ function getProjectName(projectDir) {
|
|
|
1795
1996
|
}
|
|
1796
1997
|
} catch {
|
|
1797
1998
|
}
|
|
1999
|
+
try {
|
|
2000
|
+
const cargoPath = join7(projectDir, "Cargo.toml");
|
|
2001
|
+
if (existsSync7(cargoPath)) {
|
|
2002
|
+
const content = readFileSync7(cargoPath, "utf-8");
|
|
2003
|
+
const packageMatch = content.match(/\[package\]([\s\S]*?)(?=\n\[|$)/s);
|
|
2004
|
+
if (packageMatch) {
|
|
2005
|
+
const nameMatch = packageMatch[1].match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
2006
|
+
if (nameMatch) {
|
|
2007
|
+
return nameMatch[1];
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
1798
2013
|
return basename2(projectDir);
|
|
1799
2014
|
}
|
|
1800
2015
|
function getStackLabel(stack) {
|
|
2016
|
+
if (Array.isArray(stack)) {
|
|
2017
|
+
if (stack.length === 0) return "Unknown";
|
|
2018
|
+
return stack.map((s) => getStackLabel(s)).join(" + ");
|
|
2019
|
+
}
|
|
1801
2020
|
if (stack === "nodejs") return "Node.js (package.json)";
|
|
1802
2021
|
if (stack === "python") return "Python";
|
|
2022
|
+
if (stack === "rust") return "Rust (Cargo.toml)";
|
|
1803
2023
|
return "Unknown";
|
|
1804
2024
|
}
|
|
1805
2025
|
function getCoverageTool(stack) {
|
|
1806
2026
|
if (stack === "python") return "coverage.py";
|
|
2027
|
+
if (stack === "rust") return "cargo-tarpaulin";
|
|
1807
2028
|
return "c8";
|
|
1808
2029
|
}
|
|
1809
2030
|
function generateAgentsMdContent(projectDir, stack) {
|
|
2031
|
+
if (Array.isArray(stack) && stack.length > 1) {
|
|
2032
|
+
return generateMultiStackAgentsMd(projectDir, stack);
|
|
2033
|
+
}
|
|
2034
|
+
if (Array.isArray(stack)) {
|
|
2035
|
+
stack = stack.length === 1 ? stack[0].stack : null;
|
|
2036
|
+
}
|
|
1810
2037
|
const projectName = basename2(projectDir);
|
|
1811
|
-
const stackLabel = stack === "nodejs" ? "Node.js" : stack === "python" ? "Python" : "Unknown";
|
|
2038
|
+
const stackLabel = stack === "nodejs" ? "Node.js" : stack === "python" ? "Python" : stack === "rust" ? "Rust" : "Unknown";
|
|
1812
2039
|
const lines = [
|
|
1813
2040
|
`# ${projectName}`,
|
|
1814
2041
|
"",
|
|
@@ -1834,6 +2061,14 @@ function generateAgentsMdContent(projectDir, stack) {
|
|
|
1834
2061
|
"python -m pytest # Run tests",
|
|
1835
2062
|
"```"
|
|
1836
2063
|
);
|
|
2064
|
+
} else if (stack === "rust") {
|
|
2065
|
+
lines.push(
|
|
2066
|
+
"```bash",
|
|
2067
|
+
"cargo build # Build the project",
|
|
2068
|
+
"cargo test # Run tests",
|
|
2069
|
+
"cargo tarpaulin --out json # Run coverage",
|
|
2070
|
+
"```"
|
|
2071
|
+
);
|
|
1837
2072
|
} else {
|
|
1838
2073
|
lines.push("```bash", "# No recognized stack \u2014 add build/test commands here", "```");
|
|
1839
2074
|
}
|
|
@@ -1858,6 +2093,70 @@ function generateAgentsMdContent(projectDir, stack) {
|
|
|
1858
2093
|
);
|
|
1859
2094
|
return lines.join("\n");
|
|
1860
2095
|
}
|
|
2096
|
+
function stackDisplayName(stack) {
|
|
2097
|
+
if (stack === "nodejs") return "Node.js";
|
|
2098
|
+
if (stack === "python") return "Python";
|
|
2099
|
+
if (stack === "rust") return "Rust";
|
|
2100
|
+
return "Unknown";
|
|
2101
|
+
}
|
|
2102
|
+
function generateMultiStackAgentsMd(projectDir, stacks) {
|
|
2103
|
+
const projectName = basename2(projectDir);
|
|
2104
|
+
const stackNames = stacks.map((s) => stackDisplayName(s.stack));
|
|
2105
|
+
const lines = [
|
|
2106
|
+
`# ${projectName}`,
|
|
2107
|
+
"",
|
|
2108
|
+
"## Stack",
|
|
2109
|
+
"",
|
|
2110
|
+
`- **Language/Runtime:** ${stackNames.join(" + ")}`,
|
|
2111
|
+
"",
|
|
2112
|
+
"## Build & Test Commands",
|
|
2113
|
+
""
|
|
2114
|
+
];
|
|
2115
|
+
for (const detection of stacks) {
|
|
2116
|
+
const label = stackDisplayName(detection.stack);
|
|
2117
|
+
const heading = detection.dir === "." ? `### ${label}` : `### ${label} (${detection.dir}/)`;
|
|
2118
|
+
const prefix = detection.dir === "." ? "" : `cd ${detection.dir} && `;
|
|
2119
|
+
lines.push(heading, "", "```bash");
|
|
2120
|
+
if (detection.stack === "nodejs") {
|
|
2121
|
+
lines.push(
|
|
2122
|
+
`${prefix}npm install # Install dependencies`,
|
|
2123
|
+
`${prefix}npm run build # Build the project`,
|
|
2124
|
+
`${prefix}npm test # Run tests`
|
|
2125
|
+
);
|
|
2126
|
+
} else if (detection.stack === "python") {
|
|
2127
|
+
lines.push(
|
|
2128
|
+
`${prefix}pip install -r requirements.txt # Install dependencies`,
|
|
2129
|
+
`${prefix}python -m pytest # Run tests`
|
|
2130
|
+
);
|
|
2131
|
+
} else if (detection.stack === "rust") {
|
|
2132
|
+
lines.push(
|
|
2133
|
+
`${prefix}cargo build # Build the project`,
|
|
2134
|
+
`${prefix}cargo test # Run tests`,
|
|
2135
|
+
`${prefix}cargo tarpaulin --out json # Run coverage`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
lines.push("```", "");
|
|
2139
|
+
}
|
|
2140
|
+
lines.push(
|
|
2141
|
+
"## Project Structure",
|
|
2142
|
+
"",
|
|
2143
|
+
"```",
|
|
2144
|
+
`${projectName}/`,
|
|
2145
|
+
"\u251C\u2500\u2500 src/ # Source code",
|
|
2146
|
+
"\u251C\u2500\u2500 tests/ # Test files",
|
|
2147
|
+
"\u251C\u2500\u2500 docs/ # Documentation",
|
|
2148
|
+
"\u2514\u2500\u2500 .claude/ # Codeharness state",
|
|
2149
|
+
"```",
|
|
2150
|
+
"",
|
|
2151
|
+
"## Conventions",
|
|
2152
|
+
"",
|
|
2153
|
+
"- All changes must pass tests before commit",
|
|
2154
|
+
"- Maintain test coverage targets",
|
|
2155
|
+
"- Follow existing code style and patterns",
|
|
2156
|
+
""
|
|
2157
|
+
);
|
|
2158
|
+
return lines.join("\n");
|
|
2159
|
+
}
|
|
1861
2160
|
function generateDocsIndexContent() {
|
|
1862
2161
|
return [
|
|
1863
2162
|
"# Project Documentation",
|
|
@@ -1884,7 +2183,8 @@ async function scaffoldDocs(opts) {
|
|
|
1884
2183
|
let readme = "skipped";
|
|
1885
2184
|
const agentsMdPath = join7(opts.projectDir, "AGENTS.md");
|
|
1886
2185
|
if (!existsSync7(agentsMdPath)) {
|
|
1887
|
-
const
|
|
2186
|
+
const stackArg = opts.stacks && opts.stacks.length > 1 ? opts.stacks : opts.stack;
|
|
2187
|
+
const content = generateAgentsMdContent(opts.projectDir, stackArg);
|
|
1888
2188
|
generateFile(agentsMdPath, content);
|
|
1889
2189
|
agentsMd = "created";
|
|
1890
2190
|
} else {
|
|
@@ -1913,9 +2213,10 @@ async function scaffoldDocs(opts) {
|
|
|
1913
2213
|
} catch {
|
|
1914
2214
|
cliHelpOutput = "Run: codeharness --help";
|
|
1915
2215
|
}
|
|
2216
|
+
const readmeStack = opts.stacks && opts.stacks.length > 1 ? opts.stacks.map((s) => s.stack) : opts.stack;
|
|
1916
2217
|
const readmeContent = readmeTemplate({
|
|
1917
2218
|
projectName: getProjectName(opts.projectDir),
|
|
1918
|
-
stack:
|
|
2219
|
+
stack: readmeStack,
|
|
1919
2220
|
cliHelpOutput
|
|
1920
2221
|
});
|
|
1921
2222
|
generateFile(readmePath, readmeContent);
|
|
@@ -1939,12 +2240,209 @@ async function scaffoldDocs(opts) {
|
|
|
1939
2240
|
}
|
|
1940
2241
|
}
|
|
1941
2242
|
|
|
2243
|
+
// src/modules/infra/dockerfile-template.ts
|
|
2244
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
2245
|
+
import { join as join8 } from "path";
|
|
2246
|
+
function nodejsTemplate() {
|
|
2247
|
+
return `# Base image \u2014 pinned version for reproducibility
|
|
2248
|
+
FROM node:22-slim
|
|
2249
|
+
|
|
2250
|
+
ARG TARBALL=package.tgz
|
|
2251
|
+
|
|
2252
|
+
# System utilities for verification
|
|
2253
|
+
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
|
|
2254
|
+
|
|
2255
|
+
# Install project from tarball (black-box: no source code)
|
|
2256
|
+
COPY \${TARBALL} /tmp/\${TARBALL}
|
|
2257
|
+
RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
|
|
2258
|
+
|
|
2259
|
+
# Run as non-root user
|
|
2260
|
+
USER node
|
|
2261
|
+
|
|
2262
|
+
WORKDIR /workspace
|
|
2263
|
+
`;
|
|
2264
|
+
}
|
|
2265
|
+
function pythonTemplate() {
|
|
2266
|
+
return `# Base image \u2014 pinned version for reproducibility
|
|
2267
|
+
FROM python:3.12-slim
|
|
2268
|
+
|
|
2269
|
+
# System utilities for verification
|
|
2270
|
+
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
|
|
2271
|
+
|
|
2272
|
+
# Install project from wheel or sdist
|
|
2273
|
+
COPY dist/ /tmp/dist/
|
|
2274
|
+
RUN pip install /tmp/dist/*.whl && rm -rf /tmp/dist/ && pip cache purge
|
|
2275
|
+
|
|
2276
|
+
# Run as non-root user
|
|
2277
|
+
USER nobody
|
|
2278
|
+
|
|
2279
|
+
WORKDIR /workspace
|
|
2280
|
+
`;
|
|
2281
|
+
}
|
|
2282
|
+
function rustTemplate() {
|
|
2283
|
+
return `# === Builder stage ===
|
|
2284
|
+
FROM rust:1.82-slim AS builder
|
|
2285
|
+
|
|
2286
|
+
WORKDIR /build
|
|
2287
|
+
|
|
2288
|
+
# Copy project files
|
|
2289
|
+
COPY . .
|
|
2290
|
+
|
|
2291
|
+
# Build release binary
|
|
2292
|
+
RUN cargo build --release
|
|
2293
|
+
|
|
2294
|
+
# === Runtime stage ===
|
|
2295
|
+
FROM debian:bookworm-slim
|
|
2296
|
+
|
|
2297
|
+
# System utilities for verification
|
|
2298
|
+
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
|
|
2299
|
+
|
|
2300
|
+
# Install compiled binary from builder (update 'myapp' to your binary name)
|
|
2301
|
+
COPY --from=builder /build/target/release/myapp /usr/local/bin/myapp
|
|
2302
|
+
|
|
2303
|
+
# Run as non-root user
|
|
2304
|
+
USER nobody
|
|
2305
|
+
|
|
2306
|
+
WORKDIR /workspace
|
|
2307
|
+
`;
|
|
2308
|
+
}
|
|
2309
|
+
function genericTemplate() {
|
|
2310
|
+
return `# Base image \u2014 pinned version for reproducibility
|
|
2311
|
+
FROM node:22-slim
|
|
2312
|
+
|
|
2313
|
+
# System utilities for verification
|
|
2314
|
+
RUN apt-get update && apt-get install -y --no-install-recommends bash curl jq git && rm -rf /var/lib/apt/lists/*
|
|
2315
|
+
|
|
2316
|
+
# Install project binary (update this for your project)
|
|
2317
|
+
RUN npm install -g placeholder && npm cache clean --force
|
|
2318
|
+
|
|
2319
|
+
# Run as non-root user
|
|
2320
|
+
USER node
|
|
2321
|
+
|
|
2322
|
+
WORKDIR /workspace
|
|
2323
|
+
`;
|
|
2324
|
+
}
|
|
2325
|
+
function nodejsBuildStage() {
|
|
2326
|
+
return `# === Build stage: nodejs ===
|
|
2327
|
+
FROM node:22-slim AS build-nodejs
|
|
2328
|
+
WORKDIR /build
|
|
2329
|
+
COPY package*.json ./
|
|
2330
|
+
RUN npm ci --production
|
|
2331
|
+
COPY . .
|
|
2332
|
+
`;
|
|
2333
|
+
}
|
|
2334
|
+
function pythonBuildStage() {
|
|
2335
|
+
return `# === Build stage: python ===
|
|
2336
|
+
FROM python:3.12-slim AS build-python
|
|
2337
|
+
WORKDIR /build
|
|
2338
|
+
COPY . .
|
|
2339
|
+
RUN pip install --target=/build/dist .
|
|
2340
|
+
`;
|
|
2341
|
+
}
|
|
2342
|
+
function rustBuildStage() {
|
|
2343
|
+
return `# === Build stage: rust ===
|
|
2344
|
+
FROM rust:1.82-slim AS build-rust
|
|
2345
|
+
WORKDIR /build
|
|
2346
|
+
COPY . .
|
|
2347
|
+
RUN cargo build --release
|
|
2348
|
+
`;
|
|
2349
|
+
}
|
|
2350
|
+
function runtimeCopyDirectives(stacks) {
|
|
2351
|
+
const lines = [];
|
|
2352
|
+
for (const stack of stacks) {
|
|
2353
|
+
if (stack === "nodejs") {
|
|
2354
|
+
lines.push("COPY --from=build-nodejs /build/node_modules ./node_modules");
|
|
2355
|
+
lines.push("COPY --from=build-nodejs /build/ ./app/");
|
|
2356
|
+
} else if (stack === "python") {
|
|
2357
|
+
lines.push("COPY --from=build-python /build/dist /opt/app/python/");
|
|
2358
|
+
} else if (stack === "rust") {
|
|
2359
|
+
lines.push("COPY --from=build-rust /build/target/release/myapp /usr/local/bin/myapp");
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return lines.join("\n");
|
|
2363
|
+
}
|
|
2364
|
+
var MULTI_STAGE_STACKS = /* @__PURE__ */ new Set(["nodejs", "python", "rust"]);
|
|
2365
|
+
function multiStageTemplate(detections) {
|
|
2366
|
+
const supported = detections.filter((d) => MULTI_STAGE_STACKS.has(d.stack));
|
|
2367
|
+
if (supported.length === 0) {
|
|
2368
|
+
return genericTemplate();
|
|
2369
|
+
}
|
|
2370
|
+
const buildStages = [];
|
|
2371
|
+
const stacks = supported.map((d) => d.stack);
|
|
2372
|
+
for (const stack of stacks) {
|
|
2373
|
+
if (stack === "nodejs") buildStages.push(nodejsBuildStage());
|
|
2374
|
+
else if (stack === "python") buildStages.push(pythonBuildStage());
|
|
2375
|
+
else if (stack === "rust") buildStages.push(rustBuildStage());
|
|
2376
|
+
}
|
|
2377
|
+
const copyLines = runtimeCopyDirectives(stacks);
|
|
2378
|
+
return `# NOTE: Customize COPY paths for your monorepo layout. Each build stage should only copy its own sources.
|
|
2379
|
+
${buildStages.join("\n")}
|
|
2380
|
+
# === Runtime stage ===
|
|
2381
|
+
FROM debian:bookworm-slim
|
|
2382
|
+
|
|
2383
|
+
# System utilities for verification
|
|
2384
|
+
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
|
|
2385
|
+
|
|
2386
|
+
# Install artifacts from build stages
|
|
2387
|
+
${copyLines}
|
|
2388
|
+
|
|
2389
|
+
# Run as non-root user
|
|
2390
|
+
USER nobody
|
|
2391
|
+
|
|
2392
|
+
WORKDIR /workspace
|
|
2393
|
+
`;
|
|
2394
|
+
}
|
|
2395
|
+
function generateDockerfileTemplate(projectDir, stackOrDetections) {
|
|
2396
|
+
if (!projectDir) {
|
|
2397
|
+
return fail2("projectDir is required");
|
|
2398
|
+
}
|
|
2399
|
+
const dockerfilePath = join8(projectDir, "Dockerfile");
|
|
2400
|
+
if (existsSync8(dockerfilePath)) {
|
|
2401
|
+
return fail2("Dockerfile already exists");
|
|
2402
|
+
}
|
|
2403
|
+
if (Array.isArray(stackOrDetections) && stackOrDetections.length > 1) {
|
|
2404
|
+
const content2 = multiStageTemplate(stackOrDetections);
|
|
2405
|
+
const stacks = stackOrDetections.map((d) => d.stack);
|
|
2406
|
+
try {
|
|
2407
|
+
writeFileSync5(dockerfilePath, content2, "utf-8");
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2410
|
+
return fail2(`Failed to write Dockerfile: ${message}`);
|
|
2411
|
+
}
|
|
2412
|
+
return ok2({ path: dockerfilePath, stack: stacks[0], stacks });
|
|
2413
|
+
}
|
|
2414
|
+
const stack = Array.isArray(stackOrDetections) ? stackOrDetections.length === 1 ? stackOrDetections[0].stack : null : stackOrDetections;
|
|
2415
|
+
let content;
|
|
2416
|
+
let resolvedStack;
|
|
2417
|
+
if (stack === "nodejs") {
|
|
2418
|
+
content = nodejsTemplate();
|
|
2419
|
+
resolvedStack = "nodejs";
|
|
2420
|
+
} else if (stack === "python") {
|
|
2421
|
+
content = pythonTemplate();
|
|
2422
|
+
resolvedStack = "python";
|
|
2423
|
+
} else if (stack === "rust") {
|
|
2424
|
+
content = rustTemplate();
|
|
2425
|
+
resolvedStack = "rust";
|
|
2426
|
+
} else {
|
|
2427
|
+
content = genericTemplate();
|
|
2428
|
+
resolvedStack = "generic";
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
writeFileSync5(dockerfilePath, content, "utf-8");
|
|
2432
|
+
} catch (err) {
|
|
2433
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2434
|
+
return fail2(`Failed to write Dockerfile: ${message}`);
|
|
2435
|
+
}
|
|
2436
|
+
return ok2({ path: dockerfilePath, stack: resolvedStack, stacks: [resolvedStack] });
|
|
2437
|
+
}
|
|
2438
|
+
|
|
1942
2439
|
// src/modules/infra/init-project.ts
|
|
1943
|
-
var HARNESS_VERSION = true ? "0.
|
|
2440
|
+
var HARNESS_VERSION = true ? "0.24.1" : "0.0.0-dev";
|
|
1944
2441
|
function failResult(opts, error) {
|
|
1945
2442
|
return {
|
|
1946
2443
|
status: "fail",
|
|
1947
2444
|
stack: null,
|
|
2445
|
+
stacks: [],
|
|
1948
2446
|
error,
|
|
1949
2447
|
enforcement: { frontend: opts.frontend, database: opts.database, api: opts.api },
|
|
1950
2448
|
documentation: { agents_md: "skipped", docs_scaffold: "skipped", readme: "skipped" }
|
|
@@ -1971,6 +2469,7 @@ async function initProjectInner(opts) {
|
|
|
1971
2469
|
const result = {
|
|
1972
2470
|
status: "ok",
|
|
1973
2471
|
stack: null,
|
|
2472
|
+
stacks: [],
|
|
1974
2473
|
enforcement: { frontend: opts.frontend, database: opts.database, api: opts.api },
|
|
1975
2474
|
documentation: { agents_md: "skipped", docs_scaffold: "skipped", readme: "skipped" }
|
|
1976
2475
|
};
|
|
@@ -1981,14 +2480,29 @@ async function initProjectInner(opts) {
|
|
|
1981
2480
|
emitError(urlError, isJson);
|
|
1982
2481
|
return ok2(failResult(opts, urlError));
|
|
1983
2482
|
}
|
|
1984
|
-
const
|
|
2483
|
+
const allStacks = detectStacks(projectDir);
|
|
2484
|
+
const rootDetection = allStacks.find((s) => s.dir === ".");
|
|
2485
|
+
const stack = rootDetection ? rootDetection.stack : null;
|
|
2486
|
+
if (!stack && !isJson) warn("No recognized stack detected");
|
|
1985
2487
|
result.stack = stack;
|
|
2488
|
+
result.stacks = [...new Set(allStacks.map((s) => s.stack))];
|
|
1986
2489
|
const appType = detectAppType(projectDir, stack);
|
|
1987
2490
|
result.app_type = appType;
|
|
1988
2491
|
if (!isJson) {
|
|
1989
|
-
if (
|
|
2492
|
+
if (result.stacks.length > 0) {
|
|
2493
|
+
info(`Stack detected: ${getStackLabel(result.stacks)}`);
|
|
2494
|
+
} else if (stack) {
|
|
2495
|
+
info(`Stack detected: ${getStackLabel(stack)}`);
|
|
2496
|
+
}
|
|
1990
2497
|
info(`App type: ${appType}`);
|
|
1991
2498
|
}
|
|
2499
|
+
const dfResult = generateDockerfileTemplate(projectDir, allStacks);
|
|
2500
|
+
if (isOk(dfResult)) {
|
|
2501
|
+
result.dockerfile = { generated: true, stack: dfResult.data.stack, stacks: dfResult.data.stacks };
|
|
2502
|
+
if (!isJson) info(`Generated Dockerfile for ${dfResult.data.stacks.join("+") || dfResult.data.stack} project.`);
|
|
2503
|
+
} else {
|
|
2504
|
+
if (!isJson) info("Dockerfile already exists -- skipping template generation.");
|
|
2505
|
+
}
|
|
1992
2506
|
const dockerCheck = checkDocker({ observability: opts.observability, otelEndpoint: opts.otelEndpoint, logsUrl: opts.logsUrl, opensearchUrl: opts.opensearchUrl, isJson });
|
|
1993
2507
|
if (!isOk(dockerCheck)) return fail2(dockerCheck.error);
|
|
1994
2508
|
const { available: dockerAvailable, criticalFailure, dockerResult: criticalDockerResult } = dockerCheck.data;
|
|
@@ -2027,16 +2541,31 @@ async function initProjectInner(opts) {
|
|
|
2027
2541
|
state.initialized = true;
|
|
2028
2542
|
state.app_type = appType;
|
|
2029
2543
|
state.enforcement = { frontend: opts.frontend, database: opts.database, api: opts.api };
|
|
2544
|
+
const coverageTools = {};
|
|
2545
|
+
for (const detection of allStacks) {
|
|
2546
|
+
coverageTools[detection.stack] = getCoverageTool(detection.stack);
|
|
2547
|
+
}
|
|
2030
2548
|
state.coverage.tool = getCoverageTool(stack);
|
|
2549
|
+
state.coverage.tools = coverageTools;
|
|
2550
|
+
state.stacks = result.stacks;
|
|
2031
2551
|
writeState(state, projectDir);
|
|
2032
2552
|
if (!isJson) ok("State file: .claude/codeharness.local.md created");
|
|
2033
|
-
const docsResult = await scaffoldDocs({ projectDir, stack, isJson });
|
|
2553
|
+
const docsResult = await scaffoldDocs({ projectDir, stack, stacks: allStacks, isJson });
|
|
2034
2554
|
if (isOk(docsResult)) result.documentation = docsResult.data;
|
|
2035
2555
|
if (!opts.observability) {
|
|
2036
2556
|
result.otlp = { status: "skipped", packages_installed: false, start_script_patched: false, env_vars_configured: false };
|
|
2037
2557
|
if (!isJson) info("OTLP: skipped (--no-observability)");
|
|
2038
2558
|
} else {
|
|
2039
|
-
|
|
2559
|
+
for (const detection of allStacks) {
|
|
2560
|
+
const stackDir = detection.dir === "." ? projectDir : join9(projectDir, detection.dir);
|
|
2561
|
+
const stackOtlp = instrumentProject(stackDir, detection.stack, { json: isJson, appType });
|
|
2562
|
+
if (detection.dir === "." && detection.stack === stack) {
|
|
2563
|
+
result.otlp = stackOtlp;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (!result.otlp) {
|
|
2567
|
+
result.otlp = instrumentProject(projectDir, stack, { json: isJson, appType });
|
|
2568
|
+
}
|
|
2040
2569
|
}
|
|
2041
2570
|
try {
|
|
2042
2571
|
const u = readState(projectDir);
|
|
@@ -2071,7 +2600,7 @@ async function initProjectInner(opts) {
|
|
|
2071
2600
|
}
|
|
2072
2601
|
function handleRerun(opts, result) {
|
|
2073
2602
|
const { projectDir, json: isJson = false } = opts;
|
|
2074
|
-
if (!
|
|
2603
|
+
if (!existsSync9(getStatePath(projectDir))) return null;
|
|
2075
2604
|
try {
|
|
2076
2605
|
const existingState = readState(projectDir);
|
|
2077
2606
|
const legacyObsDisabled = existingState.enforcement.observability === false;
|
|
@@ -2080,6 +2609,7 @@ function handleRerun(opts, result) {
|
|
|
2080
2609
|
return null;
|
|
2081
2610
|
}
|
|
2082
2611
|
result.stack = existingState.stack;
|
|
2612
|
+
result.stacks = existingState.stacks ?? [];
|
|
2083
2613
|
result.enforcement = existingState.enforcement;
|
|
2084
2614
|
result.documentation = { agents_md: "exists", docs_scaffold: "exists", readme: "exists" };
|
|
2085
2615
|
result.dependencies = verifyDeps(isJson);
|
|
@@ -2123,6 +2653,136 @@ var DEFAULT_LOGS_URL = `http://localhost:${DEFAULT_PORTS.logs}`;
|
|
|
2123
2653
|
var DEFAULT_METRICS_URL = `http://localhost:${DEFAULT_PORTS.metrics}`;
|
|
2124
2654
|
var DEFAULT_TRACES_URL = `http://localhost:${DEFAULT_PORTS.traces}`;
|
|
2125
2655
|
|
|
2656
|
+
// src/modules/infra/dockerfile-validator.ts
|
|
2657
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
2658
|
+
import { join as join10 } from "path";
|
|
2659
|
+
var DEFAULT_RULES = {
|
|
2660
|
+
requirePinnedFrom: true,
|
|
2661
|
+
requireBinaryOnPath: true,
|
|
2662
|
+
verificationTools: ["curl", "jq"],
|
|
2663
|
+
forbidSourceCopy: true,
|
|
2664
|
+
requireNonRootUser: true,
|
|
2665
|
+
requireCacheCleanup: true
|
|
2666
|
+
};
|
|
2667
|
+
function dfGap(rule, description, suggestedFix, line) {
|
|
2668
|
+
const g = { rule, description, suggestedFix };
|
|
2669
|
+
if (line !== void 0) return { ...g, line };
|
|
2670
|
+
return g;
|
|
2671
|
+
}
|
|
2672
|
+
function loadRules(projectDir) {
|
|
2673
|
+
const rulesPath = join10(projectDir, "patches", "infra", "dockerfile-rules.md");
|
|
2674
|
+
if (!existsSync10(rulesPath)) {
|
|
2675
|
+
return {
|
|
2676
|
+
rules: DEFAULT_RULES,
|
|
2677
|
+
warnings: ["dockerfile-rules.md not found -- using defaults."]
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
return { rules: DEFAULT_RULES, warnings: [] };
|
|
2681
|
+
}
|
|
2682
|
+
function checkPinnedFrom(lines) {
|
|
2683
|
+
const gaps = [];
|
|
2684
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2685
|
+
if (!/^\s*FROM\s+/i.test(lines[i])) continue;
|
|
2686
|
+
const ref = lines[i].replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
|
|
2687
|
+
if (ref.endsWith(":latest")) {
|
|
2688
|
+
gaps.push(dfGap("pinned-from", `unpinned base image -- use specific version.`, `Pin ${ref} to a specific version tag`, i + 1));
|
|
2689
|
+
} else if (!ref.includes(":") && !ref.includes("@")) {
|
|
2690
|
+
gaps.push(dfGap("pinned-from", `unpinned base image -- use specific version.`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`, i + 1));
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
return gaps;
|
|
2694
|
+
}
|
|
2695
|
+
function checkBinaryOnPath(lines) {
|
|
2696
|
+
const content = lines.join("\n");
|
|
2697
|
+
const hasBinary = /npm\s+install\s+(-g|--global)\b/i.test(content) || /pip\s+install\b/i.test(content) || /COPY\s+--from=/i.test(content);
|
|
2698
|
+
if (!hasBinary) {
|
|
2699
|
+
return [dfGap("binary-on-path", "project binary not installed.", "Add npm install -g, pip install, or COPY --from to install the project binary")];
|
|
2700
|
+
}
|
|
2701
|
+
return [];
|
|
2702
|
+
}
|
|
2703
|
+
function checkVerificationTools(lines, tools) {
|
|
2704
|
+
const gaps = [];
|
|
2705
|
+
for (const tool of tools) {
|
|
2706
|
+
let found = false;
|
|
2707
|
+
for (const line of lines) {
|
|
2708
|
+
const lower = line.toLowerCase();
|
|
2709
|
+
const isInstallLine = lower.includes("apt-get install") || lower.includes("apk add");
|
|
2710
|
+
if (isInstallLine && new RegExp(`\\b${tool.toLowerCase()}\\b`).test(lower)) {
|
|
2711
|
+
found = true;
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
if (!found) {
|
|
2716
|
+
gaps.push(dfGap("verification-tools", `verification tool missing: ${tool}`, `Install ${tool} via apt-get install or apk add`));
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
return gaps;
|
|
2720
|
+
}
|
|
2721
|
+
function checkNoSourceCopy(lines) {
|
|
2722
|
+
const gaps = [];
|
|
2723
|
+
const sourcePatterns = [/COPY\s+(?:--\S+\s+)*src\//i, /COPY\s+(?:--\S+\s+)*lib\//i, /COPY\s+(?:--\S+\s+)*test\//i];
|
|
2724
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2725
|
+
for (const pattern of sourcePatterns) {
|
|
2726
|
+
if (pattern.test(lines[i])) {
|
|
2727
|
+
gaps.push(dfGap("no-source-copy", "source code copied into container -- use build artifact instead.", "Use COPY --from=builder or COPY dist/ instead of copying source", i + 1));
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
return gaps;
|
|
2732
|
+
}
|
|
2733
|
+
function checkNonRootUser(lines) {
|
|
2734
|
+
const userLines = lines.filter((l) => /^\s*USER\s+/i.test(l));
|
|
2735
|
+
if (userLines.length === 0) {
|
|
2736
|
+
return [dfGap("non-root-user", "no non-root USER instruction found.", "Add USER <non-root-user> instruction (e.g., USER node)")];
|
|
2737
|
+
}
|
|
2738
|
+
const hasNonRoot = userLines.some((l) => {
|
|
2739
|
+
const user = l.replace(/^\s*USER\s+/i, "").trim().split(/\s+/)[0];
|
|
2740
|
+
return user.toLowerCase() !== "root";
|
|
2741
|
+
});
|
|
2742
|
+
if (!hasNonRoot) {
|
|
2743
|
+
return [dfGap("non-root-user", "no non-root USER instruction found.", "Add USER <non-root-user> instruction (e.g., USER node)")];
|
|
2744
|
+
}
|
|
2745
|
+
return [];
|
|
2746
|
+
}
|
|
2747
|
+
function checkCacheCleanup(lines) {
|
|
2748
|
+
const content = lines.join("\n");
|
|
2749
|
+
const hasCleanup = /rm\s+-rf\s+\/var\/lib\/apt\/lists/i.test(content) || /rm\s+-rf\s+\/var\/cache\/apk/i.test(content) || /npm\s+cache\s+clean/i.test(content) || /pip\s+cache\s+purge/i.test(content);
|
|
2750
|
+
if (!hasCleanup) {
|
|
2751
|
+
return [dfGap("cache-cleanup", "no cache cleanup detected.", "Add cache cleanup: rm -rf /var/lib/apt/lists/*, npm cache clean --force, or pip cache purge")];
|
|
2752
|
+
}
|
|
2753
|
+
return [];
|
|
2754
|
+
}
|
|
2755
|
+
function validateDockerfile(projectDir) {
|
|
2756
|
+
const dfPath = join10(projectDir, "Dockerfile");
|
|
2757
|
+
if (!existsSync10(dfPath)) {
|
|
2758
|
+
return fail2("No Dockerfile found");
|
|
2759
|
+
}
|
|
2760
|
+
let content;
|
|
2761
|
+
try {
|
|
2762
|
+
content = readFileSync8(dfPath, "utf-8");
|
|
2763
|
+
} catch {
|
|
2764
|
+
return fail2("Dockerfile exists but could not be read");
|
|
2765
|
+
}
|
|
2766
|
+
const lines = content.split("\n");
|
|
2767
|
+
const fromLines = lines.filter((l) => /^\s*FROM\s+/i.test(l));
|
|
2768
|
+
if (fromLines.length === 0) {
|
|
2769
|
+
return fail2("Dockerfile has no FROM instruction");
|
|
2770
|
+
}
|
|
2771
|
+
const { rules, warnings } = loadRules(projectDir);
|
|
2772
|
+
const gaps = [];
|
|
2773
|
+
if (rules.requirePinnedFrom) gaps.push(...checkPinnedFrom(lines));
|
|
2774
|
+
if (rules.requireBinaryOnPath) gaps.push(...checkBinaryOnPath(lines));
|
|
2775
|
+
gaps.push(...checkVerificationTools(lines, rules.verificationTools));
|
|
2776
|
+
if (rules.forbidSourceCopy) gaps.push(...checkNoSourceCopy(lines));
|
|
2777
|
+
if (rules.requireNonRootUser) gaps.push(...checkNonRootUser(lines));
|
|
2778
|
+
if (rules.requireCacheCleanup) gaps.push(...checkCacheCleanup(lines));
|
|
2779
|
+
return ok2({
|
|
2780
|
+
passed: gaps.length === 0,
|
|
2781
|
+
gaps,
|
|
2782
|
+
warnings
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2126
2786
|
// src/modules/infra/index.ts
|
|
2127
2787
|
async function initProject2(opts) {
|
|
2128
2788
|
return initProject(opts);
|
|
@@ -2156,7 +2816,7 @@ function registerInitCommand(program) {
|
|
|
2156
2816
|
}
|
|
2157
2817
|
|
|
2158
2818
|
// src/commands/bridge.ts
|
|
2159
|
-
import { existsSync as
|
|
2819
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2160
2820
|
function registerBridgeCommand(program) {
|
|
2161
2821
|
program.command("bridge").description("Bridge BMAD epics/stories into beads task store").option("--epics <path>", "Path to BMAD epics markdown file").option("--dry-run", "Parse and display without creating beads issues").action((opts, cmd) => {
|
|
2162
2822
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -2168,7 +2828,7 @@ function registerBridgeCommand(program) {
|
|
|
2168
2828
|
process.exitCode = 2;
|
|
2169
2829
|
return;
|
|
2170
2830
|
}
|
|
2171
|
-
if (!
|
|
2831
|
+
if (!existsSync11(epicsPath)) {
|
|
2172
2832
|
fail(`Epics file not found: ${epicsPath}`, { json: isJson });
|
|
2173
2833
|
process.exitCode = 1;
|
|
2174
2834
|
return;
|
|
@@ -2241,14 +2901,13 @@ function registerBridgeCommand(program) {
|
|
|
2241
2901
|
|
|
2242
2902
|
// src/commands/run.ts
|
|
2243
2903
|
import { spawn } from "child_process";
|
|
2244
|
-
import { existsSync as
|
|
2245
|
-
import { join as
|
|
2904
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
2905
|
+
import { join as join16, dirname as dirname4 } from "path";
|
|
2246
2906
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2247
|
-
import { StringDecoder } from "string_decoder";
|
|
2248
2907
|
|
|
2249
2908
|
// src/lib/beads-sync.ts
|
|
2250
|
-
import { existsSync as
|
|
2251
|
-
import { join as
|
|
2909
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
2910
|
+
import { join as join11 } from "path";
|
|
2252
2911
|
import { parse as parse2 } from "yaml";
|
|
2253
2912
|
var BEADS_TO_STORY_STATUS = {
|
|
2254
2913
|
open: "in-progress",
|
|
@@ -2279,10 +2938,10 @@ function resolveStoryFilePath(beadsIssue) {
|
|
|
2279
2938
|
return trimmed;
|
|
2280
2939
|
}
|
|
2281
2940
|
function readStoryFileStatus(filePath) {
|
|
2282
|
-
if (!
|
|
2941
|
+
if (!existsSync12(filePath)) {
|
|
2283
2942
|
return null;
|
|
2284
2943
|
}
|
|
2285
|
-
const content =
|
|
2944
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2286
2945
|
const match = content.match(/^Status:\s*(.+)$/m);
|
|
2287
2946
|
if (!match) {
|
|
2288
2947
|
return null;
|
|
@@ -2290,11 +2949,11 @@ function readStoryFileStatus(filePath) {
|
|
|
2290
2949
|
return match[1].trim();
|
|
2291
2950
|
}
|
|
2292
2951
|
function updateStoryFileStatus(filePath, newStatus) {
|
|
2293
|
-
const content =
|
|
2952
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2294
2953
|
const statusRegex = /^Status:\s*.+$/m;
|
|
2295
2954
|
if (statusRegex.test(content)) {
|
|
2296
2955
|
const updated = content.replace(statusRegex, `Status: ${newStatus}`);
|
|
2297
|
-
|
|
2956
|
+
writeFileSync6(filePath, updated, "utf-8");
|
|
2298
2957
|
} else {
|
|
2299
2958
|
const lines = content.split("\n");
|
|
2300
2959
|
const titleIndex = lines.findIndex((l) => l.startsWith("# "));
|
|
@@ -2303,18 +2962,18 @@ function updateStoryFileStatus(filePath, newStatus) {
|
|
|
2303
2962
|
} else {
|
|
2304
2963
|
lines.unshift(`Status: ${newStatus}`, "");
|
|
2305
2964
|
}
|
|
2306
|
-
|
|
2965
|
+
writeFileSync6(filePath, lines.join("\n"), "utf-8");
|
|
2307
2966
|
}
|
|
2308
2967
|
}
|
|
2309
2968
|
var SPRINT_STATUS_PATH = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
2310
2969
|
function readSprintStatus(dir) {
|
|
2311
2970
|
const root = dir ?? process.cwd();
|
|
2312
|
-
const filePath =
|
|
2313
|
-
if (!
|
|
2971
|
+
const filePath = join11(root, SPRINT_STATUS_PATH);
|
|
2972
|
+
if (!existsSync12(filePath)) {
|
|
2314
2973
|
return {};
|
|
2315
2974
|
}
|
|
2316
2975
|
try {
|
|
2317
|
-
const content =
|
|
2976
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2318
2977
|
const parsed = parse2(content);
|
|
2319
2978
|
if (!parsed || typeof parsed !== "object") {
|
|
2320
2979
|
return {};
|
|
@@ -2330,18 +2989,18 @@ function readSprintStatus(dir) {
|
|
|
2330
2989
|
}
|
|
2331
2990
|
function updateSprintStatus(storyKey, newStatus, dir) {
|
|
2332
2991
|
const root = dir ?? process.cwd();
|
|
2333
|
-
const filePath =
|
|
2334
|
-
if (!
|
|
2992
|
+
const filePath = join11(root, SPRINT_STATUS_PATH);
|
|
2993
|
+
if (!existsSync12(filePath)) {
|
|
2335
2994
|
warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
|
|
2336
2995
|
return;
|
|
2337
2996
|
}
|
|
2338
|
-
const content =
|
|
2997
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2339
2998
|
const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
|
|
2340
2999
|
if (!keyPattern.test(content)) {
|
|
2341
3000
|
return;
|
|
2342
3001
|
}
|
|
2343
3002
|
const updated = content.replace(keyPattern, `$1${newStatus}$2`);
|
|
2344
|
-
|
|
3003
|
+
writeFileSync6(filePath, updated, "utf-8");
|
|
2345
3004
|
}
|
|
2346
3005
|
function escapeRegExp(s) {
|
|
2347
3006
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2376,7 +3035,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
|
|
|
2376
3035
|
};
|
|
2377
3036
|
}
|
|
2378
3037
|
const storyKey = storyKeyFromPath(storyFilePath);
|
|
2379
|
-
const fullPath =
|
|
3038
|
+
const fullPath = join11(root, storyFilePath);
|
|
2380
3039
|
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
2381
3040
|
if (currentStoryStatus === null) {
|
|
2382
3041
|
return {
|
|
@@ -2421,7 +3080,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
|
|
|
2421
3080
|
function syncStoryFileToBeads(storyKey, beadsFns, dir) {
|
|
2422
3081
|
const root = dir ?? process.cwd();
|
|
2423
3082
|
const storyFilePath = `_bmad-output/implementation-artifacts/${storyKey}.md`;
|
|
2424
|
-
const fullPath =
|
|
3083
|
+
const fullPath = join11(root, storyFilePath);
|
|
2425
3084
|
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
2426
3085
|
if (currentStoryStatus === null) {
|
|
2427
3086
|
return {
|
|
@@ -2510,10 +3169,10 @@ function syncClose(beadsId, beadsFns, dir) {
|
|
|
2510
3169
|
};
|
|
2511
3170
|
}
|
|
2512
3171
|
const storyKey = storyKeyFromPath(storyFilePath);
|
|
2513
|
-
const fullPath =
|
|
3172
|
+
const fullPath = join11(root, storyFilePath);
|
|
2514
3173
|
const previousStatus = readStoryFileStatus(fullPath);
|
|
2515
3174
|
if (previousStatus === null) {
|
|
2516
|
-
if (!
|
|
3175
|
+
if (!existsSync12(fullPath)) {
|
|
2517
3176
|
return {
|
|
2518
3177
|
storyKey,
|
|
2519
3178
|
beadsId,
|
|
@@ -2645,176 +3304,25 @@ function generateRalphPrompt(config) {
|
|
|
2645
3304
|
|
|
2646
3305
|
`;
|
|
2647
3306
|
prompt += `The following stories have exceeded the retry limit and should be skipped:
|
|
2648
|
-
`;
|
|
2649
|
-
for (const story of config.flaggedStories) {
|
|
2650
|
-
prompt += `- \`${story}\`
|
|
2651
|
-
`;
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
return prompt;
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
// src/lib/stream-parser.ts
|
|
2658
|
-
function parseStreamLine(line) {
|
|
2659
|
-
const trimmed = line.trim();
|
|
2660
|
-
if (trimmed.length === 0) {
|
|
2661
|
-
return null;
|
|
2662
|
-
}
|
|
2663
|
-
let parsed;
|
|
2664
|
-
try {
|
|
2665
|
-
parsed = JSON.parse(trimmed);
|
|
2666
|
-
} catch {
|
|
2667
|
-
return null;
|
|
2668
|
-
}
|
|
2669
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2670
|
-
return null;
|
|
2671
|
-
}
|
|
2672
|
-
const wrapperType = parsed.type;
|
|
2673
|
-
if (wrapperType === "stream_event") {
|
|
2674
|
-
return parseStreamEvent(parsed);
|
|
2675
|
-
}
|
|
2676
|
-
if (wrapperType === "system") {
|
|
2677
|
-
return parseSystemEvent(parsed);
|
|
2678
|
-
}
|
|
2679
|
-
if (wrapperType === "result") {
|
|
2680
|
-
return parseResultEvent(parsed);
|
|
2681
|
-
}
|
|
2682
|
-
return null;
|
|
2683
|
-
}
|
|
2684
|
-
function parseStreamEvent(parsed) {
|
|
2685
|
-
const event = parsed.event;
|
|
2686
|
-
if (!event || typeof event !== "object") {
|
|
2687
|
-
return null;
|
|
2688
|
-
}
|
|
2689
|
-
const eventType = event.type;
|
|
2690
|
-
if (eventType === "content_block_start") {
|
|
2691
|
-
return parseContentBlockStart(event);
|
|
2692
|
-
}
|
|
2693
|
-
if (eventType === "content_block_delta") {
|
|
2694
|
-
return parseContentBlockDelta(event);
|
|
2695
|
-
}
|
|
2696
|
-
if (eventType === "content_block_stop") {
|
|
2697
|
-
return { type: "tool-complete" };
|
|
2698
|
-
}
|
|
2699
|
-
return null;
|
|
2700
|
-
}
|
|
2701
|
-
function parseContentBlockStart(event) {
|
|
2702
|
-
const contentBlock = event.content_block;
|
|
2703
|
-
if (!contentBlock || typeof contentBlock !== "object") {
|
|
2704
|
-
return null;
|
|
2705
|
-
}
|
|
2706
|
-
if (contentBlock.type === "tool_use") {
|
|
2707
|
-
const name = contentBlock.name;
|
|
2708
|
-
const id = contentBlock.id;
|
|
2709
|
-
if (typeof name === "string" && typeof id === "string") {
|
|
2710
|
-
return { type: "tool-start", name, id };
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
return null;
|
|
2714
|
-
}
|
|
2715
|
-
function parseContentBlockDelta(event) {
|
|
2716
|
-
const delta = event.delta;
|
|
2717
|
-
if (!delta || typeof delta !== "object") {
|
|
2718
|
-
return null;
|
|
2719
|
-
}
|
|
2720
|
-
if (delta.type === "input_json_delta") {
|
|
2721
|
-
const partialJson = delta.partial_json;
|
|
2722
|
-
if (typeof partialJson === "string") {
|
|
2723
|
-
return { type: "tool-input", partial: partialJson };
|
|
2724
|
-
}
|
|
2725
|
-
return null;
|
|
2726
|
-
}
|
|
2727
|
-
if (delta.type === "text_delta") {
|
|
2728
|
-
const text = delta.text;
|
|
2729
|
-
if (typeof text === "string") {
|
|
2730
|
-
return { type: "text", text };
|
|
2731
|
-
}
|
|
2732
|
-
return null;
|
|
2733
|
-
}
|
|
2734
|
-
return null;
|
|
2735
|
-
}
|
|
2736
|
-
function parseSystemEvent(parsed) {
|
|
2737
|
-
const subtype = parsed.subtype;
|
|
2738
|
-
if (subtype === "api_retry") {
|
|
2739
|
-
const attempt = parsed.attempt;
|
|
2740
|
-
const delay = parsed.retry_delay_ms;
|
|
2741
|
-
if (typeof attempt === "number" && typeof delay === "number") {
|
|
2742
|
-
return { type: "retry", attempt, delay };
|
|
3307
|
+
`;
|
|
3308
|
+
for (const story of config.flaggedStories) {
|
|
3309
|
+
prompt += `- \`${story}\`
|
|
3310
|
+
`;
|
|
2743
3311
|
}
|
|
2744
|
-
return null;
|
|
2745
|
-
}
|
|
2746
|
-
return null;
|
|
2747
|
-
}
|
|
2748
|
-
function parseResultEvent(parsed) {
|
|
2749
|
-
const costUsd = parsed.cost_usd;
|
|
2750
|
-
const sessionId = parsed.session_id;
|
|
2751
|
-
if (typeof costUsd === "number" && typeof sessionId === "string") {
|
|
2752
|
-
return { type: "result", cost: costUsd, sessionId };
|
|
2753
3312
|
}
|
|
2754
|
-
return
|
|
3313
|
+
return prompt;
|
|
2755
3314
|
}
|
|
2756
3315
|
|
|
2757
3316
|
// src/lib/ink-renderer.tsx
|
|
2758
3317
|
import { render as inkRender } from "ink";
|
|
2759
3318
|
|
|
2760
3319
|
// src/lib/ink-components.tsx
|
|
2761
|
-
import { Text, Box
|
|
3320
|
+
import { Text as Text2, Box as Box3 } from "ink";
|
|
3321
|
+
|
|
3322
|
+
// src/lib/ink-activity-components.tsx
|
|
3323
|
+
import { Text, Box } from "ink";
|
|
2762
3324
|
import { Spinner } from "@inkjs/ui";
|
|
2763
3325
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2764
|
-
function Header({ info: info2 }) {
|
|
2765
|
-
if (!info2) return null;
|
|
2766
|
-
const pct = info2.total > 0 ? Math.round(info2.done / info2.total * 100) : 0;
|
|
2767
|
-
return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2768
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C6 " }),
|
|
2769
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: info2.storyKey || "(waiting)" }),
|
|
2770
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 " }),
|
|
2771
|
-
/* @__PURE__ */ jsx(Text, { color: "yellow", children: info2.phase || "..." }),
|
|
2772
|
-
info2.elapsed && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2502 ${info2.elapsed}` }),
|
|
2773
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 Sprint: " }),
|
|
2774
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: info2.done }),
|
|
2775
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "/" }),
|
|
2776
|
-
/* @__PURE__ */ jsx(Text, { children: String(info2.total) }),
|
|
2777
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${pct}%)` })
|
|
2778
|
-
] });
|
|
2779
|
-
}
|
|
2780
|
-
function shortKey(key) {
|
|
2781
|
-
const m = key.match(/^(\d+-\d+)/);
|
|
2782
|
-
return m ? m[1] : key;
|
|
2783
|
-
}
|
|
2784
|
-
function StoryBreakdown({ stories }) {
|
|
2785
|
-
if (stories.length === 0) return null;
|
|
2786
|
-
const groups = {};
|
|
2787
|
-
for (const s of stories) {
|
|
2788
|
-
if (!groups[s.status]) groups[s.status] = [];
|
|
2789
|
-
groups[s.status].push(s.key);
|
|
2790
|
-
}
|
|
2791
|
-
return /* @__PURE__ */ jsxs(Box, { paddingX: 1, gap: 2, children: [
|
|
2792
|
-
groups["done"]?.length && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2793
|
-
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
2794
|
-
groups["done"].length,
|
|
2795
|
-
" \u2713"
|
|
2796
|
-
] }),
|
|
2797
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " done" })
|
|
2798
|
-
] }),
|
|
2799
|
-
groups["in-progress"]?.map((k) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
2800
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25C6 " }),
|
|
2801
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: k })
|
|
2802
|
-
] }, k)),
|
|
2803
|
-
groups["pending"]?.length && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2804
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "next: " }),
|
|
2805
|
-
/* @__PURE__ */ jsx(Text, { children: groups["pending"].slice(0, 3).map((k) => shortKey(k)).join(" ") }),
|
|
2806
|
-
groups["pending"].length > 3 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` +${groups["pending"].length - 3}` })
|
|
2807
|
-
] }),
|
|
2808
|
-
groups["failed"]?.map((k) => /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
2809
|
-
"\u2717 ",
|
|
2810
|
-
shortKey(k)
|
|
2811
|
-
] }) }, k)),
|
|
2812
|
-
groups["blocked"]?.map((k) => /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
2813
|
-
"\u2715 ",
|
|
2814
|
-
shortKey(k)
|
|
2815
|
-
] }) }, k))
|
|
2816
|
-
] });
|
|
2817
|
-
}
|
|
2818
3326
|
var MESSAGE_STYLE = {
|
|
2819
3327
|
ok: { prefix: "[OK]", color: "green" },
|
|
2820
3328
|
warn: { prefix: "[WARN]", color: "yellow" },
|
|
@@ -2873,24 +3381,159 @@ function RetryNotice({ info: info2 }) {
|
|
|
2873
3381
|
"ms)"
|
|
2874
3382
|
] });
|
|
2875
3383
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
/* @__PURE__ */
|
|
2883
|
-
/* @__PURE__ */
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3384
|
+
|
|
3385
|
+
// src/lib/ink-app.tsx
|
|
3386
|
+
import { Box as Box2, Static } from "ink";
|
|
3387
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
3388
|
+
function App({ state }) {
|
|
3389
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
3390
|
+
/* @__PURE__ */ jsx2(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx2(StoryMessageLine, { msg }, i) }),
|
|
3391
|
+
/* @__PURE__ */ jsx2(Header, { info: state.sprintInfo }),
|
|
3392
|
+
/* @__PURE__ */ jsx2(StoryBreakdown, { stories: state.stories, sprintInfo: state.sprintInfo }),
|
|
3393
|
+
state.stories.length > 0 && /* @__PURE__ */ jsx2(Separator, {}),
|
|
3394
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, children: [
|
|
3395
|
+
/* @__PURE__ */ jsx2(CompletedTools, { tools: state.completedTools }),
|
|
3396
|
+
state.activeTool && /* @__PURE__ */ jsx2(ActiveTool, { name: state.activeTool.name }),
|
|
3397
|
+
state.lastThought && /* @__PURE__ */ jsx2(LastThought, { text: state.lastThought }),
|
|
3398
|
+
state.retryInfo && /* @__PURE__ */ jsx2(RetryNotice, { info: state.retryInfo })
|
|
2888
3399
|
] })
|
|
2889
3400
|
] });
|
|
2890
3401
|
}
|
|
2891
3402
|
|
|
3403
|
+
// src/lib/ink-components.tsx
|
|
3404
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
3405
|
+
function Separator() {
|
|
3406
|
+
const width = process.stdout.columns || 60;
|
|
3407
|
+
return /* @__PURE__ */ jsx3(Text2, { children: "\u2501".repeat(width) });
|
|
3408
|
+
}
|
|
3409
|
+
function shortKey(key) {
|
|
3410
|
+
const m = key.match(/^(\d+-\d+)/);
|
|
3411
|
+
return m ? m[1] : key;
|
|
3412
|
+
}
|
|
3413
|
+
function formatCost(cost) {
|
|
3414
|
+
return `$${cost.toFixed(2)}`;
|
|
3415
|
+
}
|
|
3416
|
+
function Header({ info: info2 }) {
|
|
3417
|
+
if (!info2) return null;
|
|
3418
|
+
const parts = ["codeharness run"];
|
|
3419
|
+
if (info2.iterationCount != null) {
|
|
3420
|
+
parts.push(`iteration ${info2.iterationCount}`);
|
|
3421
|
+
}
|
|
3422
|
+
if (info2.elapsed) {
|
|
3423
|
+
parts.push(`${info2.elapsed} elapsed`);
|
|
3424
|
+
}
|
|
3425
|
+
if (info2.totalCost != null) {
|
|
3426
|
+
parts.push(`${formatCost(info2.totalCost)} spent`);
|
|
3427
|
+
}
|
|
3428
|
+
const headerLine = parts.join(" | ");
|
|
3429
|
+
let phaseLine = "";
|
|
3430
|
+
if (info2.phase) {
|
|
3431
|
+
phaseLine = `Phase: ${info2.phase}`;
|
|
3432
|
+
if (info2.acProgress) {
|
|
3433
|
+
phaseLine += ` \u2192 AC ${info2.acProgress}`;
|
|
3434
|
+
}
|
|
3435
|
+
if (info2.currentCommand) {
|
|
3436
|
+
phaseLine += ` (${info2.currentCommand})`;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
3440
|
+
/* @__PURE__ */ jsx3(Text2, { children: headerLine }),
|
|
3441
|
+
/* @__PURE__ */ jsx3(Separator, {}),
|
|
3442
|
+
/* @__PURE__ */ jsx3(Text2, { children: `Story: ${info2.storyKey || "(waiting)"}` }),
|
|
3443
|
+
phaseLine && /* @__PURE__ */ jsx3(Text2, { children: phaseLine })
|
|
3444
|
+
] });
|
|
3445
|
+
}
|
|
3446
|
+
function StoryBreakdown({ stories, sprintInfo }) {
|
|
3447
|
+
if (stories.length === 0) return null;
|
|
3448
|
+
const done = [];
|
|
3449
|
+
const inProgress = [];
|
|
3450
|
+
const pending = [];
|
|
3451
|
+
const failed = [];
|
|
3452
|
+
const blocked = [];
|
|
3453
|
+
for (const s of stories) {
|
|
3454
|
+
switch (s.status) {
|
|
3455
|
+
case "done":
|
|
3456
|
+
done.push(s);
|
|
3457
|
+
break;
|
|
3458
|
+
case "in-progress":
|
|
3459
|
+
inProgress.push(s);
|
|
3460
|
+
break;
|
|
3461
|
+
case "pending":
|
|
3462
|
+
pending.push(s);
|
|
3463
|
+
break;
|
|
3464
|
+
case "failed":
|
|
3465
|
+
failed.push(s);
|
|
3466
|
+
break;
|
|
3467
|
+
case "blocked":
|
|
3468
|
+
blocked.push(s);
|
|
3469
|
+
break;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
const lines = [];
|
|
3473
|
+
if (done.length > 0) {
|
|
3474
|
+
const doneItems = done.map((s) => `${shortKey(s.key)} \u2713`).join(" ");
|
|
3475
|
+
lines.push(
|
|
3476
|
+
/* @__PURE__ */ jsxs3(Text2, { children: [
|
|
3477
|
+
/* @__PURE__ */ jsx3(Text2, { color: "green", children: "Done: " }),
|
|
3478
|
+
/* @__PURE__ */ jsx3(Text2, { color: "green", children: doneItems })
|
|
3479
|
+
] }, "done")
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
if (inProgress.length > 0) {
|
|
3483
|
+
for (const s of inProgress) {
|
|
3484
|
+
let thisText = `${shortKey(s.key)} \u25C6`;
|
|
3485
|
+
if (sprintInfo && sprintInfo.storyKey && shortKey(s.key) === shortKey(sprintInfo.storyKey)) {
|
|
3486
|
+
if (sprintInfo.phase) thisText += ` ${sprintInfo.phase}`;
|
|
3487
|
+
if (sprintInfo.acProgress) thisText += ` (${sprintInfo.acProgress} ACs)`;
|
|
3488
|
+
}
|
|
3489
|
+
lines.push(
|
|
3490
|
+
/* @__PURE__ */ jsxs3(Text2, { children: [
|
|
3491
|
+
/* @__PURE__ */ jsx3(Text2, { color: "cyan", children: "This: " }),
|
|
3492
|
+
/* @__PURE__ */ jsx3(Text2, { color: "cyan", children: thisText })
|
|
3493
|
+
] }, `this-${s.key}`)
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
if (pending.length > 0) {
|
|
3498
|
+
lines.push(
|
|
3499
|
+
/* @__PURE__ */ jsxs3(Text2, { children: [
|
|
3500
|
+
/* @__PURE__ */ jsx3(Text2, { children: "Next: " }),
|
|
3501
|
+
/* @__PURE__ */ jsx3(Text2, { children: shortKey(pending[0].key) }),
|
|
3502
|
+
pending.length > 1 && /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` (+${pending.length - 1} more)` })
|
|
3503
|
+
] }, "next")
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
if (blocked.length > 0) {
|
|
3507
|
+
const blockedItems = blocked.map((s) => {
|
|
3508
|
+
let item = `${shortKey(s.key)} \u2715`;
|
|
3509
|
+
if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
|
|
3510
|
+
return item;
|
|
3511
|
+
}).join(" ");
|
|
3512
|
+
lines.push(
|
|
3513
|
+
/* @__PURE__ */ jsxs3(Text2, { children: [
|
|
3514
|
+
/* @__PURE__ */ jsx3(Text2, { color: "yellow", children: "Blocked: " }),
|
|
3515
|
+
/* @__PURE__ */ jsx3(Text2, { color: "yellow", children: blockedItems })
|
|
3516
|
+
] }, "blocked")
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
if (failed.length > 0) {
|
|
3520
|
+
const failedItems = failed.map((s) => {
|
|
3521
|
+
let item = `${shortKey(s.key)} \u2717`;
|
|
3522
|
+
if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
|
|
3523
|
+
return item;
|
|
3524
|
+
}).join(" ");
|
|
3525
|
+
lines.push(
|
|
3526
|
+
/* @__PURE__ */ jsxs3(Text2, { children: [
|
|
3527
|
+
/* @__PURE__ */ jsx3(Text2, { color: "red", children: "Failed: " }),
|
|
3528
|
+
/* @__PURE__ */ jsx3(Text2, { color: "red", children: failedItems })
|
|
3529
|
+
] }, "failed")
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: lines });
|
|
3533
|
+
}
|
|
3534
|
+
|
|
2892
3535
|
// src/lib/ink-renderer.tsx
|
|
2893
|
-
import { jsx as
|
|
3536
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
2894
3537
|
var noopHandle = {
|
|
2895
3538
|
update() {
|
|
2896
3539
|
},
|
|
@@ -2919,18 +3562,16 @@ function startRenderer(options) {
|
|
|
2919
3562
|
retryInfo: null
|
|
2920
3563
|
};
|
|
2921
3564
|
let cleaned = false;
|
|
2922
|
-
const inkInstance = inkRender(/* @__PURE__ */
|
|
3565
|
+
const inkInstance = inkRender(/* @__PURE__ */ jsx4(App, { state }), {
|
|
2923
3566
|
exitOnCtrlC: false,
|
|
2924
|
-
patchConsole:
|
|
3567
|
+
patchConsole: !options?._forceTTY,
|
|
2925
3568
|
incrementalRendering: true,
|
|
2926
|
-
// Only redraw changed lines (v6.5+)
|
|
2927
3569
|
maxFps: 15
|
|
2928
|
-
// Dashboard doesn't need 30fps
|
|
2929
3570
|
});
|
|
2930
3571
|
function rerender() {
|
|
2931
3572
|
if (!cleaned) {
|
|
2932
3573
|
state = { ...state };
|
|
2933
|
-
inkInstance.rerender(/* @__PURE__ */
|
|
3574
|
+
inkInstance.rerender(/* @__PURE__ */ jsx4(App, { state }));
|
|
2934
3575
|
}
|
|
2935
3576
|
}
|
|
2936
3577
|
function cleanup() {
|
|
@@ -2968,7 +3609,8 @@ function startRenderer(options) {
|
|
|
2968
3609
|
break;
|
|
2969
3610
|
case "tool-input":
|
|
2970
3611
|
state.activeToolArgs += event.partial;
|
|
2971
|
-
|
|
3612
|
+
return;
|
|
3613
|
+
// Skip rerender — args only shown on completion
|
|
2972
3614
|
case "tool-complete":
|
|
2973
3615
|
if (state.activeTool) {
|
|
2974
3616
|
const entry = {
|
|
@@ -2989,13 +3631,28 @@ function startRenderer(options) {
|
|
|
2989
3631
|
state.retryInfo = { attempt: event.attempt, delay: event.delay };
|
|
2990
3632
|
break;
|
|
2991
3633
|
case "result":
|
|
3634
|
+
if (event.cost > 0 && state.sprintInfo) {
|
|
3635
|
+
state.sprintInfo = {
|
|
3636
|
+
...state.sprintInfo,
|
|
3637
|
+
totalCost: (state.sprintInfo.totalCost ?? 0) + event.cost
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
2992
3640
|
break;
|
|
2993
3641
|
}
|
|
2994
3642
|
rerender();
|
|
2995
3643
|
}
|
|
2996
3644
|
function updateSprintState(sprintState) {
|
|
2997
3645
|
if (cleaned) return;
|
|
2998
|
-
state.sprintInfo
|
|
3646
|
+
if (sprintState && state.sprintInfo) {
|
|
3647
|
+
state.sprintInfo = {
|
|
3648
|
+
...sprintState,
|
|
3649
|
+
totalCost: sprintState.totalCost ?? state.sprintInfo.totalCost,
|
|
3650
|
+
acProgress: sprintState.acProgress ?? state.sprintInfo.acProgress,
|
|
3651
|
+
currentCommand: sprintState.currentCommand ?? state.sprintInfo.currentCommand
|
|
3652
|
+
};
|
|
3653
|
+
} else {
|
|
3654
|
+
state.sprintInfo = sprintState ?? null;
|
|
3655
|
+
}
|
|
2999
3656
|
rerender();
|
|
3000
3657
|
}
|
|
3001
3658
|
function updateStories(stories) {
|
|
@@ -3012,12 +3669,12 @@ function startRenderer(options) {
|
|
|
3012
3669
|
}
|
|
3013
3670
|
|
|
3014
3671
|
// src/modules/sprint/state.ts
|
|
3015
|
-
import { readFileSync as
|
|
3016
|
-
import { join as
|
|
3672
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, renameSync, existsSync as existsSync14 } from "fs";
|
|
3673
|
+
import { join as join13 } from "path";
|
|
3017
3674
|
|
|
3018
3675
|
// src/modules/sprint/migration.ts
|
|
3019
|
-
import { readFileSync as
|
|
3020
|
-
import { join as
|
|
3676
|
+
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
3677
|
+
import { join as join12 } from "path";
|
|
3021
3678
|
var OLD_FILES = {
|
|
3022
3679
|
storyRetries: "ralph/.story_retries",
|
|
3023
3680
|
flaggedStories: "ralph/.flagged_stories",
|
|
@@ -3026,13 +3683,13 @@ var OLD_FILES = {
|
|
|
3026
3683
|
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
3027
3684
|
};
|
|
3028
3685
|
function resolve(relative2) {
|
|
3029
|
-
return
|
|
3686
|
+
return join12(process.cwd(), relative2);
|
|
3030
3687
|
}
|
|
3031
3688
|
function readIfExists(relative2) {
|
|
3032
3689
|
const p = resolve(relative2);
|
|
3033
|
-
if (!
|
|
3690
|
+
if (!existsSync13(p)) return null;
|
|
3034
3691
|
try {
|
|
3035
|
-
return
|
|
3692
|
+
return readFileSync10(p, "utf-8");
|
|
3036
3693
|
} catch {
|
|
3037
3694
|
return null;
|
|
3038
3695
|
}
|
|
@@ -3101,7 +3758,11 @@ function parseRalphStatus(content) {
|
|
|
3101
3758
|
iteration: data.loop_count ?? 0,
|
|
3102
3759
|
cost: 0,
|
|
3103
3760
|
completed: [],
|
|
3104
|
-
failed: []
|
|
3761
|
+
failed: [],
|
|
3762
|
+
currentStory: null,
|
|
3763
|
+
currentPhase: null,
|
|
3764
|
+
lastAction: null,
|
|
3765
|
+
acProgress: null
|
|
3105
3766
|
};
|
|
3106
3767
|
} catch {
|
|
3107
3768
|
return null;
|
|
@@ -3132,7 +3793,7 @@ function parseSessionIssues(content) {
|
|
|
3132
3793
|
return items;
|
|
3133
3794
|
}
|
|
3134
3795
|
function migrateFromOldFormat() {
|
|
3135
|
-
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) =>
|
|
3796
|
+
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync13(resolve(rel)));
|
|
3136
3797
|
if (!hasAnyOldFile) return fail2("No old format files found for migration");
|
|
3137
3798
|
try {
|
|
3138
3799
|
const stories = {};
|
|
@@ -3173,10 +3834,10 @@ function projectRoot() {
|
|
|
3173
3834
|
return process.cwd();
|
|
3174
3835
|
}
|
|
3175
3836
|
function statePath() {
|
|
3176
|
-
return
|
|
3837
|
+
return join13(projectRoot(), "sprint-state.json");
|
|
3177
3838
|
}
|
|
3178
3839
|
function tmpPath() {
|
|
3179
|
-
return
|
|
3840
|
+
return join13(projectRoot(), ".sprint-state.json.tmp");
|
|
3180
3841
|
}
|
|
3181
3842
|
function defaultState() {
|
|
3182
3843
|
return {
|
|
@@ -3209,7 +3870,7 @@ function writeStateAtomic(state) {
|
|
|
3209
3870
|
const data = JSON.stringify(state, null, 2) + "\n";
|
|
3210
3871
|
const tmp = tmpPath();
|
|
3211
3872
|
const final = statePath();
|
|
3212
|
-
|
|
3873
|
+
writeFileSync7(tmp, data, "utf-8");
|
|
3213
3874
|
renameSync(tmp, final);
|
|
3214
3875
|
return ok2(void 0);
|
|
3215
3876
|
} catch (err) {
|
|
@@ -3219,9 +3880,9 @@ function writeStateAtomic(state) {
|
|
|
3219
3880
|
}
|
|
3220
3881
|
function getSprintState() {
|
|
3221
3882
|
const fp = statePath();
|
|
3222
|
-
if (
|
|
3883
|
+
if (existsSync14(fp)) {
|
|
3223
3884
|
try {
|
|
3224
|
-
const raw =
|
|
3885
|
+
const raw = readFileSync11(fp, "utf-8");
|
|
3225
3886
|
const parsed = JSON.parse(raw);
|
|
3226
3887
|
const defaults = defaultState();
|
|
3227
3888
|
const run = parsed.run;
|
|
@@ -3518,9 +4179,9 @@ function generateReport(state, now) {
|
|
|
3518
4179
|
}
|
|
3519
4180
|
|
|
3520
4181
|
// src/modules/sprint/timeout.ts
|
|
3521
|
-
import { readFileSync as
|
|
4182
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
|
|
3522
4183
|
import { execSync } from "child_process";
|
|
3523
|
-
import { join as
|
|
4184
|
+
import { join as join14 } from "path";
|
|
3524
4185
|
var GIT_TIMEOUT_MS = 5e3;
|
|
3525
4186
|
var DEFAULT_MAX_LINES = 100;
|
|
3526
4187
|
function captureGitDiff() {
|
|
@@ -3549,14 +4210,14 @@ function captureGitDiff() {
|
|
|
3549
4210
|
}
|
|
3550
4211
|
function captureStateDelta(beforePath, afterPath) {
|
|
3551
4212
|
try {
|
|
3552
|
-
if (!
|
|
4213
|
+
if (!existsSync15(beforePath)) {
|
|
3553
4214
|
return fail2(`State snapshot not found: ${beforePath}`);
|
|
3554
4215
|
}
|
|
3555
|
-
if (!
|
|
4216
|
+
if (!existsSync15(afterPath)) {
|
|
3556
4217
|
return fail2(`Current state file not found: ${afterPath}`);
|
|
3557
4218
|
}
|
|
3558
|
-
const beforeRaw =
|
|
3559
|
-
const afterRaw =
|
|
4219
|
+
const beforeRaw = readFileSync12(beforePath, "utf-8");
|
|
4220
|
+
const afterRaw = readFileSync12(afterPath, "utf-8");
|
|
3560
4221
|
const before = JSON.parse(beforeRaw);
|
|
3561
4222
|
const after = JSON.parse(afterRaw);
|
|
3562
4223
|
const beforeStories = before.stories ?? {};
|
|
@@ -3581,10 +4242,10 @@ function captureStateDelta(beforePath, afterPath) {
|
|
|
3581
4242
|
}
|
|
3582
4243
|
function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
|
|
3583
4244
|
try {
|
|
3584
|
-
if (!
|
|
4245
|
+
if (!existsSync15(outputFile)) {
|
|
3585
4246
|
return fail2(`Output file not found: ${outputFile}`);
|
|
3586
4247
|
}
|
|
3587
|
-
const content =
|
|
4248
|
+
const content = readFileSync12(outputFile, "utf-8");
|
|
3588
4249
|
const lines = content.split("\n");
|
|
3589
4250
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
3590
4251
|
lines.pop();
|
|
@@ -3632,7 +4293,7 @@ function captureTimeoutReport(opts) {
|
|
|
3632
4293
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3633
4294
|
const gitResult = captureGitDiff();
|
|
3634
4295
|
const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
|
|
3635
|
-
const statePath2 =
|
|
4296
|
+
const statePath2 = join14(process.cwd(), "sprint-state.json");
|
|
3636
4297
|
const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
|
|
3637
4298
|
const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
|
|
3638
4299
|
const stderrResult = capturePartialStderr(opts.outputFile);
|
|
@@ -3646,15 +4307,15 @@ function captureTimeoutReport(opts) {
|
|
|
3646
4307
|
partialStderr,
|
|
3647
4308
|
timestamp
|
|
3648
4309
|
};
|
|
3649
|
-
const reportDir =
|
|
4310
|
+
const reportDir = join14(process.cwd(), "ralph", "logs");
|
|
3650
4311
|
const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3651
4312
|
const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
|
|
3652
|
-
const reportPath =
|
|
3653
|
-
if (!
|
|
4313
|
+
const reportPath = join14(reportDir, reportFileName);
|
|
4314
|
+
if (!existsSync15(reportDir)) {
|
|
3654
4315
|
mkdirSync3(reportDir, { recursive: true });
|
|
3655
4316
|
}
|
|
3656
4317
|
const reportContent = formatReport(capture);
|
|
3657
|
-
|
|
4318
|
+
writeFileSync8(reportPath, reportContent, "utf-8");
|
|
3658
4319
|
return ok2({
|
|
3659
4320
|
filePath: reportPath,
|
|
3660
4321
|
capture
|
|
@@ -3666,14 +4327,14 @@ function captureTimeoutReport(opts) {
|
|
|
3666
4327
|
}
|
|
3667
4328
|
function findLatestTimeoutReport(storyKey) {
|
|
3668
4329
|
try {
|
|
3669
|
-
const reportDir =
|
|
3670
|
-
if (!
|
|
4330
|
+
const reportDir = join14(process.cwd(), "ralph", "logs");
|
|
4331
|
+
if (!existsSync15(reportDir)) {
|
|
3671
4332
|
return ok2(null);
|
|
3672
4333
|
}
|
|
3673
4334
|
const safeStoryKey = storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3674
4335
|
const prefix = `timeout-report-`;
|
|
3675
4336
|
const suffix = `-${safeStoryKey}.md`;
|
|
3676
|
-
const files =
|
|
4337
|
+
const files = readdirSync3(reportDir, { encoding: "utf-8" });
|
|
3677
4338
|
const matches = [];
|
|
3678
4339
|
for (const f of files) {
|
|
3679
4340
|
if (f.startsWith(prefix) && f.endsWith(suffix)) {
|
|
@@ -3689,8 +4350,8 @@ function findLatestTimeoutReport(storyKey) {
|
|
|
3689
4350
|
}
|
|
3690
4351
|
matches.sort((a, b) => b.iteration - a.iteration);
|
|
3691
4352
|
const latest = matches[0];
|
|
3692
|
-
const reportPath =
|
|
3693
|
-
const content =
|
|
4353
|
+
const reportPath = join14(reportDir, latest.fileName);
|
|
4354
|
+
const content = readFileSync12(reportPath, "utf-8");
|
|
3694
4355
|
let durationMinutes = 0;
|
|
3695
4356
|
let filesChanged = 0;
|
|
3696
4357
|
const durationMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*minutes/);
|
|
@@ -3719,12 +4380,12 @@ function findLatestTimeoutReport(storyKey) {
|
|
|
3719
4380
|
}
|
|
3720
4381
|
|
|
3721
4382
|
// src/modules/sprint/feedback.ts
|
|
3722
|
-
import { readFileSync as
|
|
3723
|
-
import { existsSync as
|
|
3724
|
-
import { join as
|
|
4383
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
4384
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4385
|
+
import { join as join15 } from "path";
|
|
3725
4386
|
|
|
3726
4387
|
// src/modules/sprint/validator.ts
|
|
3727
|
-
import { readFileSync as
|
|
4388
|
+
import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
|
|
3728
4389
|
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
3729
4390
|
"backlog",
|
|
3730
4391
|
"ready",
|
|
@@ -3755,10 +4416,10 @@ function parseSprintStatusKeys(content) {
|
|
|
3755
4416
|
}
|
|
3756
4417
|
function parseStateFile(statePath2) {
|
|
3757
4418
|
try {
|
|
3758
|
-
if (!
|
|
4419
|
+
if (!existsSync17(statePath2)) {
|
|
3759
4420
|
return fail2(`State file not found: ${statePath2}`);
|
|
3760
4421
|
}
|
|
3761
|
-
const raw =
|
|
4422
|
+
const raw = readFileSync14(statePath2, "utf-8");
|
|
3762
4423
|
const parsed = JSON.parse(raw);
|
|
3763
4424
|
return ok2(parsed);
|
|
3764
4425
|
} catch (err) {
|
|
@@ -3773,10 +4434,10 @@ function validateStateConsistency(statePath2, sprintStatusPath) {
|
|
|
3773
4434
|
return fail2(stateResult.error);
|
|
3774
4435
|
}
|
|
3775
4436
|
const state = stateResult.data;
|
|
3776
|
-
if (!
|
|
4437
|
+
if (!existsSync17(sprintStatusPath)) {
|
|
3777
4438
|
return fail2(`Sprint status file not found: ${sprintStatusPath}`);
|
|
3778
4439
|
}
|
|
3779
|
-
const statusContent =
|
|
4440
|
+
const statusContent = readFileSync14(sprintStatusPath, "utf-8");
|
|
3780
4441
|
const keysResult = parseSprintStatusKeys(statusContent);
|
|
3781
4442
|
if (!keysResult.success) {
|
|
3782
4443
|
return fail2(keysResult.error);
|
|
@@ -3894,6 +4555,109 @@ function clearRunProgress2() {
|
|
|
3894
4555
|
return clearRunProgress();
|
|
3895
4556
|
}
|
|
3896
4557
|
|
|
4558
|
+
// src/lib/run-helpers.ts
|
|
4559
|
+
import { StringDecoder } from "string_decoder";
|
|
4560
|
+
|
|
4561
|
+
// src/lib/stream-parser.ts
|
|
4562
|
+
function parseStreamLine(line) {
|
|
4563
|
+
const trimmed = line.trim();
|
|
4564
|
+
if (trimmed.length === 0) {
|
|
4565
|
+
return null;
|
|
4566
|
+
}
|
|
4567
|
+
let parsed;
|
|
4568
|
+
try {
|
|
4569
|
+
parsed = JSON.parse(trimmed);
|
|
4570
|
+
} catch {
|
|
4571
|
+
return null;
|
|
4572
|
+
}
|
|
4573
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4574
|
+
return null;
|
|
4575
|
+
}
|
|
4576
|
+
const wrapperType = parsed.type;
|
|
4577
|
+
if (wrapperType === "stream_event") {
|
|
4578
|
+
return parseStreamEvent(parsed);
|
|
4579
|
+
}
|
|
4580
|
+
if (wrapperType === "system") {
|
|
4581
|
+
return parseSystemEvent(parsed);
|
|
4582
|
+
}
|
|
4583
|
+
if (wrapperType === "result") {
|
|
4584
|
+
return parseResultEvent(parsed);
|
|
4585
|
+
}
|
|
4586
|
+
return null;
|
|
4587
|
+
}
|
|
4588
|
+
function parseStreamEvent(parsed) {
|
|
4589
|
+
const event = parsed.event;
|
|
4590
|
+
if (!event || typeof event !== "object") {
|
|
4591
|
+
return null;
|
|
4592
|
+
}
|
|
4593
|
+
const eventType = event.type;
|
|
4594
|
+
if (eventType === "content_block_start") {
|
|
4595
|
+
return parseContentBlockStart(event);
|
|
4596
|
+
}
|
|
4597
|
+
if (eventType === "content_block_delta") {
|
|
4598
|
+
return parseContentBlockDelta(event);
|
|
4599
|
+
}
|
|
4600
|
+
if (eventType === "content_block_stop") {
|
|
4601
|
+
return { type: "tool-complete" };
|
|
4602
|
+
}
|
|
4603
|
+
return null;
|
|
4604
|
+
}
|
|
4605
|
+
function parseContentBlockStart(event) {
|
|
4606
|
+
const contentBlock = event.content_block;
|
|
4607
|
+
if (!contentBlock || typeof contentBlock !== "object") {
|
|
4608
|
+
return null;
|
|
4609
|
+
}
|
|
4610
|
+
if (contentBlock.type === "tool_use") {
|
|
4611
|
+
const name = contentBlock.name;
|
|
4612
|
+
const id = contentBlock.id;
|
|
4613
|
+
if (typeof name === "string" && typeof id === "string") {
|
|
4614
|
+
return { type: "tool-start", name, id };
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
return null;
|
|
4618
|
+
}
|
|
4619
|
+
function parseContentBlockDelta(event) {
|
|
4620
|
+
const delta = event.delta;
|
|
4621
|
+
if (!delta || typeof delta !== "object") {
|
|
4622
|
+
return null;
|
|
4623
|
+
}
|
|
4624
|
+
if (delta.type === "input_json_delta") {
|
|
4625
|
+
const partialJson = delta.partial_json;
|
|
4626
|
+
if (typeof partialJson === "string") {
|
|
4627
|
+
return { type: "tool-input", partial: partialJson };
|
|
4628
|
+
}
|
|
4629
|
+
return null;
|
|
4630
|
+
}
|
|
4631
|
+
if (delta.type === "text_delta") {
|
|
4632
|
+
const text = delta.text;
|
|
4633
|
+
if (typeof text === "string") {
|
|
4634
|
+
return { type: "text", text };
|
|
4635
|
+
}
|
|
4636
|
+
return null;
|
|
4637
|
+
}
|
|
4638
|
+
return null;
|
|
4639
|
+
}
|
|
4640
|
+
function parseSystemEvent(parsed) {
|
|
4641
|
+
const subtype = parsed.subtype;
|
|
4642
|
+
if (subtype === "api_retry") {
|
|
4643
|
+
const attempt = parsed.attempt;
|
|
4644
|
+
const delay = parsed.retry_delay_ms;
|
|
4645
|
+
if (typeof attempt === "number" && typeof delay === "number") {
|
|
4646
|
+
return { type: "retry", attempt, delay };
|
|
4647
|
+
}
|
|
4648
|
+
return null;
|
|
4649
|
+
}
|
|
4650
|
+
return null;
|
|
4651
|
+
}
|
|
4652
|
+
function parseResultEvent(parsed) {
|
|
4653
|
+
const costUsd = parsed.cost_usd;
|
|
4654
|
+
const sessionId = parsed.session_id;
|
|
4655
|
+
if (typeof costUsd === "number" && typeof sessionId === "string") {
|
|
4656
|
+
return { type: "result", cost: costUsd, sessionId };
|
|
4657
|
+
}
|
|
4658
|
+
return null;
|
|
4659
|
+
}
|
|
4660
|
+
|
|
3897
4661
|
// src/lib/run-helpers.ts
|
|
3898
4662
|
var STORY_KEY_PATTERN = /^\d+-\d+-/;
|
|
3899
4663
|
function countStories(statuses) {
|
|
@@ -3928,6 +4692,9 @@ function buildSpawnArgs(opts) {
|
|
|
3928
4692
|
"--prompt",
|
|
3929
4693
|
opts.promptFile
|
|
3930
4694
|
];
|
|
4695
|
+
if (!opts.quiet) {
|
|
4696
|
+
args.push("--live");
|
|
4697
|
+
}
|
|
3931
4698
|
if (opts.maxStoryRetries !== void 0) {
|
|
3932
4699
|
args.push("--max-story-retries", String(opts.maxStoryRetries));
|
|
3933
4700
|
}
|
|
@@ -3979,6 +4746,7 @@ var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
|
|
|
3979
4746
|
var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
|
|
3980
4747
|
var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
|
|
3981
4748
|
var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
|
|
4749
|
+
var LOOP_ITERATION = /\[LOOP\]\s+iteration\s+(\d+)/;
|
|
3982
4750
|
var ERROR_LINE = /\[ERROR\]\s+(.+)/;
|
|
3983
4751
|
function parseRalphMessage(rawLine) {
|
|
3984
4752
|
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
@@ -4023,6 +4791,41 @@ function parseRalphMessage(rawLine) {
|
|
|
4023
4791
|
}
|
|
4024
4792
|
return null;
|
|
4025
4793
|
}
|
|
4794
|
+
function parseIterationMessage(rawLine) {
|
|
4795
|
+
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
4796
|
+
if (clean.length === 0) return null;
|
|
4797
|
+
const match = LOOP_ITERATION.exec(clean);
|
|
4798
|
+
if (match) {
|
|
4799
|
+
return parseInt(match[1], 10);
|
|
4800
|
+
}
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
function createLineProcessor(callbacks, opts) {
|
|
4804
|
+
let partial = "";
|
|
4805
|
+
const decoder = new StringDecoder("utf8");
|
|
4806
|
+
return (data) => {
|
|
4807
|
+
const text = partial + decoder.write(data);
|
|
4808
|
+
const parts = text.split("\n");
|
|
4809
|
+
partial = parts.pop() ?? "";
|
|
4810
|
+
for (const line of parts) {
|
|
4811
|
+
if (line.trim().length === 0) continue;
|
|
4812
|
+
const event = parseStreamLine(line);
|
|
4813
|
+
if (event) {
|
|
4814
|
+
callbacks.onEvent(event);
|
|
4815
|
+
}
|
|
4816
|
+
if (opts?.parseRalph) {
|
|
4817
|
+
const msg = parseRalphMessage(line);
|
|
4818
|
+
if (msg && callbacks.onMessage) {
|
|
4819
|
+
callbacks.onMessage(msg);
|
|
4820
|
+
}
|
|
4821
|
+
const iteration = parseIterationMessage(line);
|
|
4822
|
+
if (iteration !== null && callbacks.onIteration) {
|
|
4823
|
+
callbacks.onIteration(iteration);
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
};
|
|
4828
|
+
}
|
|
4026
4829
|
|
|
4027
4830
|
// src/commands/run.ts
|
|
4028
4831
|
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
@@ -4033,10 +4836,10 @@ function resolveRalphPath() {
|
|
|
4033
4836
|
if (root.endsWith("/src") || root.endsWith("\\src")) {
|
|
4034
4837
|
root = dirname4(root);
|
|
4035
4838
|
}
|
|
4036
|
-
return
|
|
4839
|
+
return join16(root, "ralph", "ralph.sh");
|
|
4037
4840
|
}
|
|
4038
4841
|
function resolvePluginDir() {
|
|
4039
|
-
return
|
|
4842
|
+
return join16(process.cwd(), ".claude");
|
|
4040
4843
|
}
|
|
4041
4844
|
function registerRunCommand(program) {
|
|
4042
4845
|
program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "43200").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--quiet", "Suppress terminal output (background mode)", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "10").option("--reset", "Clear retry counters, flagged stories, and circuit breaker before starting", false).action(async (options, cmd) => {
|
|
@@ -4044,19 +4847,19 @@ function registerRunCommand(program) {
|
|
|
4044
4847
|
const isJson = !!globalOpts.json;
|
|
4045
4848
|
const outputOpts = { json: isJson };
|
|
4046
4849
|
const ralphPath = resolveRalphPath();
|
|
4047
|
-
if (!
|
|
4850
|
+
if (!existsSync18(ralphPath)) {
|
|
4048
4851
|
fail("Ralph loop not found \u2014 reinstall codeharness", outputOpts);
|
|
4049
4852
|
process.exitCode = 1;
|
|
4050
4853
|
return;
|
|
4051
4854
|
}
|
|
4052
4855
|
const pluginDir = resolvePluginDir();
|
|
4053
|
-
if (!
|
|
4856
|
+
if (!existsSync18(pluginDir)) {
|
|
4054
4857
|
fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
|
|
4055
4858
|
process.exitCode = 1;
|
|
4056
4859
|
return;
|
|
4057
4860
|
}
|
|
4058
4861
|
const projectDir = process.cwd();
|
|
4059
|
-
const sprintStatusPath =
|
|
4862
|
+
const sprintStatusPath = join16(projectDir, SPRINT_STATUS_REL);
|
|
4060
4863
|
const statuses = readSprintStatus(projectDir);
|
|
4061
4864
|
const counts = countStories(statuses);
|
|
4062
4865
|
if (counts.total === 0) {
|
|
@@ -4075,12 +4878,12 @@ function registerRunCommand(program) {
|
|
|
4075
4878
|
process.exitCode = 1;
|
|
4076
4879
|
return;
|
|
4077
4880
|
}
|
|
4078
|
-
const promptFile =
|
|
4079
|
-
const flaggedFilePath =
|
|
4881
|
+
const promptFile = join16(projectDir, "ralph", ".harness-prompt.md");
|
|
4882
|
+
const flaggedFilePath = join16(projectDir, "ralph", ".flagged_stories");
|
|
4080
4883
|
let flaggedStories;
|
|
4081
|
-
if (
|
|
4884
|
+
if (existsSync18(flaggedFilePath)) {
|
|
4082
4885
|
try {
|
|
4083
|
-
const flaggedContent =
|
|
4886
|
+
const flaggedContent = readFileSync15(flaggedFilePath, "utf-8");
|
|
4084
4887
|
flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
|
|
4085
4888
|
} catch {
|
|
4086
4889
|
}
|
|
@@ -4092,7 +4895,7 @@ function registerRunCommand(program) {
|
|
|
4092
4895
|
});
|
|
4093
4896
|
try {
|
|
4094
4897
|
mkdirSync4(dirname4(promptFile), { recursive: true });
|
|
4095
|
-
|
|
4898
|
+
writeFileSync10(promptFile, promptContent, "utf-8");
|
|
4096
4899
|
} catch (err) {
|
|
4097
4900
|
const message = err instanceof Error ? err.message : String(err);
|
|
4098
4901
|
fail(`Failed to write prompt file: ${message}`, outputOpts);
|
|
@@ -4119,6 +4922,7 @@ function registerRunCommand(program) {
|
|
|
4119
4922
|
const rendererHandle = startRenderer({ quiet });
|
|
4120
4923
|
let sprintStateInterval = null;
|
|
4121
4924
|
const sessionStartTime = Date.now();
|
|
4925
|
+
let currentIterationCount = 0;
|
|
4122
4926
|
try {
|
|
4123
4927
|
const initialState = getSprintState2();
|
|
4124
4928
|
if (initialState.success) {
|
|
@@ -4128,7 +4932,8 @@ function registerRunCommand(program) {
|
|
|
4128
4932
|
phase: s.run.currentPhase ?? "",
|
|
4129
4933
|
done: s.sprint.done,
|
|
4130
4934
|
total: s.sprint.total,
|
|
4131
|
-
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4935
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime),
|
|
4936
|
+
iterationCount: currentIterationCount
|
|
4132
4937
|
};
|
|
4133
4938
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4134
4939
|
}
|
|
@@ -4143,30 +4948,18 @@ function registerRunCommand(program) {
|
|
|
4143
4948
|
env
|
|
4144
4949
|
});
|
|
4145
4950
|
if (!quiet && child.stdout && child.stderr) {
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
}
|
|
4159
|
-
if (opts?.parseRalph) {
|
|
4160
|
-
const msg = parseRalphMessage(line);
|
|
4161
|
-
if (msg) {
|
|
4162
|
-
rendererHandle.addMessage(msg);
|
|
4163
|
-
}
|
|
4164
|
-
}
|
|
4165
|
-
}
|
|
4166
|
-
};
|
|
4167
|
-
};
|
|
4168
|
-
child.stdout.on("data", makeLineHandler());
|
|
4169
|
-
child.stderr.on("data", makeLineHandler({ parseRalph: true }));
|
|
4951
|
+
const stdoutHandler = createLineProcessor({
|
|
4952
|
+
onEvent: (event) => rendererHandle.update(event)
|
|
4953
|
+
});
|
|
4954
|
+
const stderrHandler = createLineProcessor({
|
|
4955
|
+
onEvent: (event) => rendererHandle.update(event),
|
|
4956
|
+
onMessage: (msg) => rendererHandle.addMessage(msg),
|
|
4957
|
+
onIteration: (iteration) => {
|
|
4958
|
+
currentIterationCount = iteration;
|
|
4959
|
+
}
|
|
4960
|
+
}, { parseRalph: true });
|
|
4961
|
+
child.stdout.on("data", stdoutHandler);
|
|
4962
|
+
child.stderr.on("data", stderrHandler);
|
|
4170
4963
|
sprintStateInterval = setInterval(() => {
|
|
4171
4964
|
try {
|
|
4172
4965
|
const stateResult = getSprintState2();
|
|
@@ -4177,7 +4970,8 @@ function registerRunCommand(program) {
|
|
|
4177
4970
|
phase: s.run.currentPhase ?? "",
|
|
4178
4971
|
done: s.sprint.done,
|
|
4179
4972
|
total: s.sprint.total,
|
|
4180
|
-
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4973
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime),
|
|
4974
|
+
iterationCount: currentIterationCount
|
|
4181
4975
|
};
|
|
4182
4976
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4183
4977
|
}
|
|
@@ -4201,10 +4995,10 @@ function registerRunCommand(program) {
|
|
|
4201
4995
|
});
|
|
4202
4996
|
});
|
|
4203
4997
|
if (isJson) {
|
|
4204
|
-
const statusFile =
|
|
4205
|
-
if (
|
|
4998
|
+
const statusFile = join16(projectDir, "ralph", "status.json");
|
|
4999
|
+
if (existsSync18(statusFile)) {
|
|
4206
5000
|
try {
|
|
4207
|
-
const statusData = JSON.parse(
|
|
5001
|
+
const statusData = JSON.parse(readFileSync15(statusFile, "utf-8"));
|
|
4208
5002
|
const finalStatuses = readSprintStatus(projectDir);
|
|
4209
5003
|
const finalCounts = countStories(finalStatuses);
|
|
4210
5004
|
jsonOutput({
|
|
@@ -4256,14 +5050,14 @@ function registerRunCommand(program) {
|
|
|
4256
5050
|
}
|
|
4257
5051
|
|
|
4258
5052
|
// src/commands/verify.ts
|
|
4259
|
-
import { existsSync as
|
|
4260
|
-
import { join as
|
|
5053
|
+
import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
|
|
5054
|
+
import { join as join25 } from "path";
|
|
4261
5055
|
|
|
4262
5056
|
// src/modules/verify/index.ts
|
|
4263
|
-
import { readFileSync as
|
|
5057
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
4264
5058
|
|
|
4265
5059
|
// src/modules/verify/proof.ts
|
|
4266
|
-
import { existsSync as
|
|
5060
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
4267
5061
|
function classifyEvidenceCommands(proofContent) {
|
|
4268
5062
|
const results = [];
|
|
4269
5063
|
const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
|
|
@@ -4349,10 +5143,10 @@ function validateProofQuality(proofPath) {
|
|
|
4349
5143
|
otherCount: 0,
|
|
4350
5144
|
blackBoxPass: false
|
|
4351
5145
|
};
|
|
4352
|
-
if (!
|
|
5146
|
+
if (!existsSync19(proofPath)) {
|
|
4353
5147
|
return emptyResult;
|
|
4354
5148
|
}
|
|
4355
|
-
const content =
|
|
5149
|
+
const content = readFileSync16(proofPath, "utf-8");
|
|
4356
5150
|
const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
|
|
4357
5151
|
const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
|
|
4358
5152
|
const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
|
|
@@ -4495,21 +5289,21 @@ function validateProofQuality(proofPath) {
|
|
|
4495
5289
|
|
|
4496
5290
|
// src/modules/verify/orchestrator.ts
|
|
4497
5291
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
4498
|
-
import { mkdirSync as mkdirSync6, writeFileSync as
|
|
4499
|
-
import { join as
|
|
5292
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync12 } from "fs";
|
|
5293
|
+
import { join as join18 } from "path";
|
|
4500
5294
|
|
|
4501
5295
|
// src/lib/doc-health.ts
|
|
4502
5296
|
import { execSync as execSync2 } from "child_process";
|
|
4503
5297
|
import {
|
|
4504
|
-
existsSync as
|
|
5298
|
+
existsSync as existsSync20,
|
|
4505
5299
|
mkdirSync as mkdirSync5,
|
|
4506
|
-
readFileSync as
|
|
4507
|
-
readdirSync as
|
|
5300
|
+
readFileSync as readFileSync17,
|
|
5301
|
+
readdirSync as readdirSync4,
|
|
4508
5302
|
statSync,
|
|
4509
5303
|
unlinkSync,
|
|
4510
|
-
writeFileSync as
|
|
5304
|
+
writeFileSync as writeFileSync11
|
|
4511
5305
|
} from "fs";
|
|
4512
|
-
import { join as
|
|
5306
|
+
import { join as join17, relative } from "path";
|
|
4513
5307
|
var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
|
|
4514
5308
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
4515
5309
|
var DEFAULT_MODULE_THRESHOLD = 3;
|
|
@@ -4520,7 +5314,7 @@ function findModules(dir, threshold) {
|
|
|
4520
5314
|
function walk(current) {
|
|
4521
5315
|
let entries;
|
|
4522
5316
|
try {
|
|
4523
|
-
entries =
|
|
5317
|
+
entries = readdirSync4(current);
|
|
4524
5318
|
} catch {
|
|
4525
5319
|
return;
|
|
4526
5320
|
}
|
|
@@ -4531,7 +5325,7 @@ function findModules(dir, threshold) {
|
|
|
4531
5325
|
let sourceCount = 0;
|
|
4532
5326
|
const subdirs = [];
|
|
4533
5327
|
for (const entry of entries) {
|
|
4534
|
-
const fullPath =
|
|
5328
|
+
const fullPath = join17(current, entry);
|
|
4535
5329
|
let stat;
|
|
4536
5330
|
try {
|
|
4537
5331
|
stat = statSync(fullPath);
|
|
@@ -4572,14 +5366,14 @@ function getNewestSourceMtime(dir) {
|
|
|
4572
5366
|
function walk(current) {
|
|
4573
5367
|
let entries;
|
|
4574
5368
|
try {
|
|
4575
|
-
entries =
|
|
5369
|
+
entries = readdirSync4(current);
|
|
4576
5370
|
} catch {
|
|
4577
5371
|
return;
|
|
4578
5372
|
}
|
|
4579
5373
|
const dirName = current.split("/").pop() ?? "";
|
|
4580
5374
|
if (dirName === "node_modules" || dirName === ".git") return;
|
|
4581
5375
|
for (const entry of entries) {
|
|
4582
|
-
const fullPath =
|
|
5376
|
+
const fullPath = join17(current, entry);
|
|
4583
5377
|
let stat;
|
|
4584
5378
|
try {
|
|
4585
5379
|
stat = statSync(fullPath);
|
|
@@ -4608,14 +5402,14 @@ function getSourceFilesInModule(modulePath) {
|
|
|
4608
5402
|
function walk(current) {
|
|
4609
5403
|
let entries;
|
|
4610
5404
|
try {
|
|
4611
|
-
entries =
|
|
5405
|
+
entries = readdirSync4(current);
|
|
4612
5406
|
} catch {
|
|
4613
5407
|
return;
|
|
4614
5408
|
}
|
|
4615
5409
|
const dirName = current.split("/").pop() ?? "";
|
|
4616
5410
|
if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
|
|
4617
5411
|
for (const entry of entries) {
|
|
4618
|
-
const fullPath =
|
|
5412
|
+
const fullPath = join17(current, entry);
|
|
4619
5413
|
let stat;
|
|
4620
5414
|
try {
|
|
4621
5415
|
stat = statSync(fullPath);
|
|
@@ -4636,8 +5430,8 @@ function getSourceFilesInModule(modulePath) {
|
|
|
4636
5430
|
return files;
|
|
4637
5431
|
}
|
|
4638
5432
|
function getMentionedFilesInAgentsMd(agentsPath) {
|
|
4639
|
-
if (!
|
|
4640
|
-
const content =
|
|
5433
|
+
if (!existsSync20(agentsPath)) return [];
|
|
5434
|
+
const content = readFileSync17(agentsPath, "utf-8");
|
|
4641
5435
|
const mentioned = /* @__PURE__ */ new Set();
|
|
4642
5436
|
const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
|
|
4643
5437
|
let match;
|
|
@@ -4661,12 +5455,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
|
|
|
4661
5455
|
}
|
|
4662
5456
|
function checkAgentsMdForModule(modulePath, dir) {
|
|
4663
5457
|
const root = dir ?? process.cwd();
|
|
4664
|
-
const fullModulePath =
|
|
4665
|
-
let agentsPath =
|
|
4666
|
-
if (!
|
|
4667
|
-
agentsPath =
|
|
5458
|
+
const fullModulePath = join17(root, modulePath);
|
|
5459
|
+
let agentsPath = join17(fullModulePath, "AGENTS.md");
|
|
5460
|
+
if (!existsSync20(agentsPath)) {
|
|
5461
|
+
agentsPath = join17(root, "AGENTS.md");
|
|
4668
5462
|
}
|
|
4669
|
-
if (!
|
|
5463
|
+
if (!existsSync20(agentsPath)) {
|
|
4670
5464
|
return {
|
|
4671
5465
|
path: relative(root, agentsPath),
|
|
4672
5466
|
grade: "missing",
|
|
@@ -4697,9 +5491,9 @@ function checkAgentsMdForModule(modulePath, dir) {
|
|
|
4697
5491
|
};
|
|
4698
5492
|
}
|
|
4699
5493
|
function checkDoNotEditHeaders(docPath) {
|
|
4700
|
-
if (!
|
|
5494
|
+
if (!existsSync20(docPath)) return false;
|
|
4701
5495
|
try {
|
|
4702
|
-
const content =
|
|
5496
|
+
const content = readFileSync17(docPath, "utf-8");
|
|
4703
5497
|
if (content.length === 0) return false;
|
|
4704
5498
|
return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
|
|
4705
5499
|
} catch {
|
|
@@ -4711,17 +5505,17 @@ function scanDocHealth(dir) {
|
|
|
4711
5505
|
const root = dir ?? process.cwd();
|
|
4712
5506
|
const documents = [];
|
|
4713
5507
|
const modules = findModules(root);
|
|
4714
|
-
const rootAgentsPath =
|
|
4715
|
-
if (
|
|
5508
|
+
const rootAgentsPath = join17(root, "AGENTS.md");
|
|
5509
|
+
if (existsSync20(rootAgentsPath)) {
|
|
4716
5510
|
if (modules.length > 0) {
|
|
4717
5511
|
const docMtime = statSync(rootAgentsPath).mtime;
|
|
4718
5512
|
let allMissing = [];
|
|
4719
5513
|
let staleModule = "";
|
|
4720
5514
|
let newestCode = null;
|
|
4721
5515
|
for (const mod of modules) {
|
|
4722
|
-
const fullModPath =
|
|
4723
|
-
const modAgentsPath =
|
|
4724
|
-
if (
|
|
5516
|
+
const fullModPath = join17(root, mod);
|
|
5517
|
+
const modAgentsPath = join17(fullModPath, "AGENTS.md");
|
|
5518
|
+
if (existsSync20(modAgentsPath)) continue;
|
|
4725
5519
|
const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
|
|
4726
5520
|
if (missing.length > 0 && staleModule === "") {
|
|
4727
5521
|
staleModule = mod;
|
|
@@ -4769,8 +5563,8 @@ function scanDocHealth(dir) {
|
|
|
4769
5563
|
});
|
|
4770
5564
|
}
|
|
4771
5565
|
for (const mod of modules) {
|
|
4772
|
-
const modAgentsPath =
|
|
4773
|
-
if (
|
|
5566
|
+
const modAgentsPath = join17(root, mod, "AGENTS.md");
|
|
5567
|
+
if (existsSync20(modAgentsPath)) {
|
|
4774
5568
|
const result = checkAgentsMdForModule(mod, root);
|
|
4775
5569
|
if (result.path !== "AGENTS.md") {
|
|
4776
5570
|
documents.push(result);
|
|
@@ -4778,9 +5572,9 @@ function scanDocHealth(dir) {
|
|
|
4778
5572
|
}
|
|
4779
5573
|
}
|
|
4780
5574
|
}
|
|
4781
|
-
const indexPath =
|
|
4782
|
-
if (
|
|
4783
|
-
const content =
|
|
5575
|
+
const indexPath = join17(root, "docs", "index.md");
|
|
5576
|
+
if (existsSync20(indexPath)) {
|
|
5577
|
+
const content = readFileSync17(indexPath, "utf-8");
|
|
4784
5578
|
const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
|
|
4785
5579
|
documents.push({
|
|
4786
5580
|
path: "docs/index.md",
|
|
@@ -4790,11 +5584,11 @@ function scanDocHealth(dir) {
|
|
|
4790
5584
|
reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
|
|
4791
5585
|
});
|
|
4792
5586
|
}
|
|
4793
|
-
const activeDir =
|
|
4794
|
-
if (
|
|
4795
|
-
const files =
|
|
5587
|
+
const activeDir = join17(root, "docs", "exec-plans", "active");
|
|
5588
|
+
if (existsSync20(activeDir)) {
|
|
5589
|
+
const files = readdirSync4(activeDir).filter((f) => f.endsWith(".md"));
|
|
4796
5590
|
for (const file of files) {
|
|
4797
|
-
const filePath =
|
|
5591
|
+
const filePath = join17(activeDir, file);
|
|
4798
5592
|
documents.push({
|
|
4799
5593
|
path: `docs/exec-plans/active/${file}`,
|
|
4800
5594
|
grade: "fresh",
|
|
@@ -4805,11 +5599,11 @@ function scanDocHealth(dir) {
|
|
|
4805
5599
|
}
|
|
4806
5600
|
}
|
|
4807
5601
|
for (const subdir of ["quality", "generated"]) {
|
|
4808
|
-
const dirPath =
|
|
4809
|
-
if (!
|
|
4810
|
-
const files =
|
|
5602
|
+
const dirPath = join17(root, "docs", subdir);
|
|
5603
|
+
if (!existsSync20(dirPath)) continue;
|
|
5604
|
+
const files = readdirSync4(dirPath).filter((f) => !f.startsWith("."));
|
|
4811
5605
|
for (const file of files) {
|
|
4812
|
-
const filePath =
|
|
5606
|
+
const filePath = join17(dirPath, file);
|
|
4813
5607
|
let stat;
|
|
4814
5608
|
try {
|
|
4815
5609
|
stat = statSync(filePath);
|
|
@@ -4842,7 +5636,7 @@ function scanDocHealth(dir) {
|
|
|
4842
5636
|
}
|
|
4843
5637
|
function checkAgentsMdLineCount(filePath, docPath, documents) {
|
|
4844
5638
|
try {
|
|
4845
|
-
const content =
|
|
5639
|
+
const content = readFileSync17(filePath, "utf-8");
|
|
4846
5640
|
const lineCount = content.split("\n").length;
|
|
4847
5641
|
if (lineCount > 100) {
|
|
4848
5642
|
documents.push({
|
|
@@ -4880,15 +5674,15 @@ function checkStoryDocFreshness(storyId, dir) {
|
|
|
4880
5674
|
for (const mod of modulesToCheck) {
|
|
4881
5675
|
const result = checkAgentsMdForModule(mod, root);
|
|
4882
5676
|
documents.push(result);
|
|
4883
|
-
const moduleAgentsPath =
|
|
4884
|
-
const actualAgentsPath =
|
|
4885
|
-
if (
|
|
5677
|
+
const moduleAgentsPath = join17(root, mod, "AGENTS.md");
|
|
5678
|
+
const actualAgentsPath = existsSync20(moduleAgentsPath) ? moduleAgentsPath : join17(root, "AGENTS.md");
|
|
5679
|
+
if (existsSync20(actualAgentsPath)) {
|
|
4886
5680
|
checkAgentsMdLineCount(actualAgentsPath, result.path, documents);
|
|
4887
5681
|
}
|
|
4888
5682
|
}
|
|
4889
5683
|
if (modulesToCheck.length === 0) {
|
|
4890
|
-
const rootAgentsPath =
|
|
4891
|
-
if (
|
|
5684
|
+
const rootAgentsPath = join17(root, "AGENTS.md");
|
|
5685
|
+
if (existsSync20(rootAgentsPath)) {
|
|
4892
5686
|
documents.push({
|
|
4893
5687
|
path: "AGENTS.md",
|
|
4894
5688
|
grade: "fresh",
|
|
@@ -4926,11 +5720,11 @@ function getRecentlyChangedFiles(dir) {
|
|
|
4926
5720
|
}
|
|
4927
5721
|
function completeExecPlan(storyId, dir) {
|
|
4928
5722
|
const root = dir ?? process.cwd();
|
|
4929
|
-
const activePath =
|
|
4930
|
-
if (!
|
|
5723
|
+
const activePath = join17(root, "docs", "exec-plans", "active", `${storyId}.md`);
|
|
5724
|
+
if (!existsSync20(activePath)) {
|
|
4931
5725
|
return null;
|
|
4932
5726
|
}
|
|
4933
|
-
let content =
|
|
5727
|
+
let content = readFileSync17(activePath, "utf-8");
|
|
4934
5728
|
content = content.replace(/^Status:\s*active$/m, "Status: completed");
|
|
4935
5729
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4936
5730
|
content = content.replace(
|
|
@@ -4938,10 +5732,10 @@ function completeExecPlan(storyId, dir) {
|
|
|
4938
5732
|
`$1
|
|
4939
5733
|
Completed: ${timestamp}`
|
|
4940
5734
|
);
|
|
4941
|
-
const completedDir =
|
|
5735
|
+
const completedDir = join17(root, "docs", "exec-plans", "completed");
|
|
4942
5736
|
mkdirSync5(completedDir, { recursive: true });
|
|
4943
|
-
const completedPath =
|
|
4944
|
-
|
|
5737
|
+
const completedPath = join17(completedDir, `${storyId}.md`);
|
|
5738
|
+
writeFileSync11(completedPath, content, "utf-8");
|
|
4945
5739
|
try {
|
|
4946
5740
|
unlinkSync(activePath);
|
|
4947
5741
|
} catch {
|
|
@@ -5066,8 +5860,8 @@ function checkPreconditions(dir, storyId) {
|
|
|
5066
5860
|
}
|
|
5067
5861
|
function createProofDocument(storyId, storyTitle, acs, dir) {
|
|
5068
5862
|
const root = dir ?? process.cwd();
|
|
5069
|
-
const verificationDir =
|
|
5070
|
-
const screenshotsDir =
|
|
5863
|
+
const verificationDir = join18(root, "verification");
|
|
5864
|
+
const screenshotsDir = join18(verificationDir, "screenshots");
|
|
5071
5865
|
mkdirSync6(verificationDir, { recursive: true });
|
|
5072
5866
|
mkdirSync6(screenshotsDir, { recursive: true });
|
|
5073
5867
|
const criteria = acs.map((ac) => ({
|
|
@@ -5081,8 +5875,8 @@ function createProofDocument(storyId, storyTitle, acs, dir) {
|
|
|
5081
5875
|
storyTitle,
|
|
5082
5876
|
acceptanceCriteria: criteria
|
|
5083
5877
|
});
|
|
5084
|
-
const proofPath =
|
|
5085
|
-
|
|
5878
|
+
const proofPath = join18(verificationDir, `${storyId}-proof.md`);
|
|
5879
|
+
writeFileSync12(proofPath, content, "utf-8");
|
|
5086
5880
|
return proofPath;
|
|
5087
5881
|
}
|
|
5088
5882
|
function runShowboatVerify(proofPath) {
|
|
@@ -5134,7 +5928,7 @@ function closeBeadsIssue(storyId, dir) {
|
|
|
5134
5928
|
}
|
|
5135
5929
|
|
|
5136
5930
|
// src/modules/verify/parser.ts
|
|
5137
|
-
import { existsSync as
|
|
5931
|
+
import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
|
|
5138
5932
|
var UI_KEYWORDS = [
|
|
5139
5933
|
"agent-browser",
|
|
5140
5934
|
"screenshot",
|
|
@@ -5204,12 +5998,12 @@ function classifyAC(description) {
|
|
|
5204
5998
|
return "general";
|
|
5205
5999
|
}
|
|
5206
6000
|
function parseStoryACs(storyFilePath) {
|
|
5207
|
-
if (!
|
|
6001
|
+
if (!existsSync22(storyFilePath)) {
|
|
5208
6002
|
throw new Error(
|
|
5209
6003
|
`Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
|
|
5210
6004
|
);
|
|
5211
6005
|
}
|
|
5212
|
-
const content =
|
|
6006
|
+
const content = readFileSync18(storyFilePath, "utf-8");
|
|
5213
6007
|
const lines = content.split("\n");
|
|
5214
6008
|
let acSectionStart = -1;
|
|
5215
6009
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -5301,7 +6095,7 @@ function parseObservabilityGaps(proofContent) {
|
|
|
5301
6095
|
|
|
5302
6096
|
// src/modules/observability/analyzer.ts
|
|
5303
6097
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5304
|
-
import { join as
|
|
6098
|
+
import { join as join19 } from "path";
|
|
5305
6099
|
var DEFAULT_RULES_DIR = "patches/observability/";
|
|
5306
6100
|
var DEFAULT_TIMEOUT = 6e4;
|
|
5307
6101
|
var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
|
|
@@ -5335,7 +6129,7 @@ function analyze(projectDir, config) {
|
|
|
5335
6129
|
}
|
|
5336
6130
|
const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
|
|
5337
6131
|
const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
|
|
5338
|
-
const fullRulesDir =
|
|
6132
|
+
const fullRulesDir = join19(projectDir, rulesDir);
|
|
5339
6133
|
const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
|
|
5340
6134
|
if (!rawResult.success) {
|
|
5341
6135
|
return fail2(rawResult.error);
|
|
@@ -5419,8 +6213,8 @@ function normalizeSeverity(severity) {
|
|
|
5419
6213
|
}
|
|
5420
6214
|
|
|
5421
6215
|
// src/modules/observability/coverage.ts
|
|
5422
|
-
import { readFileSync as
|
|
5423
|
-
import { join as
|
|
6216
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync2, existsSync as existsSync23 } from "fs";
|
|
6217
|
+
import { join as join20 } from "path";
|
|
5424
6218
|
var STATE_FILE2 = "sprint-state.json";
|
|
5425
6219
|
var DEFAULT_STATIC_TARGET = 80;
|
|
5426
6220
|
function defaultCoverageState() {
|
|
@@ -5436,12 +6230,12 @@ function defaultCoverageState() {
|
|
|
5436
6230
|
};
|
|
5437
6231
|
}
|
|
5438
6232
|
function readStateFile(projectDir) {
|
|
5439
|
-
const fp =
|
|
5440
|
-
if (!
|
|
6233
|
+
const fp = join20(projectDir, STATE_FILE2);
|
|
6234
|
+
if (!existsSync23(fp)) {
|
|
5441
6235
|
return ok2({});
|
|
5442
6236
|
}
|
|
5443
6237
|
try {
|
|
5444
|
-
const raw =
|
|
6238
|
+
const raw = readFileSync19(fp, "utf-8");
|
|
5445
6239
|
const parsed = JSON.parse(raw);
|
|
5446
6240
|
return ok2(parsed);
|
|
5447
6241
|
} catch (err) {
|
|
@@ -5508,8 +6302,8 @@ function parseGapArray(raw) {
|
|
|
5508
6302
|
}
|
|
5509
6303
|
|
|
5510
6304
|
// src/modules/observability/runtime-coverage.ts
|
|
5511
|
-
import { readFileSync as
|
|
5512
|
-
import { join as
|
|
6305
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, renameSync as renameSync3, existsSync as existsSync24 } from "fs";
|
|
6306
|
+
import { join as join21 } from "path";
|
|
5513
6307
|
|
|
5514
6308
|
// src/modules/observability/coverage-gate.ts
|
|
5515
6309
|
var DEFAULT_STATIC_TARGET2 = 80;
|
|
@@ -5551,8 +6345,8 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
|
|
|
5551
6345
|
|
|
5552
6346
|
// src/modules/observability/runtime-validator.ts
|
|
5553
6347
|
import { execSync as execSync3 } from "child_process";
|
|
5554
|
-
import { readdirSync as
|
|
5555
|
-
import { join as
|
|
6348
|
+
import { readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
6349
|
+
import { join as join22 } from "path";
|
|
5556
6350
|
var DEFAULT_CONFIG = {
|
|
5557
6351
|
testCommand: "npm test",
|
|
5558
6352
|
otlpEndpoint: "http://localhost:4318",
|
|
@@ -5679,11 +6473,11 @@ function mapEventsToModules(events, projectDir, modules) {
|
|
|
5679
6473
|
});
|
|
5680
6474
|
}
|
|
5681
6475
|
function discoverModules(projectDir) {
|
|
5682
|
-
const srcDir =
|
|
6476
|
+
const srcDir = join22(projectDir, "src");
|
|
5683
6477
|
try {
|
|
5684
|
-
return
|
|
6478
|
+
return readdirSync5(srcDir).filter((name) => {
|
|
5685
6479
|
try {
|
|
5686
|
-
return statSync2(
|
|
6480
|
+
return statSync2(join22(srcDir, name)).isDirectory();
|
|
5687
6481
|
} catch {
|
|
5688
6482
|
return false;
|
|
5689
6483
|
}
|
|
@@ -5712,7 +6506,7 @@ function parseLogEvents(text) {
|
|
|
5712
6506
|
|
|
5713
6507
|
// src/modules/verify/browser.ts
|
|
5714
6508
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5715
|
-
import { existsSync as
|
|
6509
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
|
|
5716
6510
|
|
|
5717
6511
|
// src/modules/verify/validation-ac-fr.ts
|
|
5718
6512
|
var FR_ACS = [
|
|
@@ -6350,8 +7144,8 @@ function getACById(id) {
|
|
|
6350
7144
|
|
|
6351
7145
|
// src/modules/verify/validation-runner.ts
|
|
6352
7146
|
import { execSync as execSync4 } from "child_process";
|
|
6353
|
-
import { writeFileSync as
|
|
6354
|
-
import { join as
|
|
7147
|
+
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync7 } from "fs";
|
|
7148
|
+
import { join as join23, dirname as dirname5 } from "path";
|
|
6355
7149
|
var MAX_VALIDATION_ATTEMPTS = 10;
|
|
6356
7150
|
var AC_COMMAND_TIMEOUT_MS = 3e4;
|
|
6357
7151
|
var VAL_KEY_PREFIX = "val-";
|
|
@@ -6460,7 +7254,7 @@ function executeValidationAC(ac) {
|
|
|
6460
7254
|
function createFixStory(ac, error) {
|
|
6461
7255
|
try {
|
|
6462
7256
|
const storyKey = `val-fix-${ac.id}`;
|
|
6463
|
-
const storyPath =
|
|
7257
|
+
const storyPath = join23(
|
|
6464
7258
|
process.cwd(),
|
|
6465
7259
|
"_bmad-output",
|
|
6466
7260
|
"implementation-artifacts",
|
|
@@ -6504,7 +7298,7 @@ function createFixStory(ac, error) {
|
|
|
6504
7298
|
""
|
|
6505
7299
|
].join("\n");
|
|
6506
7300
|
mkdirSync7(dirname5(storyPath), { recursive: true });
|
|
6507
|
-
|
|
7301
|
+
writeFileSync15(storyPath, markdown, "utf-8");
|
|
6508
7302
|
return ok2(storyKey);
|
|
6509
7303
|
} catch (err) {
|
|
6510
7304
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6830,8 +7624,8 @@ function runValidationCycle() {
|
|
|
6830
7624
|
|
|
6831
7625
|
// src/modules/verify/env.ts
|
|
6832
7626
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
6833
|
-
import { existsSync as
|
|
6834
|
-
import { join as
|
|
7627
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync22, cpSync, rmSync, statSync as statSync3 } from "fs";
|
|
7628
|
+
import { join as join24, basename as basename4 } from "path";
|
|
6835
7629
|
import { createHash } from "crypto";
|
|
6836
7630
|
var IMAGE_TAG = "codeharness-verify";
|
|
6837
7631
|
var STORY_DIR = "_bmad-output/implementation-artifacts";
|
|
@@ -6844,20 +7638,20 @@ function isValidStoryKey(storyKey) {
|
|
|
6844
7638
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
6845
7639
|
}
|
|
6846
7640
|
function computeDistHash(projectDir) {
|
|
6847
|
-
const distDir =
|
|
6848
|
-
if (!
|
|
7641
|
+
const distDir = join24(projectDir, "dist");
|
|
7642
|
+
if (!existsSync26(distDir)) return null;
|
|
6849
7643
|
const hash = createHash("sha256");
|
|
6850
7644
|
const files = collectFiles(distDir).sort();
|
|
6851
7645
|
for (const file of files) {
|
|
6852
7646
|
hash.update(file.slice(distDir.length));
|
|
6853
|
-
hash.update(
|
|
7647
|
+
hash.update(readFileSync22(file));
|
|
6854
7648
|
}
|
|
6855
7649
|
return hash.digest("hex");
|
|
6856
7650
|
}
|
|
6857
7651
|
function collectFiles(dir) {
|
|
6858
7652
|
const results = [];
|
|
6859
|
-
for (const entry of
|
|
6860
|
-
const fullPath =
|
|
7653
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
7654
|
+
const fullPath = join24(dir, entry.name);
|
|
6861
7655
|
if (entry.isDirectory()) {
|
|
6862
7656
|
results.push(...collectFiles(fullPath));
|
|
6863
7657
|
} else {
|
|
@@ -6884,10 +7678,13 @@ function storeDistHash(projectDir, hash) {
|
|
|
6884
7678
|
}
|
|
6885
7679
|
}
|
|
6886
7680
|
function detectProjectType(projectDir) {
|
|
6887
|
-
const
|
|
7681
|
+
const allStacks = detectStacks(projectDir);
|
|
7682
|
+
const rootDetection = allStacks.find((s) => s.dir === ".");
|
|
7683
|
+
const stack = rootDetection ? rootDetection.stack : null;
|
|
6888
7684
|
if (stack === "nodejs") return "nodejs";
|
|
6889
7685
|
if (stack === "python") return "python";
|
|
6890
|
-
if (
|
|
7686
|
+
if (stack === "rust") return "rust";
|
|
7687
|
+
if (existsSync26(join24(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
6891
7688
|
return "generic";
|
|
6892
7689
|
}
|
|
6893
7690
|
function buildVerifyImage(options = {}) {
|
|
@@ -6897,7 +7694,7 @@ function buildVerifyImage(options = {}) {
|
|
|
6897
7694
|
}
|
|
6898
7695
|
const projectType = detectProjectType(projectDir);
|
|
6899
7696
|
const currentHash = computeDistHash(projectDir);
|
|
6900
|
-
if (projectType === "generic" || projectType === "plugin") {
|
|
7697
|
+
if (projectType === "generic" || projectType === "plugin" || projectType === "rust") {
|
|
6901
7698
|
} else if (!currentHash) {
|
|
6902
7699
|
throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
|
|
6903
7700
|
}
|
|
@@ -6912,10 +7709,12 @@ function buildVerifyImage(options = {}) {
|
|
|
6912
7709
|
buildNodeImage(projectDir);
|
|
6913
7710
|
} else if (projectType === "python") {
|
|
6914
7711
|
buildPythonImage(projectDir);
|
|
7712
|
+
} else if (projectType === "rust") {
|
|
7713
|
+
buildSimpleImage(projectDir, "rust", 3e5);
|
|
6915
7714
|
} else if (projectType === "plugin") {
|
|
6916
7715
|
buildPluginImage(projectDir);
|
|
6917
7716
|
} else {
|
|
6918
|
-
|
|
7717
|
+
buildSimpleImage(projectDir, "generic");
|
|
6919
7718
|
}
|
|
6920
7719
|
if (currentHash) {
|
|
6921
7720
|
storeDistHash(projectDir, currentHash);
|
|
@@ -6931,12 +7730,12 @@ function buildNodeImage(projectDir) {
|
|
|
6931
7730
|
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
6932
7731
|
if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
6933
7732
|
const tarballName = basename4(lastLine);
|
|
6934
|
-
const tarballPath =
|
|
6935
|
-
const buildContext =
|
|
7733
|
+
const tarballPath = join24("/tmp", tarballName);
|
|
7734
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6936
7735
|
mkdirSync8(buildContext, { recursive: true });
|
|
6937
7736
|
try {
|
|
6938
|
-
cpSync(tarballPath,
|
|
6939
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
7737
|
+
cpSync(tarballPath, join24(buildContext, tarballName));
|
|
7738
|
+
cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
|
|
6940
7739
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
|
|
6941
7740
|
cwd: buildContext,
|
|
6942
7741
|
stdio: "pipe",
|
|
@@ -6948,17 +7747,17 @@ function buildNodeImage(projectDir) {
|
|
|
6948
7747
|
}
|
|
6949
7748
|
}
|
|
6950
7749
|
function buildPythonImage(projectDir) {
|
|
6951
|
-
const distDir =
|
|
6952
|
-
const distFiles =
|
|
7750
|
+
const distDir = join24(projectDir, "dist");
|
|
7751
|
+
const distFiles = readdirSync6(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
|
|
6953
7752
|
if (distFiles.length === 0) {
|
|
6954
7753
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
6955
7754
|
}
|
|
6956
7755
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
6957
|
-
const buildContext =
|
|
7756
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6958
7757
|
mkdirSync8(buildContext, { recursive: true });
|
|
6959
7758
|
try {
|
|
6960
|
-
cpSync(
|
|
6961
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
7759
|
+
cpSync(join24(distDir, distFile), join24(buildContext, distFile));
|
|
7760
|
+
cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
|
|
6962
7761
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
|
|
6963
7762
|
cwd: buildContext,
|
|
6964
7763
|
stdio: "pipe",
|
|
@@ -6973,19 +7772,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
6973
7772
|
if (!isValidStoryKey(storyKey)) {
|
|
6974
7773
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
6975
7774
|
}
|
|
6976
|
-
const storyFile =
|
|
6977
|
-
if (!
|
|
7775
|
+
const storyFile = join24(root, STORY_DIR, `${storyKey}.md`);
|
|
7776
|
+
if (!existsSync26(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
|
|
6978
7777
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
6979
|
-
if (
|
|
7778
|
+
if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
6980
7779
|
mkdirSync8(workspace, { recursive: true });
|
|
6981
|
-
cpSync(storyFile,
|
|
6982
|
-
const readmePath =
|
|
6983
|
-
if (
|
|
6984
|
-
const docsDir =
|
|
6985
|
-
if (
|
|
6986
|
-
cpSync(docsDir,
|
|
6987
|
-
}
|
|
6988
|
-
mkdirSync8(
|
|
7780
|
+
cpSync(storyFile, join24(workspace, "story.md"));
|
|
7781
|
+
const readmePath = join24(root, "README.md");
|
|
7782
|
+
if (existsSync26(readmePath)) cpSync(readmePath, join24(workspace, "README.md"));
|
|
7783
|
+
const docsDir = join24(root, "docs");
|
|
7784
|
+
if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
7785
|
+
cpSync(docsDir, join24(workspace, "docs"), { recursive: true });
|
|
7786
|
+
}
|
|
7787
|
+
mkdirSync8(join24(workspace, "verification"), { recursive: true });
|
|
6989
7788
|
return workspace;
|
|
6990
7789
|
}
|
|
6991
7790
|
function checkVerifyEnv() {
|
|
@@ -7027,7 +7826,7 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7027
7826
|
}
|
|
7028
7827
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7029
7828
|
const containerName = `codeharness-verify-${storyKey}`;
|
|
7030
|
-
if (
|
|
7829
|
+
if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
7031
7830
|
try {
|
|
7032
7831
|
execFileSync11("docker", ["stop", containerName], { stdio: "pipe", timeout: 15e3 });
|
|
7033
7832
|
} catch {
|
|
@@ -7038,18 +7837,18 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7038
7837
|
}
|
|
7039
7838
|
}
|
|
7040
7839
|
function buildPluginImage(projectDir) {
|
|
7041
|
-
const buildContext =
|
|
7840
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7042
7841
|
mkdirSync8(buildContext, { recursive: true });
|
|
7043
7842
|
try {
|
|
7044
|
-
const pluginDir =
|
|
7045
|
-
cpSync(pluginDir,
|
|
7843
|
+
const pluginDir = join24(projectDir, ".claude-plugin");
|
|
7844
|
+
cpSync(pluginDir, join24(buildContext, ".claude-plugin"), { recursive: true });
|
|
7046
7845
|
for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
|
|
7047
|
-
const src =
|
|
7048
|
-
if (
|
|
7049
|
-
cpSync(src,
|
|
7846
|
+
const src = join24(projectDir, dir);
|
|
7847
|
+
if (existsSync26(src) && statSync3(src).isDirectory()) {
|
|
7848
|
+
cpSync(src, join24(buildContext, dir), { recursive: true });
|
|
7050
7849
|
}
|
|
7051
7850
|
}
|
|
7052
|
-
cpSync(resolveDockerfileTemplate(projectDir, "generic"),
|
|
7851
|
+
cpSync(resolveDockerfileTemplate(projectDir, "generic"), join24(buildContext, "Dockerfile"));
|
|
7053
7852
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
7054
7853
|
cwd: buildContext,
|
|
7055
7854
|
stdio: "pipe",
|
|
@@ -7059,27 +7858,31 @@ function buildPluginImage(projectDir) {
|
|
|
7059
7858
|
rmSync(buildContext, { recursive: true, force: true });
|
|
7060
7859
|
}
|
|
7061
7860
|
}
|
|
7062
|
-
function
|
|
7063
|
-
const buildContext =
|
|
7861
|
+
function buildSimpleImage(projectDir, variant, timeout = 12e4) {
|
|
7862
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7064
7863
|
mkdirSync8(buildContext, { recursive: true });
|
|
7065
7864
|
try {
|
|
7066
|
-
cpSync(resolveDockerfileTemplate(projectDir,
|
|
7865
|
+
cpSync(resolveDockerfileTemplate(projectDir, variant), join24(buildContext, "Dockerfile"));
|
|
7067
7866
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
7068
7867
|
cwd: buildContext,
|
|
7069
7868
|
stdio: "pipe",
|
|
7070
|
-
timeout
|
|
7869
|
+
timeout
|
|
7071
7870
|
});
|
|
7072
7871
|
} finally {
|
|
7073
7872
|
rmSync(buildContext, { recursive: true, force: true });
|
|
7074
7873
|
}
|
|
7075
7874
|
}
|
|
7875
|
+
var DOCKERFILE_VARIANTS = {
|
|
7876
|
+
generic: "Dockerfile.verify.generic",
|
|
7877
|
+
rust: "Dockerfile.verify.rust"
|
|
7878
|
+
};
|
|
7076
7879
|
function resolveDockerfileTemplate(projectDir, variant) {
|
|
7077
|
-
const filename = variant
|
|
7078
|
-
const local =
|
|
7079
|
-
if (
|
|
7880
|
+
const filename = (variant && DOCKERFILE_VARIANTS[variant]) ?? "Dockerfile.verify";
|
|
7881
|
+
const local = join24(projectDir, "templates", filename);
|
|
7882
|
+
if (existsSync26(local)) return local;
|
|
7080
7883
|
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
7081
|
-
const pkg =
|
|
7082
|
-
if (
|
|
7884
|
+
const pkg = join24(pkgDir, "templates", filename);
|
|
7885
|
+
if (existsSync26(pkg)) return pkg;
|
|
7083
7886
|
throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
|
|
7084
7887
|
}
|
|
7085
7888
|
function dockerImageExists(tag) {
|
|
@@ -7154,8 +7957,8 @@ function verifyRetro(opts, isJson, root) {
|
|
|
7154
7957
|
return;
|
|
7155
7958
|
}
|
|
7156
7959
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
7157
|
-
const retroPath =
|
|
7158
|
-
if (!
|
|
7960
|
+
const retroPath = join25(root, STORY_DIR2, retroFile);
|
|
7961
|
+
if (!existsSync27(retroPath)) {
|
|
7159
7962
|
if (isJson) {
|
|
7160
7963
|
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
7161
7964
|
} else {
|
|
@@ -7172,7 +7975,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
7172
7975
|
warn(`Failed to update sprint status: ${message}`);
|
|
7173
7976
|
}
|
|
7174
7977
|
if (isJson) {
|
|
7175
|
-
jsonOutput({ status: "ok", epic: epicNum, retroFile:
|
|
7978
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join25(STORY_DIR2, retroFile) });
|
|
7176
7979
|
} else {
|
|
7177
7980
|
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
7178
7981
|
}
|
|
@@ -7183,8 +7986,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7183
7986
|
process.exitCode = 1;
|
|
7184
7987
|
return;
|
|
7185
7988
|
}
|
|
7186
|
-
const readmePath =
|
|
7187
|
-
if (!
|
|
7989
|
+
const readmePath = join25(root, "README.md");
|
|
7990
|
+
if (!existsSync27(readmePath)) {
|
|
7188
7991
|
if (isJson) {
|
|
7189
7992
|
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
7190
7993
|
} else {
|
|
@@ -7193,8 +7996,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7193
7996
|
process.exitCode = 1;
|
|
7194
7997
|
return;
|
|
7195
7998
|
}
|
|
7196
|
-
const storyFilePath =
|
|
7197
|
-
if (!
|
|
7999
|
+
const storyFilePath = join25(root, STORY_DIR2, `${storyId}.md`);
|
|
8000
|
+
if (!existsSync27(storyFilePath)) {
|
|
7198
8001
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
7199
8002
|
process.exitCode = 1;
|
|
7200
8003
|
return;
|
|
@@ -7234,8 +8037,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7234
8037
|
return;
|
|
7235
8038
|
}
|
|
7236
8039
|
const storyTitle = extractStoryTitle(storyFilePath);
|
|
7237
|
-
const expectedProofPath =
|
|
7238
|
-
const proofPath =
|
|
8040
|
+
const expectedProofPath = join25(root, "verification", `${storyId}-proof.md`);
|
|
8041
|
+
const proofPath = existsSync27(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
|
|
7239
8042
|
const proofQuality = validateProofQuality(proofPath);
|
|
7240
8043
|
if (!proofQuality.passed) {
|
|
7241
8044
|
if (isJson) {
|
|
@@ -7278,7 +8081,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7278
8081
|
let observabilityGapCount = 0;
|
|
7279
8082
|
let runtimeCoveragePercent = 0;
|
|
7280
8083
|
try {
|
|
7281
|
-
const proofContent =
|
|
8084
|
+
const proofContent = readFileSync24(proofPath, "utf-8");
|
|
7282
8085
|
const gapResult = parseObservabilityGaps(proofContent);
|
|
7283
8086
|
observabilityGapCount = gapResult.gapCount;
|
|
7284
8087
|
runtimeCoveragePercent = gapResult.totalACs === 0 ? 0 : gapResult.coveredCount / gapResult.totalACs * 100;
|
|
@@ -7340,7 +8143,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7340
8143
|
}
|
|
7341
8144
|
function extractStoryTitle(filePath) {
|
|
7342
8145
|
try {
|
|
7343
|
-
const content =
|
|
8146
|
+
const content = readFileSync24(filePath, "utf-8");
|
|
7344
8147
|
const match = /^#\s+(.+)$/m.exec(content);
|
|
7345
8148
|
return match ? match[1] : "Unknown Story";
|
|
7346
8149
|
} catch {
|
|
@@ -7349,14 +8152,14 @@ function extractStoryTitle(filePath) {
|
|
|
7349
8152
|
}
|
|
7350
8153
|
|
|
7351
8154
|
// src/lib/onboard-checks.ts
|
|
7352
|
-
import { existsSync as
|
|
7353
|
-
import { join as
|
|
8155
|
+
import { existsSync as existsSync29 } from "fs";
|
|
8156
|
+
import { join as join27, dirname as dirname6 } from "path";
|
|
7354
8157
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7355
8158
|
|
|
7356
8159
|
// src/lib/coverage.ts
|
|
7357
8160
|
import { execSync as execSync6 } from "child_process";
|
|
7358
|
-
import { existsSync as
|
|
7359
|
-
import { join as
|
|
8161
|
+
import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
|
|
8162
|
+
import { join as join26 } from "path";
|
|
7360
8163
|
function detectCoverageTool(dir) {
|
|
7361
8164
|
const baseDir = dir ?? process.cwd();
|
|
7362
8165
|
const stateHint = getStateToolHint(baseDir);
|
|
@@ -7367,6 +8170,27 @@ function detectCoverageTool(dir) {
|
|
|
7367
8170
|
if (stack === "python") {
|
|
7368
8171
|
return detectPythonCoverageTool(baseDir);
|
|
7369
8172
|
}
|
|
8173
|
+
if (stack === "rust") {
|
|
8174
|
+
try {
|
|
8175
|
+
execSync6("cargo tarpaulin --version", { stdio: "pipe", timeout: 1e4 });
|
|
8176
|
+
} catch {
|
|
8177
|
+
warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
|
|
8178
|
+
return { tool: "unknown", runCommand: "", reportFormat: "" };
|
|
8179
|
+
}
|
|
8180
|
+
const cargoPath = join26(baseDir, "Cargo.toml");
|
|
8181
|
+
let isWorkspace = false;
|
|
8182
|
+
try {
|
|
8183
|
+
const cargoContent = readFileSync25(cargoPath, "utf-8");
|
|
8184
|
+
isWorkspace = /^\[workspace\]/m.test(cargoContent);
|
|
8185
|
+
} catch {
|
|
8186
|
+
}
|
|
8187
|
+
const wsFlag = isWorkspace ? " --workspace" : "";
|
|
8188
|
+
return {
|
|
8189
|
+
tool: "cargo-tarpaulin",
|
|
8190
|
+
runCommand: `cargo tarpaulin --out json --output-dir coverage/${wsFlag}`,
|
|
8191
|
+
reportFormat: "tarpaulin-json"
|
|
8192
|
+
};
|
|
8193
|
+
}
|
|
7370
8194
|
warn("No recognized stack detected \u2014 cannot determine coverage tool");
|
|
7371
8195
|
return { tool: "unknown", runCommand: "", reportFormat: "" };
|
|
7372
8196
|
}
|
|
@@ -7379,16 +8203,16 @@ function getStateToolHint(dir) {
|
|
|
7379
8203
|
}
|
|
7380
8204
|
}
|
|
7381
8205
|
function detectNodeCoverageTool(dir, stateHint) {
|
|
7382
|
-
const hasVitestConfig =
|
|
7383
|
-
const pkgPath =
|
|
8206
|
+
const hasVitestConfig = existsSync28(join26(dir, "vitest.config.ts")) || existsSync28(join26(dir, "vitest.config.js"));
|
|
8207
|
+
const pkgPath = join26(dir, "package.json");
|
|
7384
8208
|
let hasVitestCoverageV8 = false;
|
|
7385
8209
|
let hasVitestCoverageIstanbul = false;
|
|
7386
8210
|
let hasC8 = false;
|
|
7387
8211
|
let hasJest = false;
|
|
7388
8212
|
let pkgScripts = {};
|
|
7389
|
-
if (
|
|
8213
|
+
if (existsSync28(pkgPath)) {
|
|
7390
8214
|
try {
|
|
7391
|
-
const pkg = JSON.parse(
|
|
8215
|
+
const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
|
|
7392
8216
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
7393
8217
|
hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
|
|
7394
8218
|
hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
|
|
@@ -7441,10 +8265,10 @@ function getNodeTestCommand(scripts, runner) {
|
|
|
7441
8265
|
return "npm test";
|
|
7442
8266
|
}
|
|
7443
8267
|
function detectPythonCoverageTool(dir) {
|
|
7444
|
-
const reqPath =
|
|
7445
|
-
if (
|
|
8268
|
+
const reqPath = join26(dir, "requirements.txt");
|
|
8269
|
+
if (existsSync28(reqPath)) {
|
|
7446
8270
|
try {
|
|
7447
|
-
const content =
|
|
8271
|
+
const content = readFileSync25(reqPath, "utf-8");
|
|
7448
8272
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
7449
8273
|
return {
|
|
7450
8274
|
tool: "coverage.py",
|
|
@@ -7455,10 +8279,10 @@ function detectPythonCoverageTool(dir) {
|
|
|
7455
8279
|
} catch {
|
|
7456
8280
|
}
|
|
7457
8281
|
}
|
|
7458
|
-
const pyprojectPath =
|
|
7459
|
-
if (
|
|
8282
|
+
const pyprojectPath = join26(dir, "pyproject.toml");
|
|
8283
|
+
if (existsSync28(pyprojectPath)) {
|
|
7460
8284
|
try {
|
|
7461
|
-
const content =
|
|
8285
|
+
const content = readFileSync25(pyprojectPath, "utf-8");
|
|
7462
8286
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
7463
8287
|
return {
|
|
7464
8288
|
tool: "coverage.py",
|
|
@@ -7531,6 +8355,9 @@ function parseCoverageReport(dir, format) {
|
|
|
7531
8355
|
if (format === "coverage-py-json") {
|
|
7532
8356
|
return parsePythonCoverage(dir);
|
|
7533
8357
|
}
|
|
8358
|
+
if (format === "tarpaulin-json") {
|
|
8359
|
+
return parseTarpaulinCoverage(dir);
|
|
8360
|
+
}
|
|
7534
8361
|
return 0;
|
|
7535
8362
|
}
|
|
7536
8363
|
function parseVitestCoverage(dir) {
|
|
@@ -7540,7 +8367,7 @@ function parseVitestCoverage(dir) {
|
|
|
7540
8367
|
return 0;
|
|
7541
8368
|
}
|
|
7542
8369
|
try {
|
|
7543
|
-
const report = JSON.parse(
|
|
8370
|
+
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
7544
8371
|
return report.total?.statements?.pct ?? 0;
|
|
7545
8372
|
} catch {
|
|
7546
8373
|
warn("Failed to parse coverage report");
|
|
@@ -7548,19 +8375,33 @@ function parseVitestCoverage(dir) {
|
|
|
7548
8375
|
}
|
|
7549
8376
|
}
|
|
7550
8377
|
function parsePythonCoverage(dir) {
|
|
7551
|
-
const reportPath =
|
|
7552
|
-
if (!
|
|
8378
|
+
const reportPath = join26(dir, "coverage.json");
|
|
8379
|
+
if (!existsSync28(reportPath)) {
|
|
7553
8380
|
warn("Coverage report not found at coverage.json");
|
|
7554
8381
|
return 0;
|
|
7555
8382
|
}
|
|
7556
8383
|
try {
|
|
7557
|
-
const report = JSON.parse(
|
|
8384
|
+
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
7558
8385
|
return report.totals?.percent_covered ?? 0;
|
|
7559
8386
|
} catch {
|
|
7560
8387
|
warn("Failed to parse coverage report");
|
|
7561
8388
|
return 0;
|
|
7562
8389
|
}
|
|
7563
8390
|
}
|
|
8391
|
+
function parseTarpaulinCoverage(dir) {
|
|
8392
|
+
const reportPath = join26(dir, "coverage", "tarpaulin-report.json");
|
|
8393
|
+
if (!existsSync28(reportPath)) {
|
|
8394
|
+
warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
|
|
8395
|
+
return 0;
|
|
8396
|
+
}
|
|
8397
|
+
try {
|
|
8398
|
+
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
8399
|
+
return report.coverage ?? 0;
|
|
8400
|
+
} catch {
|
|
8401
|
+
warn("Failed to parse tarpaulin coverage report");
|
|
8402
|
+
return 0;
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
7564
8405
|
function parseTestCounts(output) {
|
|
7565
8406
|
const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
|
|
7566
8407
|
if (vitestMatch) {
|
|
@@ -7576,6 +8417,18 @@ function parseTestCounts(output) {
|
|
|
7576
8417
|
failCount: jestMatch[1] ? parseInt(jestMatch[1], 10) : 0
|
|
7577
8418
|
};
|
|
7578
8419
|
}
|
|
8420
|
+
const cargoRegex = /test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed/gi;
|
|
8421
|
+
let cargoMatch = cargoRegex.exec(output);
|
|
8422
|
+
if (cargoMatch) {
|
|
8423
|
+
let totalPass = 0;
|
|
8424
|
+
let totalFail = 0;
|
|
8425
|
+
while (cargoMatch) {
|
|
8426
|
+
totalPass += parseInt(cargoMatch[1], 10);
|
|
8427
|
+
totalFail += parseInt(cargoMatch[2], 10);
|
|
8428
|
+
cargoMatch = cargoRegex.exec(output);
|
|
8429
|
+
}
|
|
8430
|
+
return { passCount: totalPass, failCount: totalFail };
|
|
8431
|
+
}
|
|
7579
8432
|
const pytestMatch = /(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/i.exec(output);
|
|
7580
8433
|
if (pytestMatch) {
|
|
7581
8434
|
return {
|
|
@@ -7655,7 +8508,7 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7655
8508
|
}
|
|
7656
8509
|
let report;
|
|
7657
8510
|
try {
|
|
7658
|
-
report = JSON.parse(
|
|
8511
|
+
report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
7659
8512
|
} catch {
|
|
7660
8513
|
warn("Failed to parse coverage-summary.json");
|
|
7661
8514
|
return { floor, violations: [], totalFiles: 0 };
|
|
@@ -7685,11 +8538,11 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7685
8538
|
}
|
|
7686
8539
|
function findCoverageSummary(dir) {
|
|
7687
8540
|
const candidates = [
|
|
7688
|
-
|
|
7689
|
-
|
|
8541
|
+
join26(dir, "coverage", "coverage-summary.json"),
|
|
8542
|
+
join26(dir, "src", "coverage", "coverage-summary.json")
|
|
7690
8543
|
];
|
|
7691
8544
|
for (const p of candidates) {
|
|
7692
|
-
if (
|
|
8545
|
+
if (existsSync28(p)) return p;
|
|
7693
8546
|
}
|
|
7694
8547
|
return null;
|
|
7695
8548
|
}
|
|
@@ -7714,7 +8567,7 @@ function printCoverageOutput(result, evaluation) {
|
|
|
7714
8567
|
// src/lib/onboard-checks.ts
|
|
7715
8568
|
function checkHarnessInitialized(dir) {
|
|
7716
8569
|
const statePath2 = getStatePath(dir ?? process.cwd());
|
|
7717
|
-
return { ok:
|
|
8570
|
+
return { ok: existsSync29(statePath2) };
|
|
7718
8571
|
}
|
|
7719
8572
|
function checkBmadInstalled(dir) {
|
|
7720
8573
|
return { ok: isBmadInstalled(dir) };
|
|
@@ -7722,8 +8575,8 @@ function checkBmadInstalled(dir) {
|
|
|
7722
8575
|
function checkHooksRegistered(dir) {
|
|
7723
8576
|
const __filename = fileURLToPath3(import.meta.url);
|
|
7724
8577
|
const __dirname2 = dirname6(__filename);
|
|
7725
|
-
const hooksPath =
|
|
7726
|
-
return { ok:
|
|
8578
|
+
const hooksPath = join27(__dirname2, "..", "..", "hooks", "hooks.json");
|
|
8579
|
+
return { ok: existsSync29(hooksPath) };
|
|
7727
8580
|
}
|
|
7728
8581
|
function runPreconditions(dir) {
|
|
7729
8582
|
const harnessCheck = checkHarnessInitialized(dir);
|
|
@@ -8361,8 +9214,8 @@ function getBeadsData() {
|
|
|
8361
9214
|
}
|
|
8362
9215
|
|
|
8363
9216
|
// src/modules/audit/dimensions.ts
|
|
8364
|
-
import { existsSync as
|
|
8365
|
-
import { join as
|
|
9217
|
+
import { existsSync as existsSync30, readdirSync as readdirSync7 } from "fs";
|
|
9218
|
+
import { join as join28 } from "path";
|
|
8366
9219
|
function gap(dimension, description, suggestedFix) {
|
|
8367
9220
|
return { dimension, description, suggestedFix };
|
|
8368
9221
|
}
|
|
@@ -8474,15 +9327,15 @@ function checkDocumentation(projectDir) {
|
|
|
8474
9327
|
function checkVerification(projectDir) {
|
|
8475
9328
|
try {
|
|
8476
9329
|
const gaps = [];
|
|
8477
|
-
const sprintPath =
|
|
8478
|
-
if (!
|
|
8479
|
-
const vDir =
|
|
9330
|
+
const sprintPath = join28(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
9331
|
+
if (!existsSync30(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
9332
|
+
const vDir = join28(projectDir, "verification");
|
|
8480
9333
|
let proofCount = 0, totalChecked = 0;
|
|
8481
|
-
if (
|
|
9334
|
+
if (existsSync30(vDir)) {
|
|
8482
9335
|
for (const file of readdirSafe(vDir)) {
|
|
8483
9336
|
if (!file.endsWith("-proof.md")) continue;
|
|
8484
9337
|
totalChecked++;
|
|
8485
|
-
const r = parseProof(
|
|
9338
|
+
const r = parseProof(join28(vDir, file));
|
|
8486
9339
|
if (isOk(r) && r.data.passed) {
|
|
8487
9340
|
proofCount++;
|
|
8488
9341
|
} else {
|
|
@@ -8504,30 +9357,21 @@ function checkVerification(projectDir) {
|
|
|
8504
9357
|
}
|
|
8505
9358
|
function checkInfrastructure(projectDir) {
|
|
8506
9359
|
try {
|
|
8507
|
-
const
|
|
8508
|
-
if (!
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
return dimOk("infrastructure", "
|
|
8514
|
-
}
|
|
8515
|
-
const
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
hasUnpinned = true;
|
|
8523
|
-
gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
|
|
8524
|
-
} else if (!ref.includes(":") && !ref.includes("@")) {
|
|
8525
|
-
hasUnpinned = true;
|
|
8526
|
-
gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
|
|
8527
|
-
}
|
|
8528
|
-
}
|
|
8529
|
-
const status = hasUnpinned ? "warn" : "pass";
|
|
8530
|
-
const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
9360
|
+
const result = validateDockerfile(projectDir);
|
|
9361
|
+
if (!result.success) {
|
|
9362
|
+
const err = result.error;
|
|
9363
|
+
if (err.includes("No Dockerfile")) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
|
|
9364
|
+
if (err.includes("could not be read")) return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
|
|
9365
|
+
if (err.includes("no FROM")) return dimOk("infrastructure", "fail", "invalid Dockerfile", [gap("infrastructure", "Dockerfile has no FROM instruction", "Add a FROM instruction with a pinned base image")]);
|
|
9366
|
+
return dimOk("infrastructure", "fail", "validation failed", [gap("infrastructure", err, "Fix Dockerfile validation errors")]);
|
|
9367
|
+
}
|
|
9368
|
+
const gaps = result.data.gaps.map((g) => gap("infrastructure", g.description, g.suggestedFix));
|
|
9369
|
+
for (const w of result.data.warnings) {
|
|
9370
|
+
gaps.push(gap("infrastructure", w, "Provide the missing configuration file"));
|
|
9371
|
+
}
|
|
9372
|
+
const issueCount = gaps.length;
|
|
9373
|
+
const status = issueCount > 0 ? "warn" : "pass";
|
|
9374
|
+
const metric = issueCount > 0 ? `Dockerfile exists (${issueCount} issue${issueCount !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
8531
9375
|
return dimOk("infrastructure", status, metric, gaps);
|
|
8532
9376
|
} catch (err) {
|
|
8533
9377
|
return dimCatch("infrastructure", err);
|
|
@@ -8535,7 +9379,7 @@ function checkInfrastructure(projectDir) {
|
|
|
8535
9379
|
}
|
|
8536
9380
|
function readdirSafe(dir) {
|
|
8537
9381
|
try {
|
|
8538
|
-
return
|
|
9382
|
+
return readdirSync7(dir);
|
|
8539
9383
|
} catch {
|
|
8540
9384
|
return [];
|
|
8541
9385
|
}
|
|
@@ -8568,8 +9412,8 @@ function formatAuditJson(result) {
|
|
|
8568
9412
|
}
|
|
8569
9413
|
|
|
8570
9414
|
// src/modules/audit/fix-generator.ts
|
|
8571
|
-
import { existsSync as
|
|
8572
|
-
import { join as
|
|
9415
|
+
import { existsSync as existsSync31, writeFileSync as writeFileSync16, mkdirSync as mkdirSync9 } from "fs";
|
|
9416
|
+
import { join as join29, dirname as dirname7 } from "path";
|
|
8573
9417
|
function buildStoryKey(gap2, index) {
|
|
8574
9418
|
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
8575
9419
|
return `audit-fix-${safeDimension}-${index}`;
|
|
@@ -8601,7 +9445,7 @@ function generateFixStories(auditResult) {
|
|
|
8601
9445
|
const stories = [];
|
|
8602
9446
|
let created = 0;
|
|
8603
9447
|
let skipped = 0;
|
|
8604
|
-
const artifactsDir =
|
|
9448
|
+
const artifactsDir = join29(
|
|
8605
9449
|
process.cwd(),
|
|
8606
9450
|
"_bmad-output",
|
|
8607
9451
|
"implementation-artifacts"
|
|
@@ -8610,8 +9454,8 @@ function generateFixStories(auditResult) {
|
|
|
8610
9454
|
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
8611
9455
|
const gap2 = dimension.gaps[i];
|
|
8612
9456
|
const key = buildStoryKey(gap2, i + 1);
|
|
8613
|
-
const filePath =
|
|
8614
|
-
if (
|
|
9457
|
+
const filePath = join29(artifactsDir, `${key}.md`);
|
|
9458
|
+
if (existsSync31(filePath)) {
|
|
8615
9459
|
stories.push({
|
|
8616
9460
|
key,
|
|
8617
9461
|
filePath,
|
|
@@ -8624,7 +9468,7 @@ function generateFixStories(auditResult) {
|
|
|
8624
9468
|
}
|
|
8625
9469
|
const markdown = buildStoryMarkdown(gap2, key);
|
|
8626
9470
|
mkdirSync9(dirname7(filePath), { recursive: true });
|
|
8627
|
-
|
|
9471
|
+
writeFileSync16(filePath, markdown, "utf-8");
|
|
8628
9472
|
stories.push({ key, filePath, gap: gap2, skipped: false });
|
|
8629
9473
|
created++;
|
|
8630
9474
|
}
|
|
@@ -8800,8 +9644,8 @@ function registerOnboardCommand(program) {
|
|
|
8800
9644
|
}
|
|
8801
9645
|
|
|
8802
9646
|
// src/commands/teardown.ts
|
|
8803
|
-
import { existsSync as
|
|
8804
|
-
import { join as
|
|
9647
|
+
import { existsSync as existsSync32, unlinkSync as unlinkSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
|
|
9648
|
+
import { join as join30 } from "path";
|
|
8805
9649
|
function buildDefaultResult() {
|
|
8806
9650
|
return {
|
|
8807
9651
|
status: "ok",
|
|
@@ -8904,16 +9748,16 @@ function registerTeardownCommand(program) {
|
|
|
8904
9748
|
info("Docker stack: not running, skipping");
|
|
8905
9749
|
}
|
|
8906
9750
|
}
|
|
8907
|
-
const composeFilePath =
|
|
8908
|
-
if (
|
|
9751
|
+
const composeFilePath = join30(projectDir, composeFile);
|
|
9752
|
+
if (existsSync32(composeFilePath)) {
|
|
8909
9753
|
unlinkSync2(composeFilePath);
|
|
8910
9754
|
result.removed.push(composeFile);
|
|
8911
9755
|
if (!isJson) {
|
|
8912
9756
|
ok(`Removed: ${composeFile}`);
|
|
8913
9757
|
}
|
|
8914
9758
|
}
|
|
8915
|
-
const otelConfigPath =
|
|
8916
|
-
if (
|
|
9759
|
+
const otelConfigPath = join30(projectDir, "otel-collector-config.yaml");
|
|
9760
|
+
if (existsSync32(otelConfigPath)) {
|
|
8917
9761
|
unlinkSync2(otelConfigPath);
|
|
8918
9762
|
result.removed.push("otel-collector-config.yaml");
|
|
8919
9763
|
if (!isJson) {
|
|
@@ -8923,8 +9767,8 @@ function registerTeardownCommand(program) {
|
|
|
8923
9767
|
}
|
|
8924
9768
|
let patchesRemoved = 0;
|
|
8925
9769
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
8926
|
-
const filePath =
|
|
8927
|
-
if (!
|
|
9770
|
+
const filePath = join30(projectDir, "_bmad", relativePath);
|
|
9771
|
+
if (!existsSync32(filePath)) {
|
|
8928
9772
|
continue;
|
|
8929
9773
|
}
|
|
8930
9774
|
try {
|
|
@@ -8943,9 +9787,9 @@ function registerTeardownCommand(program) {
|
|
|
8943
9787
|
info("BMAD patches: none found");
|
|
8944
9788
|
}
|
|
8945
9789
|
}
|
|
8946
|
-
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
8947
|
-
const pkgPath =
|
|
8948
|
-
if (
|
|
9790
|
+
if (state.otlp?.enabled && (state.stacks?.includes("nodejs") ?? state.stack === "nodejs")) {
|
|
9791
|
+
const pkgPath = join30(projectDir, "package.json");
|
|
9792
|
+
if (existsSync32(pkgPath)) {
|
|
8949
9793
|
try {
|
|
8950
9794
|
const raw = readFileSync26(pkgPath, "utf-8");
|
|
8951
9795
|
const pkg = JSON.parse(raw);
|
|
@@ -8961,7 +9805,7 @@ function registerTeardownCommand(program) {
|
|
|
8961
9805
|
for (const key of keysToRemove) {
|
|
8962
9806
|
delete scripts[key];
|
|
8963
9807
|
}
|
|
8964
|
-
|
|
9808
|
+
writeFileSync17(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
8965
9809
|
result.otlp_cleaned = true;
|
|
8966
9810
|
if (!isJson) {
|
|
8967
9811
|
ok("OTLP: removed instrumented scripts from package.json");
|
|
@@ -8987,8 +9831,8 @@ function registerTeardownCommand(program) {
|
|
|
8987
9831
|
}
|
|
8988
9832
|
}
|
|
8989
9833
|
}
|
|
8990
|
-
const harnessDir =
|
|
8991
|
-
if (
|
|
9834
|
+
const harnessDir = join30(projectDir, ".harness");
|
|
9835
|
+
if (existsSync32(harnessDir)) {
|
|
8992
9836
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
8993
9837
|
result.removed.push(".harness/");
|
|
8994
9838
|
if (!isJson) {
|
|
@@ -8996,7 +9840,7 @@ function registerTeardownCommand(program) {
|
|
|
8996
9840
|
}
|
|
8997
9841
|
}
|
|
8998
9842
|
const statePath2 = getStatePath(projectDir);
|
|
8999
|
-
if (
|
|
9843
|
+
if (existsSync32(statePath2)) {
|
|
9000
9844
|
unlinkSync2(statePath2);
|
|
9001
9845
|
result.removed.push(".claude/codeharness.local.md");
|
|
9002
9846
|
if (!isJson) {
|
|
@@ -9740,8 +10584,8 @@ function registerQueryCommand(program) {
|
|
|
9740
10584
|
}
|
|
9741
10585
|
|
|
9742
10586
|
// src/commands/retro-import.ts
|
|
9743
|
-
import { existsSync as
|
|
9744
|
-
import { join as
|
|
10587
|
+
import { existsSync as existsSync33, readFileSync as readFileSync27 } from "fs";
|
|
10588
|
+
import { join as join31 } from "path";
|
|
9745
10589
|
|
|
9746
10590
|
// src/lib/retro-parser.ts
|
|
9747
10591
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -9910,8 +10754,8 @@ function registerRetroImportCommand(program) {
|
|
|
9910
10754
|
return;
|
|
9911
10755
|
}
|
|
9912
10756
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
9913
|
-
const retroPath =
|
|
9914
|
-
if (!
|
|
10757
|
+
const retroPath = join31(root, STORY_DIR3, retroFile);
|
|
10758
|
+
if (!existsSync33(retroPath)) {
|
|
9915
10759
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
9916
10760
|
process.exitCode = 1;
|
|
9917
10761
|
return;
|
|
@@ -10299,23 +11143,23 @@ function registerVerifyEnvCommand(program) {
|
|
|
10299
11143
|
}
|
|
10300
11144
|
|
|
10301
11145
|
// src/commands/retry.ts
|
|
10302
|
-
import { join as
|
|
11146
|
+
import { join as join33 } from "path";
|
|
10303
11147
|
|
|
10304
11148
|
// src/lib/retry-state.ts
|
|
10305
|
-
import { existsSync as
|
|
10306
|
-
import { join as
|
|
11149
|
+
import { existsSync as existsSync34, readFileSync as readFileSync28, writeFileSync as writeFileSync18 } from "fs";
|
|
11150
|
+
import { join as join32 } from "path";
|
|
10307
11151
|
var RETRIES_FILE = ".story_retries";
|
|
10308
11152
|
var FLAGGED_FILE = ".flagged_stories";
|
|
10309
11153
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
10310
11154
|
function retriesPath(dir) {
|
|
10311
|
-
return
|
|
11155
|
+
return join32(dir, RETRIES_FILE);
|
|
10312
11156
|
}
|
|
10313
11157
|
function flaggedPath(dir) {
|
|
10314
|
-
return
|
|
11158
|
+
return join32(dir, FLAGGED_FILE);
|
|
10315
11159
|
}
|
|
10316
11160
|
function readRetries(dir) {
|
|
10317
11161
|
const filePath = retriesPath(dir);
|
|
10318
|
-
if (!
|
|
11162
|
+
if (!existsSync34(filePath)) {
|
|
10319
11163
|
return /* @__PURE__ */ new Map();
|
|
10320
11164
|
}
|
|
10321
11165
|
const raw = readFileSync28(filePath, "utf-8");
|
|
@@ -10340,7 +11184,7 @@ function writeRetries(dir, retries) {
|
|
|
10340
11184
|
for (const [key, count] of retries) {
|
|
10341
11185
|
lines.push(`${key}=${count}`);
|
|
10342
11186
|
}
|
|
10343
|
-
|
|
11187
|
+
writeFileSync18(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
10344
11188
|
}
|
|
10345
11189
|
function resetRetry(dir, storyKey) {
|
|
10346
11190
|
if (storyKey) {
|
|
@@ -10355,7 +11199,7 @@ function resetRetry(dir, storyKey) {
|
|
|
10355
11199
|
}
|
|
10356
11200
|
function readFlaggedStories(dir) {
|
|
10357
11201
|
const filePath = flaggedPath(dir);
|
|
10358
|
-
if (!
|
|
11202
|
+
if (!existsSync34(filePath)) {
|
|
10359
11203
|
return [];
|
|
10360
11204
|
}
|
|
10361
11205
|
const raw = readFileSync28(filePath, "utf-8");
|
|
@@ -10363,7 +11207,7 @@ function readFlaggedStories(dir) {
|
|
|
10363
11207
|
}
|
|
10364
11208
|
function writeFlaggedStories(dir, stories) {
|
|
10365
11209
|
const filePath = flaggedPath(dir);
|
|
10366
|
-
|
|
11210
|
+
writeFileSync18(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
|
|
10367
11211
|
}
|
|
10368
11212
|
function removeFlaggedStory(dir, key) {
|
|
10369
11213
|
const stories = readFlaggedStories(dir);
|
|
@@ -10383,7 +11227,7 @@ function registerRetryCommand(program) {
|
|
|
10383
11227
|
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) => {
|
|
10384
11228
|
const opts = cmd.optsWithGlobals();
|
|
10385
11229
|
const isJson = opts.json === true;
|
|
10386
|
-
const dir =
|
|
11230
|
+
const dir = join33(process.cwd(), RALPH_SUBDIR);
|
|
10387
11231
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
10388
11232
|
if (isJson) {
|
|
10389
11233
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -10792,7 +11636,7 @@ function registerAuditCommand(program) {
|
|
|
10792
11636
|
}
|
|
10793
11637
|
|
|
10794
11638
|
// src/index.ts
|
|
10795
|
-
var VERSION = true ? "0.
|
|
11639
|
+
var VERSION = true ? "0.24.1" : "0.0.0-dev";
|
|
10796
11640
|
function createProgram() {
|
|
10797
11641
|
const program = new Command();
|
|
10798
11642
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -10826,5 +11670,6 @@ if (!process.env["VITEST"]) {
|
|
|
10826
11670
|
program.parse(process.argv);
|
|
10827
11671
|
}
|
|
10828
11672
|
export {
|
|
10829
|
-
createProgram
|
|
11673
|
+
createProgram,
|
|
11674
|
+
parseStreamLine
|
|
10830
11675
|
};
|