codeharness 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1389 -542
- 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.0" : "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,
|
|
@@ -2643,178 +3302,27 @@ function generateRalphPrompt(config) {
|
|
|
2643
3302
|
prompt += `
|
|
2644
3303
|
## Flagged Stories (Skip These)
|
|
2645
3304
|
|
|
2646
|
-
`;
|
|
2647
|
-
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 };
|
|
3305
|
+
`;
|
|
3306
|
+
prompt += `The following stories have exceeded the retry limit and should be skipped:
|
|
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
|
},
|
|
@@ -2908,6 +3551,7 @@ function startRenderer(options) {
|
|
|
2908
3551
|
if (options?.quiet || !process.stdout.isTTY && !options?._forceTTY) {
|
|
2909
3552
|
return noopHandle;
|
|
2910
3553
|
}
|
|
3554
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2911
3555
|
let state = {
|
|
2912
3556
|
sprintInfo: options?.sprintState ?? null,
|
|
2913
3557
|
stories: [],
|
|
@@ -2919,7 +3563,7 @@ function startRenderer(options) {
|
|
|
2919
3563
|
retryInfo: null
|
|
2920
3564
|
};
|
|
2921
3565
|
let cleaned = false;
|
|
2922
|
-
const inkInstance = inkRender(/* @__PURE__ */
|
|
3566
|
+
const inkInstance = inkRender(/* @__PURE__ */ jsx4(App, { state }), {
|
|
2923
3567
|
exitOnCtrlC: false,
|
|
2924
3568
|
patchConsole: false,
|
|
2925
3569
|
incrementalRendering: true,
|
|
@@ -2930,7 +3574,7 @@ function startRenderer(options) {
|
|
|
2930
3574
|
function rerender() {
|
|
2931
3575
|
if (!cleaned) {
|
|
2932
3576
|
state = { ...state };
|
|
2933
|
-
inkInstance.rerender(/* @__PURE__ */
|
|
3577
|
+
inkInstance.rerender(/* @__PURE__ */ jsx4(App, { state }));
|
|
2934
3578
|
}
|
|
2935
3579
|
}
|
|
2936
3580
|
function cleanup() {
|
|
@@ -2989,13 +3633,28 @@ function startRenderer(options) {
|
|
|
2989
3633
|
state.retryInfo = { attempt: event.attempt, delay: event.delay };
|
|
2990
3634
|
break;
|
|
2991
3635
|
case "result":
|
|
3636
|
+
if (event.cost > 0 && state.sprintInfo) {
|
|
3637
|
+
state.sprintInfo = {
|
|
3638
|
+
...state.sprintInfo,
|
|
3639
|
+
totalCost: (state.sprintInfo.totalCost ?? 0) + event.cost
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
2992
3642
|
break;
|
|
2993
3643
|
}
|
|
2994
3644
|
rerender();
|
|
2995
3645
|
}
|
|
2996
3646
|
function updateSprintState(sprintState) {
|
|
2997
3647
|
if (cleaned) return;
|
|
2998
|
-
state.sprintInfo
|
|
3648
|
+
if (sprintState && state.sprintInfo) {
|
|
3649
|
+
state.sprintInfo = {
|
|
3650
|
+
...sprintState,
|
|
3651
|
+
totalCost: sprintState.totalCost ?? state.sprintInfo.totalCost,
|
|
3652
|
+
acProgress: sprintState.acProgress ?? state.sprintInfo.acProgress,
|
|
3653
|
+
currentCommand: sprintState.currentCommand ?? state.sprintInfo.currentCommand
|
|
3654
|
+
};
|
|
3655
|
+
} else {
|
|
3656
|
+
state.sprintInfo = sprintState ?? null;
|
|
3657
|
+
}
|
|
2999
3658
|
rerender();
|
|
3000
3659
|
}
|
|
3001
3660
|
function updateStories(stories) {
|
|
@@ -3012,12 +3671,12 @@ function startRenderer(options) {
|
|
|
3012
3671
|
}
|
|
3013
3672
|
|
|
3014
3673
|
// src/modules/sprint/state.ts
|
|
3015
|
-
import { readFileSync as
|
|
3016
|
-
import { join as
|
|
3674
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, renameSync, existsSync as existsSync14 } from "fs";
|
|
3675
|
+
import { join as join13 } from "path";
|
|
3017
3676
|
|
|
3018
3677
|
// src/modules/sprint/migration.ts
|
|
3019
|
-
import { readFileSync as
|
|
3020
|
-
import { join as
|
|
3678
|
+
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
3679
|
+
import { join as join12 } from "path";
|
|
3021
3680
|
var OLD_FILES = {
|
|
3022
3681
|
storyRetries: "ralph/.story_retries",
|
|
3023
3682
|
flaggedStories: "ralph/.flagged_stories",
|
|
@@ -3026,13 +3685,13 @@ var OLD_FILES = {
|
|
|
3026
3685
|
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
3027
3686
|
};
|
|
3028
3687
|
function resolve(relative2) {
|
|
3029
|
-
return
|
|
3688
|
+
return join12(process.cwd(), relative2);
|
|
3030
3689
|
}
|
|
3031
3690
|
function readIfExists(relative2) {
|
|
3032
3691
|
const p = resolve(relative2);
|
|
3033
|
-
if (!
|
|
3692
|
+
if (!existsSync13(p)) return null;
|
|
3034
3693
|
try {
|
|
3035
|
-
return
|
|
3694
|
+
return readFileSync10(p, "utf-8");
|
|
3036
3695
|
} catch {
|
|
3037
3696
|
return null;
|
|
3038
3697
|
}
|
|
@@ -3101,7 +3760,11 @@ function parseRalphStatus(content) {
|
|
|
3101
3760
|
iteration: data.loop_count ?? 0,
|
|
3102
3761
|
cost: 0,
|
|
3103
3762
|
completed: [],
|
|
3104
|
-
failed: []
|
|
3763
|
+
failed: [],
|
|
3764
|
+
currentStory: null,
|
|
3765
|
+
currentPhase: null,
|
|
3766
|
+
lastAction: null,
|
|
3767
|
+
acProgress: null
|
|
3105
3768
|
};
|
|
3106
3769
|
} catch {
|
|
3107
3770
|
return null;
|
|
@@ -3132,7 +3795,7 @@ function parseSessionIssues(content) {
|
|
|
3132
3795
|
return items;
|
|
3133
3796
|
}
|
|
3134
3797
|
function migrateFromOldFormat() {
|
|
3135
|
-
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) =>
|
|
3798
|
+
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync13(resolve(rel)));
|
|
3136
3799
|
if (!hasAnyOldFile) return fail2("No old format files found for migration");
|
|
3137
3800
|
try {
|
|
3138
3801
|
const stories = {};
|
|
@@ -3173,10 +3836,10 @@ function projectRoot() {
|
|
|
3173
3836
|
return process.cwd();
|
|
3174
3837
|
}
|
|
3175
3838
|
function statePath() {
|
|
3176
|
-
return
|
|
3839
|
+
return join13(projectRoot(), "sprint-state.json");
|
|
3177
3840
|
}
|
|
3178
3841
|
function tmpPath() {
|
|
3179
|
-
return
|
|
3842
|
+
return join13(projectRoot(), ".sprint-state.json.tmp");
|
|
3180
3843
|
}
|
|
3181
3844
|
function defaultState() {
|
|
3182
3845
|
return {
|
|
@@ -3209,7 +3872,7 @@ function writeStateAtomic(state) {
|
|
|
3209
3872
|
const data = JSON.stringify(state, null, 2) + "\n";
|
|
3210
3873
|
const tmp = tmpPath();
|
|
3211
3874
|
const final = statePath();
|
|
3212
|
-
|
|
3875
|
+
writeFileSync7(tmp, data, "utf-8");
|
|
3213
3876
|
renameSync(tmp, final);
|
|
3214
3877
|
return ok2(void 0);
|
|
3215
3878
|
} catch (err) {
|
|
@@ -3219,9 +3882,9 @@ function writeStateAtomic(state) {
|
|
|
3219
3882
|
}
|
|
3220
3883
|
function getSprintState() {
|
|
3221
3884
|
const fp = statePath();
|
|
3222
|
-
if (
|
|
3885
|
+
if (existsSync14(fp)) {
|
|
3223
3886
|
try {
|
|
3224
|
-
const raw =
|
|
3887
|
+
const raw = readFileSync11(fp, "utf-8");
|
|
3225
3888
|
const parsed = JSON.parse(raw);
|
|
3226
3889
|
const defaults = defaultState();
|
|
3227
3890
|
const run = parsed.run;
|
|
@@ -3518,9 +4181,9 @@ function generateReport(state, now) {
|
|
|
3518
4181
|
}
|
|
3519
4182
|
|
|
3520
4183
|
// src/modules/sprint/timeout.ts
|
|
3521
|
-
import { readFileSync as
|
|
4184
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
|
|
3522
4185
|
import { execSync } from "child_process";
|
|
3523
|
-
import { join as
|
|
4186
|
+
import { join as join14 } from "path";
|
|
3524
4187
|
var GIT_TIMEOUT_MS = 5e3;
|
|
3525
4188
|
var DEFAULT_MAX_LINES = 100;
|
|
3526
4189
|
function captureGitDiff() {
|
|
@@ -3549,14 +4212,14 @@ function captureGitDiff() {
|
|
|
3549
4212
|
}
|
|
3550
4213
|
function captureStateDelta(beforePath, afterPath) {
|
|
3551
4214
|
try {
|
|
3552
|
-
if (!
|
|
4215
|
+
if (!existsSync15(beforePath)) {
|
|
3553
4216
|
return fail2(`State snapshot not found: ${beforePath}`);
|
|
3554
4217
|
}
|
|
3555
|
-
if (!
|
|
4218
|
+
if (!existsSync15(afterPath)) {
|
|
3556
4219
|
return fail2(`Current state file not found: ${afterPath}`);
|
|
3557
4220
|
}
|
|
3558
|
-
const beforeRaw =
|
|
3559
|
-
const afterRaw =
|
|
4221
|
+
const beforeRaw = readFileSync12(beforePath, "utf-8");
|
|
4222
|
+
const afterRaw = readFileSync12(afterPath, "utf-8");
|
|
3560
4223
|
const before = JSON.parse(beforeRaw);
|
|
3561
4224
|
const after = JSON.parse(afterRaw);
|
|
3562
4225
|
const beforeStories = before.stories ?? {};
|
|
@@ -3581,10 +4244,10 @@ function captureStateDelta(beforePath, afterPath) {
|
|
|
3581
4244
|
}
|
|
3582
4245
|
function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
|
|
3583
4246
|
try {
|
|
3584
|
-
if (!
|
|
4247
|
+
if (!existsSync15(outputFile)) {
|
|
3585
4248
|
return fail2(`Output file not found: ${outputFile}`);
|
|
3586
4249
|
}
|
|
3587
|
-
const content =
|
|
4250
|
+
const content = readFileSync12(outputFile, "utf-8");
|
|
3588
4251
|
const lines = content.split("\n");
|
|
3589
4252
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
3590
4253
|
lines.pop();
|
|
@@ -3632,7 +4295,7 @@ function captureTimeoutReport(opts) {
|
|
|
3632
4295
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3633
4296
|
const gitResult = captureGitDiff();
|
|
3634
4297
|
const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
|
|
3635
|
-
const statePath2 =
|
|
4298
|
+
const statePath2 = join14(process.cwd(), "sprint-state.json");
|
|
3636
4299
|
const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
|
|
3637
4300
|
const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
|
|
3638
4301
|
const stderrResult = capturePartialStderr(opts.outputFile);
|
|
@@ -3646,15 +4309,15 @@ function captureTimeoutReport(opts) {
|
|
|
3646
4309
|
partialStderr,
|
|
3647
4310
|
timestamp
|
|
3648
4311
|
};
|
|
3649
|
-
const reportDir =
|
|
4312
|
+
const reportDir = join14(process.cwd(), "ralph", "logs");
|
|
3650
4313
|
const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3651
4314
|
const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
|
|
3652
|
-
const reportPath =
|
|
3653
|
-
if (!
|
|
4315
|
+
const reportPath = join14(reportDir, reportFileName);
|
|
4316
|
+
if (!existsSync15(reportDir)) {
|
|
3654
4317
|
mkdirSync3(reportDir, { recursive: true });
|
|
3655
4318
|
}
|
|
3656
4319
|
const reportContent = formatReport(capture);
|
|
3657
|
-
|
|
4320
|
+
writeFileSync8(reportPath, reportContent, "utf-8");
|
|
3658
4321
|
return ok2({
|
|
3659
4322
|
filePath: reportPath,
|
|
3660
4323
|
capture
|
|
@@ -3666,14 +4329,14 @@ function captureTimeoutReport(opts) {
|
|
|
3666
4329
|
}
|
|
3667
4330
|
function findLatestTimeoutReport(storyKey) {
|
|
3668
4331
|
try {
|
|
3669
|
-
const reportDir =
|
|
3670
|
-
if (!
|
|
4332
|
+
const reportDir = join14(process.cwd(), "ralph", "logs");
|
|
4333
|
+
if (!existsSync15(reportDir)) {
|
|
3671
4334
|
return ok2(null);
|
|
3672
4335
|
}
|
|
3673
4336
|
const safeStoryKey = storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3674
4337
|
const prefix = `timeout-report-`;
|
|
3675
4338
|
const suffix = `-${safeStoryKey}.md`;
|
|
3676
|
-
const files =
|
|
4339
|
+
const files = readdirSync3(reportDir, { encoding: "utf-8" });
|
|
3677
4340
|
const matches = [];
|
|
3678
4341
|
for (const f of files) {
|
|
3679
4342
|
if (f.startsWith(prefix) && f.endsWith(suffix)) {
|
|
@@ -3689,8 +4352,8 @@ function findLatestTimeoutReport(storyKey) {
|
|
|
3689
4352
|
}
|
|
3690
4353
|
matches.sort((a, b) => b.iteration - a.iteration);
|
|
3691
4354
|
const latest = matches[0];
|
|
3692
|
-
const reportPath =
|
|
3693
|
-
const content =
|
|
4355
|
+
const reportPath = join14(reportDir, latest.fileName);
|
|
4356
|
+
const content = readFileSync12(reportPath, "utf-8");
|
|
3694
4357
|
let durationMinutes = 0;
|
|
3695
4358
|
let filesChanged = 0;
|
|
3696
4359
|
const durationMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*minutes/);
|
|
@@ -3719,12 +4382,12 @@ function findLatestTimeoutReport(storyKey) {
|
|
|
3719
4382
|
}
|
|
3720
4383
|
|
|
3721
4384
|
// src/modules/sprint/feedback.ts
|
|
3722
|
-
import { readFileSync as
|
|
3723
|
-
import { existsSync as
|
|
3724
|
-
import { join as
|
|
4385
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
4386
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4387
|
+
import { join as join15 } from "path";
|
|
3725
4388
|
|
|
3726
4389
|
// src/modules/sprint/validator.ts
|
|
3727
|
-
import { readFileSync as
|
|
4390
|
+
import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
|
|
3728
4391
|
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
3729
4392
|
"backlog",
|
|
3730
4393
|
"ready",
|
|
@@ -3755,10 +4418,10 @@ function parseSprintStatusKeys(content) {
|
|
|
3755
4418
|
}
|
|
3756
4419
|
function parseStateFile(statePath2) {
|
|
3757
4420
|
try {
|
|
3758
|
-
if (!
|
|
4421
|
+
if (!existsSync17(statePath2)) {
|
|
3759
4422
|
return fail2(`State file not found: ${statePath2}`);
|
|
3760
4423
|
}
|
|
3761
|
-
const raw =
|
|
4424
|
+
const raw = readFileSync14(statePath2, "utf-8");
|
|
3762
4425
|
const parsed = JSON.parse(raw);
|
|
3763
4426
|
return ok2(parsed);
|
|
3764
4427
|
} catch (err) {
|
|
@@ -3773,10 +4436,10 @@ function validateStateConsistency(statePath2, sprintStatusPath) {
|
|
|
3773
4436
|
return fail2(stateResult.error);
|
|
3774
4437
|
}
|
|
3775
4438
|
const state = stateResult.data;
|
|
3776
|
-
if (!
|
|
4439
|
+
if (!existsSync17(sprintStatusPath)) {
|
|
3777
4440
|
return fail2(`Sprint status file not found: ${sprintStatusPath}`);
|
|
3778
4441
|
}
|
|
3779
|
-
const statusContent =
|
|
4442
|
+
const statusContent = readFileSync14(sprintStatusPath, "utf-8");
|
|
3780
4443
|
const keysResult = parseSprintStatusKeys(statusContent);
|
|
3781
4444
|
if (!keysResult.success) {
|
|
3782
4445
|
return fail2(keysResult.error);
|
|
@@ -3894,6 +4557,109 @@ function clearRunProgress2() {
|
|
|
3894
4557
|
return clearRunProgress();
|
|
3895
4558
|
}
|
|
3896
4559
|
|
|
4560
|
+
// src/lib/run-helpers.ts
|
|
4561
|
+
import { StringDecoder } from "string_decoder";
|
|
4562
|
+
|
|
4563
|
+
// src/lib/stream-parser.ts
|
|
4564
|
+
function parseStreamLine(line) {
|
|
4565
|
+
const trimmed = line.trim();
|
|
4566
|
+
if (trimmed.length === 0) {
|
|
4567
|
+
return null;
|
|
4568
|
+
}
|
|
4569
|
+
let parsed;
|
|
4570
|
+
try {
|
|
4571
|
+
parsed = JSON.parse(trimmed);
|
|
4572
|
+
} catch {
|
|
4573
|
+
return null;
|
|
4574
|
+
}
|
|
4575
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4576
|
+
return null;
|
|
4577
|
+
}
|
|
4578
|
+
const wrapperType = parsed.type;
|
|
4579
|
+
if (wrapperType === "stream_event") {
|
|
4580
|
+
return parseStreamEvent(parsed);
|
|
4581
|
+
}
|
|
4582
|
+
if (wrapperType === "system") {
|
|
4583
|
+
return parseSystemEvent(parsed);
|
|
4584
|
+
}
|
|
4585
|
+
if (wrapperType === "result") {
|
|
4586
|
+
return parseResultEvent(parsed);
|
|
4587
|
+
}
|
|
4588
|
+
return null;
|
|
4589
|
+
}
|
|
4590
|
+
function parseStreamEvent(parsed) {
|
|
4591
|
+
const event = parsed.event;
|
|
4592
|
+
if (!event || typeof event !== "object") {
|
|
4593
|
+
return null;
|
|
4594
|
+
}
|
|
4595
|
+
const eventType = event.type;
|
|
4596
|
+
if (eventType === "content_block_start") {
|
|
4597
|
+
return parseContentBlockStart(event);
|
|
4598
|
+
}
|
|
4599
|
+
if (eventType === "content_block_delta") {
|
|
4600
|
+
return parseContentBlockDelta(event);
|
|
4601
|
+
}
|
|
4602
|
+
if (eventType === "content_block_stop") {
|
|
4603
|
+
return { type: "tool-complete" };
|
|
4604
|
+
}
|
|
4605
|
+
return null;
|
|
4606
|
+
}
|
|
4607
|
+
function parseContentBlockStart(event) {
|
|
4608
|
+
const contentBlock = event.content_block;
|
|
4609
|
+
if (!contentBlock || typeof contentBlock !== "object") {
|
|
4610
|
+
return null;
|
|
4611
|
+
}
|
|
4612
|
+
if (contentBlock.type === "tool_use") {
|
|
4613
|
+
const name = contentBlock.name;
|
|
4614
|
+
const id = contentBlock.id;
|
|
4615
|
+
if (typeof name === "string" && typeof id === "string") {
|
|
4616
|
+
return { type: "tool-start", name, id };
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
return null;
|
|
4620
|
+
}
|
|
4621
|
+
function parseContentBlockDelta(event) {
|
|
4622
|
+
const delta = event.delta;
|
|
4623
|
+
if (!delta || typeof delta !== "object") {
|
|
4624
|
+
return null;
|
|
4625
|
+
}
|
|
4626
|
+
if (delta.type === "input_json_delta") {
|
|
4627
|
+
const partialJson = delta.partial_json;
|
|
4628
|
+
if (typeof partialJson === "string") {
|
|
4629
|
+
return { type: "tool-input", partial: partialJson };
|
|
4630
|
+
}
|
|
4631
|
+
return null;
|
|
4632
|
+
}
|
|
4633
|
+
if (delta.type === "text_delta") {
|
|
4634
|
+
const text = delta.text;
|
|
4635
|
+
if (typeof text === "string") {
|
|
4636
|
+
return { type: "text", text };
|
|
4637
|
+
}
|
|
4638
|
+
return null;
|
|
4639
|
+
}
|
|
4640
|
+
return null;
|
|
4641
|
+
}
|
|
4642
|
+
function parseSystemEvent(parsed) {
|
|
4643
|
+
const subtype = parsed.subtype;
|
|
4644
|
+
if (subtype === "api_retry") {
|
|
4645
|
+
const attempt = parsed.attempt;
|
|
4646
|
+
const delay = parsed.retry_delay_ms;
|
|
4647
|
+
if (typeof attempt === "number" && typeof delay === "number") {
|
|
4648
|
+
return { type: "retry", attempt, delay };
|
|
4649
|
+
}
|
|
4650
|
+
return null;
|
|
4651
|
+
}
|
|
4652
|
+
return null;
|
|
4653
|
+
}
|
|
4654
|
+
function parseResultEvent(parsed) {
|
|
4655
|
+
const costUsd = parsed.cost_usd;
|
|
4656
|
+
const sessionId = parsed.session_id;
|
|
4657
|
+
if (typeof costUsd === "number" && typeof sessionId === "string") {
|
|
4658
|
+
return { type: "result", cost: costUsd, sessionId };
|
|
4659
|
+
}
|
|
4660
|
+
return null;
|
|
4661
|
+
}
|
|
4662
|
+
|
|
3897
4663
|
// src/lib/run-helpers.ts
|
|
3898
4664
|
var STORY_KEY_PATTERN = /^\d+-\d+-/;
|
|
3899
4665
|
function countStories(statuses) {
|
|
@@ -3928,6 +4694,9 @@ function buildSpawnArgs(opts) {
|
|
|
3928
4694
|
"--prompt",
|
|
3929
4695
|
opts.promptFile
|
|
3930
4696
|
];
|
|
4697
|
+
if (!opts.quiet) {
|
|
4698
|
+
args.push("--live");
|
|
4699
|
+
}
|
|
3931
4700
|
if (opts.maxStoryRetries !== void 0) {
|
|
3932
4701
|
args.push("--max-story-retries", String(opts.maxStoryRetries));
|
|
3933
4702
|
}
|
|
@@ -3979,6 +4748,7 @@ var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
|
|
|
3979
4748
|
var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
|
|
3980
4749
|
var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
|
|
3981
4750
|
var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
|
|
4751
|
+
var LOOP_ITERATION = /\[LOOP\]\s+iteration\s+(\d+)/;
|
|
3982
4752
|
var ERROR_LINE = /\[ERROR\]\s+(.+)/;
|
|
3983
4753
|
function parseRalphMessage(rawLine) {
|
|
3984
4754
|
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
@@ -4023,6 +4793,41 @@ function parseRalphMessage(rawLine) {
|
|
|
4023
4793
|
}
|
|
4024
4794
|
return null;
|
|
4025
4795
|
}
|
|
4796
|
+
function parseIterationMessage(rawLine) {
|
|
4797
|
+
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
4798
|
+
if (clean.length === 0) return null;
|
|
4799
|
+
const match = LOOP_ITERATION.exec(clean);
|
|
4800
|
+
if (match) {
|
|
4801
|
+
return parseInt(match[1], 10);
|
|
4802
|
+
}
|
|
4803
|
+
return null;
|
|
4804
|
+
}
|
|
4805
|
+
function createLineProcessor(callbacks, opts) {
|
|
4806
|
+
let partial = "";
|
|
4807
|
+
const decoder = new StringDecoder("utf8");
|
|
4808
|
+
return (data) => {
|
|
4809
|
+
const text = partial + decoder.write(data);
|
|
4810
|
+
const parts = text.split("\n");
|
|
4811
|
+
partial = parts.pop() ?? "";
|
|
4812
|
+
for (const line of parts) {
|
|
4813
|
+
if (line.trim().length === 0) continue;
|
|
4814
|
+
const event = parseStreamLine(line);
|
|
4815
|
+
if (event) {
|
|
4816
|
+
callbacks.onEvent(event);
|
|
4817
|
+
}
|
|
4818
|
+
if (opts?.parseRalph) {
|
|
4819
|
+
const msg = parseRalphMessage(line);
|
|
4820
|
+
if (msg && callbacks.onMessage) {
|
|
4821
|
+
callbacks.onMessage(msg);
|
|
4822
|
+
}
|
|
4823
|
+
const iteration = parseIterationMessage(line);
|
|
4824
|
+
if (iteration !== null && callbacks.onIteration) {
|
|
4825
|
+
callbacks.onIteration(iteration);
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
};
|
|
4830
|
+
}
|
|
4026
4831
|
|
|
4027
4832
|
// src/commands/run.ts
|
|
4028
4833
|
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
@@ -4033,10 +4838,10 @@ function resolveRalphPath() {
|
|
|
4033
4838
|
if (root.endsWith("/src") || root.endsWith("\\src")) {
|
|
4034
4839
|
root = dirname4(root);
|
|
4035
4840
|
}
|
|
4036
|
-
return
|
|
4841
|
+
return join16(root, "ralph", "ralph.sh");
|
|
4037
4842
|
}
|
|
4038
4843
|
function resolvePluginDir() {
|
|
4039
|
-
return
|
|
4844
|
+
return join16(process.cwd(), ".claude");
|
|
4040
4845
|
}
|
|
4041
4846
|
function registerRunCommand(program) {
|
|
4042
4847
|
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 +4849,19 @@ function registerRunCommand(program) {
|
|
|
4044
4849
|
const isJson = !!globalOpts.json;
|
|
4045
4850
|
const outputOpts = { json: isJson };
|
|
4046
4851
|
const ralphPath = resolveRalphPath();
|
|
4047
|
-
if (!
|
|
4852
|
+
if (!existsSync18(ralphPath)) {
|
|
4048
4853
|
fail("Ralph loop not found \u2014 reinstall codeharness", outputOpts);
|
|
4049
4854
|
process.exitCode = 1;
|
|
4050
4855
|
return;
|
|
4051
4856
|
}
|
|
4052
4857
|
const pluginDir = resolvePluginDir();
|
|
4053
|
-
if (!
|
|
4858
|
+
if (!existsSync18(pluginDir)) {
|
|
4054
4859
|
fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
|
|
4055
4860
|
process.exitCode = 1;
|
|
4056
4861
|
return;
|
|
4057
4862
|
}
|
|
4058
4863
|
const projectDir = process.cwd();
|
|
4059
|
-
const sprintStatusPath =
|
|
4864
|
+
const sprintStatusPath = join16(projectDir, SPRINT_STATUS_REL);
|
|
4060
4865
|
const statuses = readSprintStatus(projectDir);
|
|
4061
4866
|
const counts = countStories(statuses);
|
|
4062
4867
|
if (counts.total === 0) {
|
|
@@ -4075,12 +4880,12 @@ function registerRunCommand(program) {
|
|
|
4075
4880
|
process.exitCode = 1;
|
|
4076
4881
|
return;
|
|
4077
4882
|
}
|
|
4078
|
-
const promptFile =
|
|
4079
|
-
const flaggedFilePath =
|
|
4883
|
+
const promptFile = join16(projectDir, "ralph", ".harness-prompt.md");
|
|
4884
|
+
const flaggedFilePath = join16(projectDir, "ralph", ".flagged_stories");
|
|
4080
4885
|
let flaggedStories;
|
|
4081
|
-
if (
|
|
4886
|
+
if (existsSync18(flaggedFilePath)) {
|
|
4082
4887
|
try {
|
|
4083
|
-
const flaggedContent =
|
|
4888
|
+
const flaggedContent = readFileSync15(flaggedFilePath, "utf-8");
|
|
4084
4889
|
flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
|
|
4085
4890
|
} catch {
|
|
4086
4891
|
}
|
|
@@ -4092,7 +4897,7 @@ function registerRunCommand(program) {
|
|
|
4092
4897
|
});
|
|
4093
4898
|
try {
|
|
4094
4899
|
mkdirSync4(dirname4(promptFile), { recursive: true });
|
|
4095
|
-
|
|
4900
|
+
writeFileSync10(promptFile, promptContent, "utf-8");
|
|
4096
4901
|
} catch (err) {
|
|
4097
4902
|
const message = err instanceof Error ? err.message : String(err);
|
|
4098
4903
|
fail(`Failed to write prompt file: ${message}`, outputOpts);
|
|
@@ -4119,6 +4924,7 @@ function registerRunCommand(program) {
|
|
|
4119
4924
|
const rendererHandle = startRenderer({ quiet });
|
|
4120
4925
|
let sprintStateInterval = null;
|
|
4121
4926
|
const sessionStartTime = Date.now();
|
|
4927
|
+
let currentIterationCount = 0;
|
|
4122
4928
|
try {
|
|
4123
4929
|
const initialState = getSprintState2();
|
|
4124
4930
|
if (initialState.success) {
|
|
@@ -4128,7 +4934,8 @@ function registerRunCommand(program) {
|
|
|
4128
4934
|
phase: s.run.currentPhase ?? "",
|
|
4129
4935
|
done: s.sprint.done,
|
|
4130
4936
|
total: s.sprint.total,
|
|
4131
|
-
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4937
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime),
|
|
4938
|
+
iterationCount: currentIterationCount
|
|
4132
4939
|
};
|
|
4133
4940
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4134
4941
|
}
|
|
@@ -4143,30 +4950,18 @@ function registerRunCommand(program) {
|
|
|
4143
4950
|
env
|
|
4144
4951
|
});
|
|
4145
4952
|
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 }));
|
|
4953
|
+
const stdoutHandler = createLineProcessor({
|
|
4954
|
+
onEvent: (event) => rendererHandle.update(event)
|
|
4955
|
+
});
|
|
4956
|
+
const stderrHandler = createLineProcessor({
|
|
4957
|
+
onEvent: (event) => rendererHandle.update(event),
|
|
4958
|
+
onMessage: (msg) => rendererHandle.addMessage(msg),
|
|
4959
|
+
onIteration: (iteration) => {
|
|
4960
|
+
currentIterationCount = iteration;
|
|
4961
|
+
}
|
|
4962
|
+
}, { parseRalph: true });
|
|
4963
|
+
child.stdout.on("data", stdoutHandler);
|
|
4964
|
+
child.stderr.on("data", stderrHandler);
|
|
4170
4965
|
sprintStateInterval = setInterval(() => {
|
|
4171
4966
|
try {
|
|
4172
4967
|
const stateResult = getSprintState2();
|
|
@@ -4177,7 +4972,8 @@ function registerRunCommand(program) {
|
|
|
4177
4972
|
phase: s.run.currentPhase ?? "",
|
|
4178
4973
|
done: s.sprint.done,
|
|
4179
4974
|
total: s.sprint.total,
|
|
4180
|
-
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4975
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime),
|
|
4976
|
+
iterationCount: currentIterationCount
|
|
4181
4977
|
};
|
|
4182
4978
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4183
4979
|
}
|
|
@@ -4201,10 +4997,10 @@ function registerRunCommand(program) {
|
|
|
4201
4997
|
});
|
|
4202
4998
|
});
|
|
4203
4999
|
if (isJson) {
|
|
4204
|
-
const statusFile =
|
|
4205
|
-
if (
|
|
5000
|
+
const statusFile = join16(projectDir, "ralph", "status.json");
|
|
5001
|
+
if (existsSync18(statusFile)) {
|
|
4206
5002
|
try {
|
|
4207
|
-
const statusData = JSON.parse(
|
|
5003
|
+
const statusData = JSON.parse(readFileSync15(statusFile, "utf-8"));
|
|
4208
5004
|
const finalStatuses = readSprintStatus(projectDir);
|
|
4209
5005
|
const finalCounts = countStories(finalStatuses);
|
|
4210
5006
|
jsonOutput({
|
|
@@ -4256,14 +5052,14 @@ function registerRunCommand(program) {
|
|
|
4256
5052
|
}
|
|
4257
5053
|
|
|
4258
5054
|
// src/commands/verify.ts
|
|
4259
|
-
import { existsSync as
|
|
4260
|
-
import { join as
|
|
5055
|
+
import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
|
|
5056
|
+
import { join as join25 } from "path";
|
|
4261
5057
|
|
|
4262
5058
|
// src/modules/verify/index.ts
|
|
4263
|
-
import { readFileSync as
|
|
5059
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
4264
5060
|
|
|
4265
5061
|
// src/modules/verify/proof.ts
|
|
4266
|
-
import { existsSync as
|
|
5062
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
4267
5063
|
function classifyEvidenceCommands(proofContent) {
|
|
4268
5064
|
const results = [];
|
|
4269
5065
|
const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
|
|
@@ -4349,10 +5145,10 @@ function validateProofQuality(proofPath) {
|
|
|
4349
5145
|
otherCount: 0,
|
|
4350
5146
|
blackBoxPass: false
|
|
4351
5147
|
};
|
|
4352
|
-
if (!
|
|
5148
|
+
if (!existsSync19(proofPath)) {
|
|
4353
5149
|
return emptyResult;
|
|
4354
5150
|
}
|
|
4355
|
-
const content =
|
|
5151
|
+
const content = readFileSync16(proofPath, "utf-8");
|
|
4356
5152
|
const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
|
|
4357
5153
|
const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
|
|
4358
5154
|
const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
|
|
@@ -4495,21 +5291,21 @@ function validateProofQuality(proofPath) {
|
|
|
4495
5291
|
|
|
4496
5292
|
// src/modules/verify/orchestrator.ts
|
|
4497
5293
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
4498
|
-
import { mkdirSync as mkdirSync6, writeFileSync as
|
|
4499
|
-
import { join as
|
|
5294
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync12 } from "fs";
|
|
5295
|
+
import { join as join18 } from "path";
|
|
4500
5296
|
|
|
4501
5297
|
// src/lib/doc-health.ts
|
|
4502
5298
|
import { execSync as execSync2 } from "child_process";
|
|
4503
5299
|
import {
|
|
4504
|
-
existsSync as
|
|
5300
|
+
existsSync as existsSync20,
|
|
4505
5301
|
mkdirSync as mkdirSync5,
|
|
4506
|
-
readFileSync as
|
|
4507
|
-
readdirSync as
|
|
5302
|
+
readFileSync as readFileSync17,
|
|
5303
|
+
readdirSync as readdirSync4,
|
|
4508
5304
|
statSync,
|
|
4509
5305
|
unlinkSync,
|
|
4510
|
-
writeFileSync as
|
|
5306
|
+
writeFileSync as writeFileSync11
|
|
4511
5307
|
} from "fs";
|
|
4512
|
-
import { join as
|
|
5308
|
+
import { join as join17, relative } from "path";
|
|
4513
5309
|
var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
|
|
4514
5310
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
4515
5311
|
var DEFAULT_MODULE_THRESHOLD = 3;
|
|
@@ -4520,7 +5316,7 @@ function findModules(dir, threshold) {
|
|
|
4520
5316
|
function walk(current) {
|
|
4521
5317
|
let entries;
|
|
4522
5318
|
try {
|
|
4523
|
-
entries =
|
|
5319
|
+
entries = readdirSync4(current);
|
|
4524
5320
|
} catch {
|
|
4525
5321
|
return;
|
|
4526
5322
|
}
|
|
@@ -4531,7 +5327,7 @@ function findModules(dir, threshold) {
|
|
|
4531
5327
|
let sourceCount = 0;
|
|
4532
5328
|
const subdirs = [];
|
|
4533
5329
|
for (const entry of entries) {
|
|
4534
|
-
const fullPath =
|
|
5330
|
+
const fullPath = join17(current, entry);
|
|
4535
5331
|
let stat;
|
|
4536
5332
|
try {
|
|
4537
5333
|
stat = statSync(fullPath);
|
|
@@ -4572,14 +5368,14 @@ function getNewestSourceMtime(dir) {
|
|
|
4572
5368
|
function walk(current) {
|
|
4573
5369
|
let entries;
|
|
4574
5370
|
try {
|
|
4575
|
-
entries =
|
|
5371
|
+
entries = readdirSync4(current);
|
|
4576
5372
|
} catch {
|
|
4577
5373
|
return;
|
|
4578
5374
|
}
|
|
4579
5375
|
const dirName = current.split("/").pop() ?? "";
|
|
4580
5376
|
if (dirName === "node_modules" || dirName === ".git") return;
|
|
4581
5377
|
for (const entry of entries) {
|
|
4582
|
-
const fullPath =
|
|
5378
|
+
const fullPath = join17(current, entry);
|
|
4583
5379
|
let stat;
|
|
4584
5380
|
try {
|
|
4585
5381
|
stat = statSync(fullPath);
|
|
@@ -4608,14 +5404,14 @@ function getSourceFilesInModule(modulePath) {
|
|
|
4608
5404
|
function walk(current) {
|
|
4609
5405
|
let entries;
|
|
4610
5406
|
try {
|
|
4611
|
-
entries =
|
|
5407
|
+
entries = readdirSync4(current);
|
|
4612
5408
|
} catch {
|
|
4613
5409
|
return;
|
|
4614
5410
|
}
|
|
4615
5411
|
const dirName = current.split("/").pop() ?? "";
|
|
4616
5412
|
if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
|
|
4617
5413
|
for (const entry of entries) {
|
|
4618
|
-
const fullPath =
|
|
5414
|
+
const fullPath = join17(current, entry);
|
|
4619
5415
|
let stat;
|
|
4620
5416
|
try {
|
|
4621
5417
|
stat = statSync(fullPath);
|
|
@@ -4636,8 +5432,8 @@ function getSourceFilesInModule(modulePath) {
|
|
|
4636
5432
|
return files;
|
|
4637
5433
|
}
|
|
4638
5434
|
function getMentionedFilesInAgentsMd(agentsPath) {
|
|
4639
|
-
if (!
|
|
4640
|
-
const content =
|
|
5435
|
+
if (!existsSync20(agentsPath)) return [];
|
|
5436
|
+
const content = readFileSync17(agentsPath, "utf-8");
|
|
4641
5437
|
const mentioned = /* @__PURE__ */ new Set();
|
|
4642
5438
|
const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
|
|
4643
5439
|
let match;
|
|
@@ -4661,12 +5457,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
|
|
|
4661
5457
|
}
|
|
4662
5458
|
function checkAgentsMdForModule(modulePath, dir) {
|
|
4663
5459
|
const root = dir ?? process.cwd();
|
|
4664
|
-
const fullModulePath =
|
|
4665
|
-
let agentsPath =
|
|
4666
|
-
if (!
|
|
4667
|
-
agentsPath =
|
|
5460
|
+
const fullModulePath = join17(root, modulePath);
|
|
5461
|
+
let agentsPath = join17(fullModulePath, "AGENTS.md");
|
|
5462
|
+
if (!existsSync20(agentsPath)) {
|
|
5463
|
+
agentsPath = join17(root, "AGENTS.md");
|
|
4668
5464
|
}
|
|
4669
|
-
if (!
|
|
5465
|
+
if (!existsSync20(agentsPath)) {
|
|
4670
5466
|
return {
|
|
4671
5467
|
path: relative(root, agentsPath),
|
|
4672
5468
|
grade: "missing",
|
|
@@ -4697,9 +5493,9 @@ function checkAgentsMdForModule(modulePath, dir) {
|
|
|
4697
5493
|
};
|
|
4698
5494
|
}
|
|
4699
5495
|
function checkDoNotEditHeaders(docPath) {
|
|
4700
|
-
if (!
|
|
5496
|
+
if (!existsSync20(docPath)) return false;
|
|
4701
5497
|
try {
|
|
4702
|
-
const content =
|
|
5498
|
+
const content = readFileSync17(docPath, "utf-8");
|
|
4703
5499
|
if (content.length === 0) return false;
|
|
4704
5500
|
return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
|
|
4705
5501
|
} catch {
|
|
@@ -4711,17 +5507,17 @@ function scanDocHealth(dir) {
|
|
|
4711
5507
|
const root = dir ?? process.cwd();
|
|
4712
5508
|
const documents = [];
|
|
4713
5509
|
const modules = findModules(root);
|
|
4714
|
-
const rootAgentsPath =
|
|
4715
|
-
if (
|
|
5510
|
+
const rootAgentsPath = join17(root, "AGENTS.md");
|
|
5511
|
+
if (existsSync20(rootAgentsPath)) {
|
|
4716
5512
|
if (modules.length > 0) {
|
|
4717
5513
|
const docMtime = statSync(rootAgentsPath).mtime;
|
|
4718
5514
|
let allMissing = [];
|
|
4719
5515
|
let staleModule = "";
|
|
4720
5516
|
let newestCode = null;
|
|
4721
5517
|
for (const mod of modules) {
|
|
4722
|
-
const fullModPath =
|
|
4723
|
-
const modAgentsPath =
|
|
4724
|
-
if (
|
|
5518
|
+
const fullModPath = join17(root, mod);
|
|
5519
|
+
const modAgentsPath = join17(fullModPath, "AGENTS.md");
|
|
5520
|
+
if (existsSync20(modAgentsPath)) continue;
|
|
4725
5521
|
const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
|
|
4726
5522
|
if (missing.length > 0 && staleModule === "") {
|
|
4727
5523
|
staleModule = mod;
|
|
@@ -4769,8 +5565,8 @@ function scanDocHealth(dir) {
|
|
|
4769
5565
|
});
|
|
4770
5566
|
}
|
|
4771
5567
|
for (const mod of modules) {
|
|
4772
|
-
const modAgentsPath =
|
|
4773
|
-
if (
|
|
5568
|
+
const modAgentsPath = join17(root, mod, "AGENTS.md");
|
|
5569
|
+
if (existsSync20(modAgentsPath)) {
|
|
4774
5570
|
const result = checkAgentsMdForModule(mod, root);
|
|
4775
5571
|
if (result.path !== "AGENTS.md") {
|
|
4776
5572
|
documents.push(result);
|
|
@@ -4778,9 +5574,9 @@ function scanDocHealth(dir) {
|
|
|
4778
5574
|
}
|
|
4779
5575
|
}
|
|
4780
5576
|
}
|
|
4781
|
-
const indexPath =
|
|
4782
|
-
if (
|
|
4783
|
-
const content =
|
|
5577
|
+
const indexPath = join17(root, "docs", "index.md");
|
|
5578
|
+
if (existsSync20(indexPath)) {
|
|
5579
|
+
const content = readFileSync17(indexPath, "utf-8");
|
|
4784
5580
|
const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
|
|
4785
5581
|
documents.push({
|
|
4786
5582
|
path: "docs/index.md",
|
|
@@ -4790,11 +5586,11 @@ function scanDocHealth(dir) {
|
|
|
4790
5586
|
reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
|
|
4791
5587
|
});
|
|
4792
5588
|
}
|
|
4793
|
-
const activeDir =
|
|
4794
|
-
if (
|
|
4795
|
-
const files =
|
|
5589
|
+
const activeDir = join17(root, "docs", "exec-plans", "active");
|
|
5590
|
+
if (existsSync20(activeDir)) {
|
|
5591
|
+
const files = readdirSync4(activeDir).filter((f) => f.endsWith(".md"));
|
|
4796
5592
|
for (const file of files) {
|
|
4797
|
-
const filePath =
|
|
5593
|
+
const filePath = join17(activeDir, file);
|
|
4798
5594
|
documents.push({
|
|
4799
5595
|
path: `docs/exec-plans/active/${file}`,
|
|
4800
5596
|
grade: "fresh",
|
|
@@ -4805,11 +5601,11 @@ function scanDocHealth(dir) {
|
|
|
4805
5601
|
}
|
|
4806
5602
|
}
|
|
4807
5603
|
for (const subdir of ["quality", "generated"]) {
|
|
4808
|
-
const dirPath =
|
|
4809
|
-
if (!
|
|
4810
|
-
const files =
|
|
5604
|
+
const dirPath = join17(root, "docs", subdir);
|
|
5605
|
+
if (!existsSync20(dirPath)) continue;
|
|
5606
|
+
const files = readdirSync4(dirPath).filter((f) => !f.startsWith("."));
|
|
4811
5607
|
for (const file of files) {
|
|
4812
|
-
const filePath =
|
|
5608
|
+
const filePath = join17(dirPath, file);
|
|
4813
5609
|
let stat;
|
|
4814
5610
|
try {
|
|
4815
5611
|
stat = statSync(filePath);
|
|
@@ -4842,7 +5638,7 @@ function scanDocHealth(dir) {
|
|
|
4842
5638
|
}
|
|
4843
5639
|
function checkAgentsMdLineCount(filePath, docPath, documents) {
|
|
4844
5640
|
try {
|
|
4845
|
-
const content =
|
|
5641
|
+
const content = readFileSync17(filePath, "utf-8");
|
|
4846
5642
|
const lineCount = content.split("\n").length;
|
|
4847
5643
|
if (lineCount > 100) {
|
|
4848
5644
|
documents.push({
|
|
@@ -4880,15 +5676,15 @@ function checkStoryDocFreshness(storyId, dir) {
|
|
|
4880
5676
|
for (const mod of modulesToCheck) {
|
|
4881
5677
|
const result = checkAgentsMdForModule(mod, root);
|
|
4882
5678
|
documents.push(result);
|
|
4883
|
-
const moduleAgentsPath =
|
|
4884
|
-
const actualAgentsPath =
|
|
4885
|
-
if (
|
|
5679
|
+
const moduleAgentsPath = join17(root, mod, "AGENTS.md");
|
|
5680
|
+
const actualAgentsPath = existsSync20(moduleAgentsPath) ? moduleAgentsPath : join17(root, "AGENTS.md");
|
|
5681
|
+
if (existsSync20(actualAgentsPath)) {
|
|
4886
5682
|
checkAgentsMdLineCount(actualAgentsPath, result.path, documents);
|
|
4887
5683
|
}
|
|
4888
5684
|
}
|
|
4889
5685
|
if (modulesToCheck.length === 0) {
|
|
4890
|
-
const rootAgentsPath =
|
|
4891
|
-
if (
|
|
5686
|
+
const rootAgentsPath = join17(root, "AGENTS.md");
|
|
5687
|
+
if (existsSync20(rootAgentsPath)) {
|
|
4892
5688
|
documents.push({
|
|
4893
5689
|
path: "AGENTS.md",
|
|
4894
5690
|
grade: "fresh",
|
|
@@ -4926,11 +5722,11 @@ function getRecentlyChangedFiles(dir) {
|
|
|
4926
5722
|
}
|
|
4927
5723
|
function completeExecPlan(storyId, dir) {
|
|
4928
5724
|
const root = dir ?? process.cwd();
|
|
4929
|
-
const activePath =
|
|
4930
|
-
if (!
|
|
5725
|
+
const activePath = join17(root, "docs", "exec-plans", "active", `${storyId}.md`);
|
|
5726
|
+
if (!existsSync20(activePath)) {
|
|
4931
5727
|
return null;
|
|
4932
5728
|
}
|
|
4933
|
-
let content =
|
|
5729
|
+
let content = readFileSync17(activePath, "utf-8");
|
|
4934
5730
|
content = content.replace(/^Status:\s*active$/m, "Status: completed");
|
|
4935
5731
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4936
5732
|
content = content.replace(
|
|
@@ -4938,10 +5734,10 @@ function completeExecPlan(storyId, dir) {
|
|
|
4938
5734
|
`$1
|
|
4939
5735
|
Completed: ${timestamp}`
|
|
4940
5736
|
);
|
|
4941
|
-
const completedDir =
|
|
5737
|
+
const completedDir = join17(root, "docs", "exec-plans", "completed");
|
|
4942
5738
|
mkdirSync5(completedDir, { recursive: true });
|
|
4943
|
-
const completedPath =
|
|
4944
|
-
|
|
5739
|
+
const completedPath = join17(completedDir, `${storyId}.md`);
|
|
5740
|
+
writeFileSync11(completedPath, content, "utf-8");
|
|
4945
5741
|
try {
|
|
4946
5742
|
unlinkSync(activePath);
|
|
4947
5743
|
} catch {
|
|
@@ -5066,8 +5862,8 @@ function checkPreconditions(dir, storyId) {
|
|
|
5066
5862
|
}
|
|
5067
5863
|
function createProofDocument(storyId, storyTitle, acs, dir) {
|
|
5068
5864
|
const root = dir ?? process.cwd();
|
|
5069
|
-
const verificationDir =
|
|
5070
|
-
const screenshotsDir =
|
|
5865
|
+
const verificationDir = join18(root, "verification");
|
|
5866
|
+
const screenshotsDir = join18(verificationDir, "screenshots");
|
|
5071
5867
|
mkdirSync6(verificationDir, { recursive: true });
|
|
5072
5868
|
mkdirSync6(screenshotsDir, { recursive: true });
|
|
5073
5869
|
const criteria = acs.map((ac) => ({
|
|
@@ -5081,8 +5877,8 @@ function createProofDocument(storyId, storyTitle, acs, dir) {
|
|
|
5081
5877
|
storyTitle,
|
|
5082
5878
|
acceptanceCriteria: criteria
|
|
5083
5879
|
});
|
|
5084
|
-
const proofPath =
|
|
5085
|
-
|
|
5880
|
+
const proofPath = join18(verificationDir, `${storyId}-proof.md`);
|
|
5881
|
+
writeFileSync12(proofPath, content, "utf-8");
|
|
5086
5882
|
return proofPath;
|
|
5087
5883
|
}
|
|
5088
5884
|
function runShowboatVerify(proofPath) {
|
|
@@ -5134,7 +5930,7 @@ function closeBeadsIssue(storyId, dir) {
|
|
|
5134
5930
|
}
|
|
5135
5931
|
|
|
5136
5932
|
// src/modules/verify/parser.ts
|
|
5137
|
-
import { existsSync as
|
|
5933
|
+
import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
|
|
5138
5934
|
var UI_KEYWORDS = [
|
|
5139
5935
|
"agent-browser",
|
|
5140
5936
|
"screenshot",
|
|
@@ -5204,12 +6000,12 @@ function classifyAC(description) {
|
|
|
5204
6000
|
return "general";
|
|
5205
6001
|
}
|
|
5206
6002
|
function parseStoryACs(storyFilePath) {
|
|
5207
|
-
if (!
|
|
6003
|
+
if (!existsSync22(storyFilePath)) {
|
|
5208
6004
|
throw new Error(
|
|
5209
6005
|
`Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
|
|
5210
6006
|
);
|
|
5211
6007
|
}
|
|
5212
|
-
const content =
|
|
6008
|
+
const content = readFileSync18(storyFilePath, "utf-8");
|
|
5213
6009
|
const lines = content.split("\n");
|
|
5214
6010
|
let acSectionStart = -1;
|
|
5215
6011
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -5301,7 +6097,7 @@ function parseObservabilityGaps(proofContent) {
|
|
|
5301
6097
|
|
|
5302
6098
|
// src/modules/observability/analyzer.ts
|
|
5303
6099
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5304
|
-
import { join as
|
|
6100
|
+
import { join as join19 } from "path";
|
|
5305
6101
|
var DEFAULT_RULES_DIR = "patches/observability/";
|
|
5306
6102
|
var DEFAULT_TIMEOUT = 6e4;
|
|
5307
6103
|
var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
|
|
@@ -5335,7 +6131,7 @@ function analyze(projectDir, config) {
|
|
|
5335
6131
|
}
|
|
5336
6132
|
const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
|
|
5337
6133
|
const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
|
|
5338
|
-
const fullRulesDir =
|
|
6134
|
+
const fullRulesDir = join19(projectDir, rulesDir);
|
|
5339
6135
|
const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
|
|
5340
6136
|
if (!rawResult.success) {
|
|
5341
6137
|
return fail2(rawResult.error);
|
|
@@ -5419,8 +6215,8 @@ function normalizeSeverity(severity) {
|
|
|
5419
6215
|
}
|
|
5420
6216
|
|
|
5421
6217
|
// src/modules/observability/coverage.ts
|
|
5422
|
-
import { readFileSync as
|
|
5423
|
-
import { join as
|
|
6218
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync2, existsSync as existsSync23 } from "fs";
|
|
6219
|
+
import { join as join20 } from "path";
|
|
5424
6220
|
var STATE_FILE2 = "sprint-state.json";
|
|
5425
6221
|
var DEFAULT_STATIC_TARGET = 80;
|
|
5426
6222
|
function defaultCoverageState() {
|
|
@@ -5436,12 +6232,12 @@ function defaultCoverageState() {
|
|
|
5436
6232
|
};
|
|
5437
6233
|
}
|
|
5438
6234
|
function readStateFile(projectDir) {
|
|
5439
|
-
const fp =
|
|
5440
|
-
if (!
|
|
6235
|
+
const fp = join20(projectDir, STATE_FILE2);
|
|
6236
|
+
if (!existsSync23(fp)) {
|
|
5441
6237
|
return ok2({});
|
|
5442
6238
|
}
|
|
5443
6239
|
try {
|
|
5444
|
-
const raw =
|
|
6240
|
+
const raw = readFileSync19(fp, "utf-8");
|
|
5445
6241
|
const parsed = JSON.parse(raw);
|
|
5446
6242
|
return ok2(parsed);
|
|
5447
6243
|
} catch (err) {
|
|
@@ -5508,8 +6304,8 @@ function parseGapArray(raw) {
|
|
|
5508
6304
|
}
|
|
5509
6305
|
|
|
5510
6306
|
// src/modules/observability/runtime-coverage.ts
|
|
5511
|
-
import { readFileSync as
|
|
5512
|
-
import { join as
|
|
6307
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, renameSync as renameSync3, existsSync as existsSync24 } from "fs";
|
|
6308
|
+
import { join as join21 } from "path";
|
|
5513
6309
|
|
|
5514
6310
|
// src/modules/observability/coverage-gate.ts
|
|
5515
6311
|
var DEFAULT_STATIC_TARGET2 = 80;
|
|
@@ -5551,8 +6347,8 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
|
|
|
5551
6347
|
|
|
5552
6348
|
// src/modules/observability/runtime-validator.ts
|
|
5553
6349
|
import { execSync as execSync3 } from "child_process";
|
|
5554
|
-
import { readdirSync as
|
|
5555
|
-
import { join as
|
|
6350
|
+
import { readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
6351
|
+
import { join as join22 } from "path";
|
|
5556
6352
|
var DEFAULT_CONFIG = {
|
|
5557
6353
|
testCommand: "npm test",
|
|
5558
6354
|
otlpEndpoint: "http://localhost:4318",
|
|
@@ -5679,11 +6475,11 @@ function mapEventsToModules(events, projectDir, modules) {
|
|
|
5679
6475
|
});
|
|
5680
6476
|
}
|
|
5681
6477
|
function discoverModules(projectDir) {
|
|
5682
|
-
const srcDir =
|
|
6478
|
+
const srcDir = join22(projectDir, "src");
|
|
5683
6479
|
try {
|
|
5684
|
-
return
|
|
6480
|
+
return readdirSync5(srcDir).filter((name) => {
|
|
5685
6481
|
try {
|
|
5686
|
-
return statSync2(
|
|
6482
|
+
return statSync2(join22(srcDir, name)).isDirectory();
|
|
5687
6483
|
} catch {
|
|
5688
6484
|
return false;
|
|
5689
6485
|
}
|
|
@@ -5712,7 +6508,7 @@ function parseLogEvents(text) {
|
|
|
5712
6508
|
|
|
5713
6509
|
// src/modules/verify/browser.ts
|
|
5714
6510
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5715
|
-
import { existsSync as
|
|
6511
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
|
|
5716
6512
|
|
|
5717
6513
|
// src/modules/verify/validation-ac-fr.ts
|
|
5718
6514
|
var FR_ACS = [
|
|
@@ -6350,8 +7146,8 @@ function getACById(id) {
|
|
|
6350
7146
|
|
|
6351
7147
|
// src/modules/verify/validation-runner.ts
|
|
6352
7148
|
import { execSync as execSync4 } from "child_process";
|
|
6353
|
-
import { writeFileSync as
|
|
6354
|
-
import { join as
|
|
7149
|
+
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync7 } from "fs";
|
|
7150
|
+
import { join as join23, dirname as dirname5 } from "path";
|
|
6355
7151
|
var MAX_VALIDATION_ATTEMPTS = 10;
|
|
6356
7152
|
var AC_COMMAND_TIMEOUT_MS = 3e4;
|
|
6357
7153
|
var VAL_KEY_PREFIX = "val-";
|
|
@@ -6460,7 +7256,7 @@ function executeValidationAC(ac) {
|
|
|
6460
7256
|
function createFixStory(ac, error) {
|
|
6461
7257
|
try {
|
|
6462
7258
|
const storyKey = `val-fix-${ac.id}`;
|
|
6463
|
-
const storyPath =
|
|
7259
|
+
const storyPath = join23(
|
|
6464
7260
|
process.cwd(),
|
|
6465
7261
|
"_bmad-output",
|
|
6466
7262
|
"implementation-artifacts",
|
|
@@ -6504,7 +7300,7 @@ function createFixStory(ac, error) {
|
|
|
6504
7300
|
""
|
|
6505
7301
|
].join("\n");
|
|
6506
7302
|
mkdirSync7(dirname5(storyPath), { recursive: true });
|
|
6507
|
-
|
|
7303
|
+
writeFileSync15(storyPath, markdown, "utf-8");
|
|
6508
7304
|
return ok2(storyKey);
|
|
6509
7305
|
} catch (err) {
|
|
6510
7306
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6830,8 +7626,8 @@ function runValidationCycle() {
|
|
|
6830
7626
|
|
|
6831
7627
|
// src/modules/verify/env.ts
|
|
6832
7628
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
6833
|
-
import { existsSync as
|
|
6834
|
-
import { join as
|
|
7629
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync22, cpSync, rmSync, statSync as statSync3 } from "fs";
|
|
7630
|
+
import { join as join24, basename as basename4 } from "path";
|
|
6835
7631
|
import { createHash } from "crypto";
|
|
6836
7632
|
var IMAGE_TAG = "codeharness-verify";
|
|
6837
7633
|
var STORY_DIR = "_bmad-output/implementation-artifacts";
|
|
@@ -6844,20 +7640,20 @@ function isValidStoryKey(storyKey) {
|
|
|
6844
7640
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
6845
7641
|
}
|
|
6846
7642
|
function computeDistHash(projectDir) {
|
|
6847
|
-
const distDir =
|
|
6848
|
-
if (!
|
|
7643
|
+
const distDir = join24(projectDir, "dist");
|
|
7644
|
+
if (!existsSync26(distDir)) return null;
|
|
6849
7645
|
const hash = createHash("sha256");
|
|
6850
7646
|
const files = collectFiles(distDir).sort();
|
|
6851
7647
|
for (const file of files) {
|
|
6852
7648
|
hash.update(file.slice(distDir.length));
|
|
6853
|
-
hash.update(
|
|
7649
|
+
hash.update(readFileSync22(file));
|
|
6854
7650
|
}
|
|
6855
7651
|
return hash.digest("hex");
|
|
6856
7652
|
}
|
|
6857
7653
|
function collectFiles(dir) {
|
|
6858
7654
|
const results = [];
|
|
6859
|
-
for (const entry of
|
|
6860
|
-
const fullPath =
|
|
7655
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
7656
|
+
const fullPath = join24(dir, entry.name);
|
|
6861
7657
|
if (entry.isDirectory()) {
|
|
6862
7658
|
results.push(...collectFiles(fullPath));
|
|
6863
7659
|
} else {
|
|
@@ -6884,10 +7680,13 @@ function storeDistHash(projectDir, hash) {
|
|
|
6884
7680
|
}
|
|
6885
7681
|
}
|
|
6886
7682
|
function detectProjectType(projectDir) {
|
|
6887
|
-
const
|
|
7683
|
+
const allStacks = detectStacks(projectDir);
|
|
7684
|
+
const rootDetection = allStacks.find((s) => s.dir === ".");
|
|
7685
|
+
const stack = rootDetection ? rootDetection.stack : null;
|
|
6888
7686
|
if (stack === "nodejs") return "nodejs";
|
|
6889
7687
|
if (stack === "python") return "python";
|
|
6890
|
-
if (
|
|
7688
|
+
if (stack === "rust") return "rust";
|
|
7689
|
+
if (existsSync26(join24(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
6891
7690
|
return "generic";
|
|
6892
7691
|
}
|
|
6893
7692
|
function buildVerifyImage(options = {}) {
|
|
@@ -6897,7 +7696,7 @@ function buildVerifyImage(options = {}) {
|
|
|
6897
7696
|
}
|
|
6898
7697
|
const projectType = detectProjectType(projectDir);
|
|
6899
7698
|
const currentHash = computeDistHash(projectDir);
|
|
6900
|
-
if (projectType === "generic" || projectType === "plugin") {
|
|
7699
|
+
if (projectType === "generic" || projectType === "plugin" || projectType === "rust") {
|
|
6901
7700
|
} else if (!currentHash) {
|
|
6902
7701
|
throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
|
|
6903
7702
|
}
|
|
@@ -6912,10 +7711,12 @@ function buildVerifyImage(options = {}) {
|
|
|
6912
7711
|
buildNodeImage(projectDir);
|
|
6913
7712
|
} else if (projectType === "python") {
|
|
6914
7713
|
buildPythonImage(projectDir);
|
|
7714
|
+
} else if (projectType === "rust") {
|
|
7715
|
+
buildSimpleImage(projectDir, "rust", 3e5);
|
|
6915
7716
|
} else if (projectType === "plugin") {
|
|
6916
7717
|
buildPluginImage(projectDir);
|
|
6917
7718
|
} else {
|
|
6918
|
-
|
|
7719
|
+
buildSimpleImage(projectDir, "generic");
|
|
6919
7720
|
}
|
|
6920
7721
|
if (currentHash) {
|
|
6921
7722
|
storeDistHash(projectDir, currentHash);
|
|
@@ -6931,12 +7732,12 @@ function buildNodeImage(projectDir) {
|
|
|
6931
7732
|
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
6932
7733
|
if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
6933
7734
|
const tarballName = basename4(lastLine);
|
|
6934
|
-
const tarballPath =
|
|
6935
|
-
const buildContext =
|
|
7735
|
+
const tarballPath = join24("/tmp", tarballName);
|
|
7736
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6936
7737
|
mkdirSync8(buildContext, { recursive: true });
|
|
6937
7738
|
try {
|
|
6938
|
-
cpSync(tarballPath,
|
|
6939
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
7739
|
+
cpSync(tarballPath, join24(buildContext, tarballName));
|
|
7740
|
+
cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
|
|
6940
7741
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
|
|
6941
7742
|
cwd: buildContext,
|
|
6942
7743
|
stdio: "pipe",
|
|
@@ -6948,17 +7749,17 @@ function buildNodeImage(projectDir) {
|
|
|
6948
7749
|
}
|
|
6949
7750
|
}
|
|
6950
7751
|
function buildPythonImage(projectDir) {
|
|
6951
|
-
const distDir =
|
|
6952
|
-
const distFiles =
|
|
7752
|
+
const distDir = join24(projectDir, "dist");
|
|
7753
|
+
const distFiles = readdirSync6(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
|
|
6953
7754
|
if (distFiles.length === 0) {
|
|
6954
7755
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
6955
7756
|
}
|
|
6956
7757
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
6957
|
-
const buildContext =
|
|
7758
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6958
7759
|
mkdirSync8(buildContext, { recursive: true });
|
|
6959
7760
|
try {
|
|
6960
|
-
cpSync(
|
|
6961
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
7761
|
+
cpSync(join24(distDir, distFile), join24(buildContext, distFile));
|
|
7762
|
+
cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
|
|
6962
7763
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
|
|
6963
7764
|
cwd: buildContext,
|
|
6964
7765
|
stdio: "pipe",
|
|
@@ -6973,19 +7774,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
6973
7774
|
if (!isValidStoryKey(storyKey)) {
|
|
6974
7775
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
6975
7776
|
}
|
|
6976
|
-
const storyFile =
|
|
6977
|
-
if (!
|
|
7777
|
+
const storyFile = join24(root, STORY_DIR, `${storyKey}.md`);
|
|
7778
|
+
if (!existsSync26(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
|
|
6978
7779
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
6979
|
-
if (
|
|
7780
|
+
if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
6980
7781
|
mkdirSync8(workspace, { recursive: true });
|
|
6981
|
-
cpSync(storyFile,
|
|
6982
|
-
const readmePath =
|
|
6983
|
-
if (
|
|
6984
|
-
const docsDir =
|
|
6985
|
-
if (
|
|
6986
|
-
cpSync(docsDir,
|
|
6987
|
-
}
|
|
6988
|
-
mkdirSync8(
|
|
7782
|
+
cpSync(storyFile, join24(workspace, "story.md"));
|
|
7783
|
+
const readmePath = join24(root, "README.md");
|
|
7784
|
+
if (existsSync26(readmePath)) cpSync(readmePath, join24(workspace, "README.md"));
|
|
7785
|
+
const docsDir = join24(root, "docs");
|
|
7786
|
+
if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
7787
|
+
cpSync(docsDir, join24(workspace, "docs"), { recursive: true });
|
|
7788
|
+
}
|
|
7789
|
+
mkdirSync8(join24(workspace, "verification"), { recursive: true });
|
|
6989
7790
|
return workspace;
|
|
6990
7791
|
}
|
|
6991
7792
|
function checkVerifyEnv() {
|
|
@@ -7027,7 +7828,7 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7027
7828
|
}
|
|
7028
7829
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7029
7830
|
const containerName = `codeharness-verify-${storyKey}`;
|
|
7030
|
-
if (
|
|
7831
|
+
if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
7031
7832
|
try {
|
|
7032
7833
|
execFileSync11("docker", ["stop", containerName], { stdio: "pipe", timeout: 15e3 });
|
|
7033
7834
|
} catch {
|
|
@@ -7038,18 +7839,18 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7038
7839
|
}
|
|
7039
7840
|
}
|
|
7040
7841
|
function buildPluginImage(projectDir) {
|
|
7041
|
-
const buildContext =
|
|
7842
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7042
7843
|
mkdirSync8(buildContext, { recursive: true });
|
|
7043
7844
|
try {
|
|
7044
|
-
const pluginDir =
|
|
7045
|
-
cpSync(pluginDir,
|
|
7845
|
+
const pluginDir = join24(projectDir, ".claude-plugin");
|
|
7846
|
+
cpSync(pluginDir, join24(buildContext, ".claude-plugin"), { recursive: true });
|
|
7046
7847
|
for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
|
|
7047
|
-
const src =
|
|
7048
|
-
if (
|
|
7049
|
-
cpSync(src,
|
|
7848
|
+
const src = join24(projectDir, dir);
|
|
7849
|
+
if (existsSync26(src) && statSync3(src).isDirectory()) {
|
|
7850
|
+
cpSync(src, join24(buildContext, dir), { recursive: true });
|
|
7050
7851
|
}
|
|
7051
7852
|
}
|
|
7052
|
-
cpSync(resolveDockerfileTemplate(projectDir, "generic"),
|
|
7853
|
+
cpSync(resolveDockerfileTemplate(projectDir, "generic"), join24(buildContext, "Dockerfile"));
|
|
7053
7854
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
7054
7855
|
cwd: buildContext,
|
|
7055
7856
|
stdio: "pipe",
|
|
@@ -7059,27 +7860,31 @@ function buildPluginImage(projectDir) {
|
|
|
7059
7860
|
rmSync(buildContext, { recursive: true, force: true });
|
|
7060
7861
|
}
|
|
7061
7862
|
}
|
|
7062
|
-
function
|
|
7063
|
-
const buildContext =
|
|
7863
|
+
function buildSimpleImage(projectDir, variant, timeout = 12e4) {
|
|
7864
|
+
const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7064
7865
|
mkdirSync8(buildContext, { recursive: true });
|
|
7065
7866
|
try {
|
|
7066
|
-
cpSync(resolveDockerfileTemplate(projectDir,
|
|
7867
|
+
cpSync(resolveDockerfileTemplate(projectDir, variant), join24(buildContext, "Dockerfile"));
|
|
7067
7868
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
7068
7869
|
cwd: buildContext,
|
|
7069
7870
|
stdio: "pipe",
|
|
7070
|
-
timeout
|
|
7871
|
+
timeout
|
|
7071
7872
|
});
|
|
7072
7873
|
} finally {
|
|
7073
7874
|
rmSync(buildContext, { recursive: true, force: true });
|
|
7074
7875
|
}
|
|
7075
7876
|
}
|
|
7877
|
+
var DOCKERFILE_VARIANTS = {
|
|
7878
|
+
generic: "Dockerfile.verify.generic",
|
|
7879
|
+
rust: "Dockerfile.verify.rust"
|
|
7880
|
+
};
|
|
7076
7881
|
function resolveDockerfileTemplate(projectDir, variant) {
|
|
7077
|
-
const filename = variant
|
|
7078
|
-
const local =
|
|
7079
|
-
if (
|
|
7882
|
+
const filename = (variant && DOCKERFILE_VARIANTS[variant]) ?? "Dockerfile.verify";
|
|
7883
|
+
const local = join24(projectDir, "templates", filename);
|
|
7884
|
+
if (existsSync26(local)) return local;
|
|
7080
7885
|
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
7081
|
-
const pkg =
|
|
7082
|
-
if (
|
|
7886
|
+
const pkg = join24(pkgDir, "templates", filename);
|
|
7887
|
+
if (existsSync26(pkg)) return pkg;
|
|
7083
7888
|
throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
|
|
7084
7889
|
}
|
|
7085
7890
|
function dockerImageExists(tag) {
|
|
@@ -7154,8 +7959,8 @@ function verifyRetro(opts, isJson, root) {
|
|
|
7154
7959
|
return;
|
|
7155
7960
|
}
|
|
7156
7961
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
7157
|
-
const retroPath =
|
|
7158
|
-
if (!
|
|
7962
|
+
const retroPath = join25(root, STORY_DIR2, retroFile);
|
|
7963
|
+
if (!existsSync27(retroPath)) {
|
|
7159
7964
|
if (isJson) {
|
|
7160
7965
|
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
7161
7966
|
} else {
|
|
@@ -7172,7 +7977,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
7172
7977
|
warn(`Failed to update sprint status: ${message}`);
|
|
7173
7978
|
}
|
|
7174
7979
|
if (isJson) {
|
|
7175
|
-
jsonOutput({ status: "ok", epic: epicNum, retroFile:
|
|
7980
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join25(STORY_DIR2, retroFile) });
|
|
7176
7981
|
} else {
|
|
7177
7982
|
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
7178
7983
|
}
|
|
@@ -7183,8 +7988,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7183
7988
|
process.exitCode = 1;
|
|
7184
7989
|
return;
|
|
7185
7990
|
}
|
|
7186
|
-
const readmePath =
|
|
7187
|
-
if (!
|
|
7991
|
+
const readmePath = join25(root, "README.md");
|
|
7992
|
+
if (!existsSync27(readmePath)) {
|
|
7188
7993
|
if (isJson) {
|
|
7189
7994
|
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
7190
7995
|
} else {
|
|
@@ -7193,8 +7998,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7193
7998
|
process.exitCode = 1;
|
|
7194
7999
|
return;
|
|
7195
8000
|
}
|
|
7196
|
-
const storyFilePath =
|
|
7197
|
-
if (!
|
|
8001
|
+
const storyFilePath = join25(root, STORY_DIR2, `${storyId}.md`);
|
|
8002
|
+
if (!existsSync27(storyFilePath)) {
|
|
7198
8003
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
7199
8004
|
process.exitCode = 1;
|
|
7200
8005
|
return;
|
|
@@ -7234,8 +8039,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7234
8039
|
return;
|
|
7235
8040
|
}
|
|
7236
8041
|
const storyTitle = extractStoryTitle(storyFilePath);
|
|
7237
|
-
const expectedProofPath =
|
|
7238
|
-
const proofPath =
|
|
8042
|
+
const expectedProofPath = join25(root, "verification", `${storyId}-proof.md`);
|
|
8043
|
+
const proofPath = existsSync27(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
|
|
7239
8044
|
const proofQuality = validateProofQuality(proofPath);
|
|
7240
8045
|
if (!proofQuality.passed) {
|
|
7241
8046
|
if (isJson) {
|
|
@@ -7278,7 +8083,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7278
8083
|
let observabilityGapCount = 0;
|
|
7279
8084
|
let runtimeCoveragePercent = 0;
|
|
7280
8085
|
try {
|
|
7281
|
-
const proofContent =
|
|
8086
|
+
const proofContent = readFileSync24(proofPath, "utf-8");
|
|
7282
8087
|
const gapResult = parseObservabilityGaps(proofContent);
|
|
7283
8088
|
observabilityGapCount = gapResult.gapCount;
|
|
7284
8089
|
runtimeCoveragePercent = gapResult.totalACs === 0 ? 0 : gapResult.coveredCount / gapResult.totalACs * 100;
|
|
@@ -7340,7 +8145,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
7340
8145
|
}
|
|
7341
8146
|
function extractStoryTitle(filePath) {
|
|
7342
8147
|
try {
|
|
7343
|
-
const content =
|
|
8148
|
+
const content = readFileSync24(filePath, "utf-8");
|
|
7344
8149
|
const match = /^#\s+(.+)$/m.exec(content);
|
|
7345
8150
|
return match ? match[1] : "Unknown Story";
|
|
7346
8151
|
} catch {
|
|
@@ -7349,14 +8154,14 @@ function extractStoryTitle(filePath) {
|
|
|
7349
8154
|
}
|
|
7350
8155
|
|
|
7351
8156
|
// src/lib/onboard-checks.ts
|
|
7352
|
-
import { existsSync as
|
|
7353
|
-
import { join as
|
|
8157
|
+
import { existsSync as existsSync29 } from "fs";
|
|
8158
|
+
import { join as join27, dirname as dirname6 } from "path";
|
|
7354
8159
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7355
8160
|
|
|
7356
8161
|
// src/lib/coverage.ts
|
|
7357
8162
|
import { execSync as execSync6 } from "child_process";
|
|
7358
|
-
import { existsSync as
|
|
7359
|
-
import { join as
|
|
8163
|
+
import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
|
|
8164
|
+
import { join as join26 } from "path";
|
|
7360
8165
|
function detectCoverageTool(dir) {
|
|
7361
8166
|
const baseDir = dir ?? process.cwd();
|
|
7362
8167
|
const stateHint = getStateToolHint(baseDir);
|
|
@@ -7367,6 +8172,27 @@ function detectCoverageTool(dir) {
|
|
|
7367
8172
|
if (stack === "python") {
|
|
7368
8173
|
return detectPythonCoverageTool(baseDir);
|
|
7369
8174
|
}
|
|
8175
|
+
if (stack === "rust") {
|
|
8176
|
+
try {
|
|
8177
|
+
execSync6("cargo tarpaulin --version", { stdio: "pipe", timeout: 1e4 });
|
|
8178
|
+
} catch {
|
|
8179
|
+
warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
|
|
8180
|
+
return { tool: "unknown", runCommand: "", reportFormat: "" };
|
|
8181
|
+
}
|
|
8182
|
+
const cargoPath = join26(baseDir, "Cargo.toml");
|
|
8183
|
+
let isWorkspace = false;
|
|
8184
|
+
try {
|
|
8185
|
+
const cargoContent = readFileSync25(cargoPath, "utf-8");
|
|
8186
|
+
isWorkspace = /^\[workspace\]/m.test(cargoContent);
|
|
8187
|
+
} catch {
|
|
8188
|
+
}
|
|
8189
|
+
const wsFlag = isWorkspace ? " --workspace" : "";
|
|
8190
|
+
return {
|
|
8191
|
+
tool: "cargo-tarpaulin",
|
|
8192
|
+
runCommand: `cargo tarpaulin --out json --output-dir coverage/${wsFlag}`,
|
|
8193
|
+
reportFormat: "tarpaulin-json"
|
|
8194
|
+
};
|
|
8195
|
+
}
|
|
7370
8196
|
warn("No recognized stack detected \u2014 cannot determine coverage tool");
|
|
7371
8197
|
return { tool: "unknown", runCommand: "", reportFormat: "" };
|
|
7372
8198
|
}
|
|
@@ -7379,16 +8205,16 @@ function getStateToolHint(dir) {
|
|
|
7379
8205
|
}
|
|
7380
8206
|
}
|
|
7381
8207
|
function detectNodeCoverageTool(dir, stateHint) {
|
|
7382
|
-
const hasVitestConfig =
|
|
7383
|
-
const pkgPath =
|
|
8208
|
+
const hasVitestConfig = existsSync28(join26(dir, "vitest.config.ts")) || existsSync28(join26(dir, "vitest.config.js"));
|
|
8209
|
+
const pkgPath = join26(dir, "package.json");
|
|
7384
8210
|
let hasVitestCoverageV8 = false;
|
|
7385
8211
|
let hasVitestCoverageIstanbul = false;
|
|
7386
8212
|
let hasC8 = false;
|
|
7387
8213
|
let hasJest = false;
|
|
7388
8214
|
let pkgScripts = {};
|
|
7389
|
-
if (
|
|
8215
|
+
if (existsSync28(pkgPath)) {
|
|
7390
8216
|
try {
|
|
7391
|
-
const pkg = JSON.parse(
|
|
8217
|
+
const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
|
|
7392
8218
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
7393
8219
|
hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
|
|
7394
8220
|
hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
|
|
@@ -7441,10 +8267,10 @@ function getNodeTestCommand(scripts, runner) {
|
|
|
7441
8267
|
return "npm test";
|
|
7442
8268
|
}
|
|
7443
8269
|
function detectPythonCoverageTool(dir) {
|
|
7444
|
-
const reqPath =
|
|
7445
|
-
if (
|
|
8270
|
+
const reqPath = join26(dir, "requirements.txt");
|
|
8271
|
+
if (existsSync28(reqPath)) {
|
|
7446
8272
|
try {
|
|
7447
|
-
const content =
|
|
8273
|
+
const content = readFileSync25(reqPath, "utf-8");
|
|
7448
8274
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
7449
8275
|
return {
|
|
7450
8276
|
tool: "coverage.py",
|
|
@@ -7455,10 +8281,10 @@ function detectPythonCoverageTool(dir) {
|
|
|
7455
8281
|
} catch {
|
|
7456
8282
|
}
|
|
7457
8283
|
}
|
|
7458
|
-
const pyprojectPath =
|
|
7459
|
-
if (
|
|
8284
|
+
const pyprojectPath = join26(dir, "pyproject.toml");
|
|
8285
|
+
if (existsSync28(pyprojectPath)) {
|
|
7460
8286
|
try {
|
|
7461
|
-
const content =
|
|
8287
|
+
const content = readFileSync25(pyprojectPath, "utf-8");
|
|
7462
8288
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
7463
8289
|
return {
|
|
7464
8290
|
tool: "coverage.py",
|
|
@@ -7531,6 +8357,9 @@ function parseCoverageReport(dir, format) {
|
|
|
7531
8357
|
if (format === "coverage-py-json") {
|
|
7532
8358
|
return parsePythonCoverage(dir);
|
|
7533
8359
|
}
|
|
8360
|
+
if (format === "tarpaulin-json") {
|
|
8361
|
+
return parseTarpaulinCoverage(dir);
|
|
8362
|
+
}
|
|
7534
8363
|
return 0;
|
|
7535
8364
|
}
|
|
7536
8365
|
function parseVitestCoverage(dir) {
|
|
@@ -7540,7 +8369,7 @@ function parseVitestCoverage(dir) {
|
|
|
7540
8369
|
return 0;
|
|
7541
8370
|
}
|
|
7542
8371
|
try {
|
|
7543
|
-
const report = JSON.parse(
|
|
8372
|
+
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
7544
8373
|
return report.total?.statements?.pct ?? 0;
|
|
7545
8374
|
} catch {
|
|
7546
8375
|
warn("Failed to parse coverage report");
|
|
@@ -7548,19 +8377,33 @@ function parseVitestCoverage(dir) {
|
|
|
7548
8377
|
}
|
|
7549
8378
|
}
|
|
7550
8379
|
function parsePythonCoverage(dir) {
|
|
7551
|
-
const reportPath =
|
|
7552
|
-
if (!
|
|
8380
|
+
const reportPath = join26(dir, "coverage.json");
|
|
8381
|
+
if (!existsSync28(reportPath)) {
|
|
7553
8382
|
warn("Coverage report not found at coverage.json");
|
|
7554
8383
|
return 0;
|
|
7555
8384
|
}
|
|
7556
8385
|
try {
|
|
7557
|
-
const report = JSON.parse(
|
|
8386
|
+
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
7558
8387
|
return report.totals?.percent_covered ?? 0;
|
|
7559
8388
|
} catch {
|
|
7560
8389
|
warn("Failed to parse coverage report");
|
|
7561
8390
|
return 0;
|
|
7562
8391
|
}
|
|
7563
8392
|
}
|
|
8393
|
+
function parseTarpaulinCoverage(dir) {
|
|
8394
|
+
const reportPath = join26(dir, "coverage", "tarpaulin-report.json");
|
|
8395
|
+
if (!existsSync28(reportPath)) {
|
|
8396
|
+
warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
|
|
8397
|
+
return 0;
|
|
8398
|
+
}
|
|
8399
|
+
try {
|
|
8400
|
+
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
8401
|
+
return report.coverage ?? 0;
|
|
8402
|
+
} catch {
|
|
8403
|
+
warn("Failed to parse tarpaulin coverage report");
|
|
8404
|
+
return 0;
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
7564
8407
|
function parseTestCounts(output) {
|
|
7565
8408
|
const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
|
|
7566
8409
|
if (vitestMatch) {
|
|
@@ -7576,6 +8419,18 @@ function parseTestCounts(output) {
|
|
|
7576
8419
|
failCount: jestMatch[1] ? parseInt(jestMatch[1], 10) : 0
|
|
7577
8420
|
};
|
|
7578
8421
|
}
|
|
8422
|
+
const cargoRegex = /test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed/gi;
|
|
8423
|
+
let cargoMatch = cargoRegex.exec(output);
|
|
8424
|
+
if (cargoMatch) {
|
|
8425
|
+
let totalPass = 0;
|
|
8426
|
+
let totalFail = 0;
|
|
8427
|
+
while (cargoMatch) {
|
|
8428
|
+
totalPass += parseInt(cargoMatch[1], 10);
|
|
8429
|
+
totalFail += parseInt(cargoMatch[2], 10);
|
|
8430
|
+
cargoMatch = cargoRegex.exec(output);
|
|
8431
|
+
}
|
|
8432
|
+
return { passCount: totalPass, failCount: totalFail };
|
|
8433
|
+
}
|
|
7579
8434
|
const pytestMatch = /(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/i.exec(output);
|
|
7580
8435
|
if (pytestMatch) {
|
|
7581
8436
|
return {
|
|
@@ -7655,7 +8510,7 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7655
8510
|
}
|
|
7656
8511
|
let report;
|
|
7657
8512
|
try {
|
|
7658
|
-
report = JSON.parse(
|
|
8513
|
+
report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
7659
8514
|
} catch {
|
|
7660
8515
|
warn("Failed to parse coverage-summary.json");
|
|
7661
8516
|
return { floor, violations: [], totalFiles: 0 };
|
|
@@ -7685,11 +8540,11 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7685
8540
|
}
|
|
7686
8541
|
function findCoverageSummary(dir) {
|
|
7687
8542
|
const candidates = [
|
|
7688
|
-
|
|
7689
|
-
|
|
8543
|
+
join26(dir, "coverage", "coverage-summary.json"),
|
|
8544
|
+
join26(dir, "src", "coverage", "coverage-summary.json")
|
|
7690
8545
|
];
|
|
7691
8546
|
for (const p of candidates) {
|
|
7692
|
-
if (
|
|
8547
|
+
if (existsSync28(p)) return p;
|
|
7693
8548
|
}
|
|
7694
8549
|
return null;
|
|
7695
8550
|
}
|
|
@@ -7714,7 +8569,7 @@ function printCoverageOutput(result, evaluation) {
|
|
|
7714
8569
|
// src/lib/onboard-checks.ts
|
|
7715
8570
|
function checkHarnessInitialized(dir) {
|
|
7716
8571
|
const statePath2 = getStatePath(dir ?? process.cwd());
|
|
7717
|
-
return { ok:
|
|
8572
|
+
return { ok: existsSync29(statePath2) };
|
|
7718
8573
|
}
|
|
7719
8574
|
function checkBmadInstalled(dir) {
|
|
7720
8575
|
return { ok: isBmadInstalled(dir) };
|
|
@@ -7722,8 +8577,8 @@ function checkBmadInstalled(dir) {
|
|
|
7722
8577
|
function checkHooksRegistered(dir) {
|
|
7723
8578
|
const __filename = fileURLToPath3(import.meta.url);
|
|
7724
8579
|
const __dirname2 = dirname6(__filename);
|
|
7725
|
-
const hooksPath =
|
|
7726
|
-
return { ok:
|
|
8580
|
+
const hooksPath = join27(__dirname2, "..", "..", "hooks", "hooks.json");
|
|
8581
|
+
return { ok: existsSync29(hooksPath) };
|
|
7727
8582
|
}
|
|
7728
8583
|
function runPreconditions(dir) {
|
|
7729
8584
|
const harnessCheck = checkHarnessInitialized(dir);
|
|
@@ -8361,8 +9216,8 @@ function getBeadsData() {
|
|
|
8361
9216
|
}
|
|
8362
9217
|
|
|
8363
9218
|
// src/modules/audit/dimensions.ts
|
|
8364
|
-
import { existsSync as
|
|
8365
|
-
import { join as
|
|
9219
|
+
import { existsSync as existsSync30, readdirSync as readdirSync7 } from "fs";
|
|
9220
|
+
import { join as join28 } from "path";
|
|
8366
9221
|
function gap(dimension, description, suggestedFix) {
|
|
8367
9222
|
return { dimension, description, suggestedFix };
|
|
8368
9223
|
}
|
|
@@ -8474,15 +9329,15 @@ function checkDocumentation(projectDir) {
|
|
|
8474
9329
|
function checkVerification(projectDir) {
|
|
8475
9330
|
try {
|
|
8476
9331
|
const gaps = [];
|
|
8477
|
-
const sprintPath =
|
|
8478
|
-
if (!
|
|
8479
|
-
const vDir =
|
|
9332
|
+
const sprintPath = join28(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
9333
|
+
if (!existsSync30(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
9334
|
+
const vDir = join28(projectDir, "verification");
|
|
8480
9335
|
let proofCount = 0, totalChecked = 0;
|
|
8481
|
-
if (
|
|
9336
|
+
if (existsSync30(vDir)) {
|
|
8482
9337
|
for (const file of readdirSafe(vDir)) {
|
|
8483
9338
|
if (!file.endsWith("-proof.md")) continue;
|
|
8484
9339
|
totalChecked++;
|
|
8485
|
-
const r = parseProof(
|
|
9340
|
+
const r = parseProof(join28(vDir, file));
|
|
8486
9341
|
if (isOk(r) && r.data.passed) {
|
|
8487
9342
|
proofCount++;
|
|
8488
9343
|
} else {
|
|
@@ -8504,30 +9359,21 @@ function checkVerification(projectDir) {
|
|
|
8504
9359
|
}
|
|
8505
9360
|
function checkInfrastructure(projectDir) {
|
|
8506
9361
|
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";
|
|
9362
|
+
const result = validateDockerfile(projectDir);
|
|
9363
|
+
if (!result.success) {
|
|
9364
|
+
const err = result.error;
|
|
9365
|
+
if (err.includes("No Dockerfile")) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
|
|
9366
|
+
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")]);
|
|
9367
|
+
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")]);
|
|
9368
|
+
return dimOk("infrastructure", "fail", "validation failed", [gap("infrastructure", err, "Fix Dockerfile validation errors")]);
|
|
9369
|
+
}
|
|
9370
|
+
const gaps = result.data.gaps.map((g) => gap("infrastructure", g.description, g.suggestedFix));
|
|
9371
|
+
for (const w of result.data.warnings) {
|
|
9372
|
+
gaps.push(gap("infrastructure", w, "Provide the missing configuration file"));
|
|
9373
|
+
}
|
|
9374
|
+
const issueCount = gaps.length;
|
|
9375
|
+
const status = issueCount > 0 ? "warn" : "pass";
|
|
9376
|
+
const metric = issueCount > 0 ? `Dockerfile exists (${issueCount} issue${issueCount !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
8531
9377
|
return dimOk("infrastructure", status, metric, gaps);
|
|
8532
9378
|
} catch (err) {
|
|
8533
9379
|
return dimCatch("infrastructure", err);
|
|
@@ -8535,7 +9381,7 @@ function checkInfrastructure(projectDir) {
|
|
|
8535
9381
|
}
|
|
8536
9382
|
function readdirSafe(dir) {
|
|
8537
9383
|
try {
|
|
8538
|
-
return
|
|
9384
|
+
return readdirSync7(dir);
|
|
8539
9385
|
} catch {
|
|
8540
9386
|
return [];
|
|
8541
9387
|
}
|
|
@@ -8568,8 +9414,8 @@ function formatAuditJson(result) {
|
|
|
8568
9414
|
}
|
|
8569
9415
|
|
|
8570
9416
|
// src/modules/audit/fix-generator.ts
|
|
8571
|
-
import { existsSync as
|
|
8572
|
-
import { join as
|
|
9417
|
+
import { existsSync as existsSync31, writeFileSync as writeFileSync16, mkdirSync as mkdirSync9 } from "fs";
|
|
9418
|
+
import { join as join29, dirname as dirname7 } from "path";
|
|
8573
9419
|
function buildStoryKey(gap2, index) {
|
|
8574
9420
|
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
8575
9421
|
return `audit-fix-${safeDimension}-${index}`;
|
|
@@ -8601,7 +9447,7 @@ function generateFixStories(auditResult) {
|
|
|
8601
9447
|
const stories = [];
|
|
8602
9448
|
let created = 0;
|
|
8603
9449
|
let skipped = 0;
|
|
8604
|
-
const artifactsDir =
|
|
9450
|
+
const artifactsDir = join29(
|
|
8605
9451
|
process.cwd(),
|
|
8606
9452
|
"_bmad-output",
|
|
8607
9453
|
"implementation-artifacts"
|
|
@@ -8610,8 +9456,8 @@ function generateFixStories(auditResult) {
|
|
|
8610
9456
|
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
8611
9457
|
const gap2 = dimension.gaps[i];
|
|
8612
9458
|
const key = buildStoryKey(gap2, i + 1);
|
|
8613
|
-
const filePath =
|
|
8614
|
-
if (
|
|
9459
|
+
const filePath = join29(artifactsDir, `${key}.md`);
|
|
9460
|
+
if (existsSync31(filePath)) {
|
|
8615
9461
|
stories.push({
|
|
8616
9462
|
key,
|
|
8617
9463
|
filePath,
|
|
@@ -8624,7 +9470,7 @@ function generateFixStories(auditResult) {
|
|
|
8624
9470
|
}
|
|
8625
9471
|
const markdown = buildStoryMarkdown(gap2, key);
|
|
8626
9472
|
mkdirSync9(dirname7(filePath), { recursive: true });
|
|
8627
|
-
|
|
9473
|
+
writeFileSync16(filePath, markdown, "utf-8");
|
|
8628
9474
|
stories.push({ key, filePath, gap: gap2, skipped: false });
|
|
8629
9475
|
created++;
|
|
8630
9476
|
}
|
|
@@ -8800,8 +9646,8 @@ function registerOnboardCommand(program) {
|
|
|
8800
9646
|
}
|
|
8801
9647
|
|
|
8802
9648
|
// src/commands/teardown.ts
|
|
8803
|
-
import { existsSync as
|
|
8804
|
-
import { join as
|
|
9649
|
+
import { existsSync as existsSync32, unlinkSync as unlinkSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
|
|
9650
|
+
import { join as join30 } from "path";
|
|
8805
9651
|
function buildDefaultResult() {
|
|
8806
9652
|
return {
|
|
8807
9653
|
status: "ok",
|
|
@@ -8904,16 +9750,16 @@ function registerTeardownCommand(program) {
|
|
|
8904
9750
|
info("Docker stack: not running, skipping");
|
|
8905
9751
|
}
|
|
8906
9752
|
}
|
|
8907
|
-
const composeFilePath =
|
|
8908
|
-
if (
|
|
9753
|
+
const composeFilePath = join30(projectDir, composeFile);
|
|
9754
|
+
if (existsSync32(composeFilePath)) {
|
|
8909
9755
|
unlinkSync2(composeFilePath);
|
|
8910
9756
|
result.removed.push(composeFile);
|
|
8911
9757
|
if (!isJson) {
|
|
8912
9758
|
ok(`Removed: ${composeFile}`);
|
|
8913
9759
|
}
|
|
8914
9760
|
}
|
|
8915
|
-
const otelConfigPath =
|
|
8916
|
-
if (
|
|
9761
|
+
const otelConfigPath = join30(projectDir, "otel-collector-config.yaml");
|
|
9762
|
+
if (existsSync32(otelConfigPath)) {
|
|
8917
9763
|
unlinkSync2(otelConfigPath);
|
|
8918
9764
|
result.removed.push("otel-collector-config.yaml");
|
|
8919
9765
|
if (!isJson) {
|
|
@@ -8923,8 +9769,8 @@ function registerTeardownCommand(program) {
|
|
|
8923
9769
|
}
|
|
8924
9770
|
let patchesRemoved = 0;
|
|
8925
9771
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
8926
|
-
const filePath =
|
|
8927
|
-
if (!
|
|
9772
|
+
const filePath = join30(projectDir, "_bmad", relativePath);
|
|
9773
|
+
if (!existsSync32(filePath)) {
|
|
8928
9774
|
continue;
|
|
8929
9775
|
}
|
|
8930
9776
|
try {
|
|
@@ -8943,9 +9789,9 @@ function registerTeardownCommand(program) {
|
|
|
8943
9789
|
info("BMAD patches: none found");
|
|
8944
9790
|
}
|
|
8945
9791
|
}
|
|
8946
|
-
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
8947
|
-
const pkgPath =
|
|
8948
|
-
if (
|
|
9792
|
+
if (state.otlp?.enabled && (state.stacks?.includes("nodejs") ?? state.stack === "nodejs")) {
|
|
9793
|
+
const pkgPath = join30(projectDir, "package.json");
|
|
9794
|
+
if (existsSync32(pkgPath)) {
|
|
8949
9795
|
try {
|
|
8950
9796
|
const raw = readFileSync26(pkgPath, "utf-8");
|
|
8951
9797
|
const pkg = JSON.parse(raw);
|
|
@@ -8961,7 +9807,7 @@ function registerTeardownCommand(program) {
|
|
|
8961
9807
|
for (const key of keysToRemove) {
|
|
8962
9808
|
delete scripts[key];
|
|
8963
9809
|
}
|
|
8964
|
-
|
|
9810
|
+
writeFileSync17(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
8965
9811
|
result.otlp_cleaned = true;
|
|
8966
9812
|
if (!isJson) {
|
|
8967
9813
|
ok("OTLP: removed instrumented scripts from package.json");
|
|
@@ -8987,8 +9833,8 @@ function registerTeardownCommand(program) {
|
|
|
8987
9833
|
}
|
|
8988
9834
|
}
|
|
8989
9835
|
}
|
|
8990
|
-
const harnessDir =
|
|
8991
|
-
if (
|
|
9836
|
+
const harnessDir = join30(projectDir, ".harness");
|
|
9837
|
+
if (existsSync32(harnessDir)) {
|
|
8992
9838
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
8993
9839
|
result.removed.push(".harness/");
|
|
8994
9840
|
if (!isJson) {
|
|
@@ -8996,7 +9842,7 @@ function registerTeardownCommand(program) {
|
|
|
8996
9842
|
}
|
|
8997
9843
|
}
|
|
8998
9844
|
const statePath2 = getStatePath(projectDir);
|
|
8999
|
-
if (
|
|
9845
|
+
if (existsSync32(statePath2)) {
|
|
9000
9846
|
unlinkSync2(statePath2);
|
|
9001
9847
|
result.removed.push(".claude/codeharness.local.md");
|
|
9002
9848
|
if (!isJson) {
|
|
@@ -9740,8 +10586,8 @@ function registerQueryCommand(program) {
|
|
|
9740
10586
|
}
|
|
9741
10587
|
|
|
9742
10588
|
// src/commands/retro-import.ts
|
|
9743
|
-
import { existsSync as
|
|
9744
|
-
import { join as
|
|
10589
|
+
import { existsSync as existsSync33, readFileSync as readFileSync27 } from "fs";
|
|
10590
|
+
import { join as join31 } from "path";
|
|
9745
10591
|
|
|
9746
10592
|
// src/lib/retro-parser.ts
|
|
9747
10593
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -9910,8 +10756,8 @@ function registerRetroImportCommand(program) {
|
|
|
9910
10756
|
return;
|
|
9911
10757
|
}
|
|
9912
10758
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
9913
|
-
const retroPath =
|
|
9914
|
-
if (!
|
|
10759
|
+
const retroPath = join31(root, STORY_DIR3, retroFile);
|
|
10760
|
+
if (!existsSync33(retroPath)) {
|
|
9915
10761
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
9916
10762
|
process.exitCode = 1;
|
|
9917
10763
|
return;
|
|
@@ -10299,23 +11145,23 @@ function registerVerifyEnvCommand(program) {
|
|
|
10299
11145
|
}
|
|
10300
11146
|
|
|
10301
11147
|
// src/commands/retry.ts
|
|
10302
|
-
import { join as
|
|
11148
|
+
import { join as join33 } from "path";
|
|
10303
11149
|
|
|
10304
11150
|
// src/lib/retry-state.ts
|
|
10305
|
-
import { existsSync as
|
|
10306
|
-
import { join as
|
|
11151
|
+
import { existsSync as existsSync34, readFileSync as readFileSync28, writeFileSync as writeFileSync18 } from "fs";
|
|
11152
|
+
import { join as join32 } from "path";
|
|
10307
11153
|
var RETRIES_FILE = ".story_retries";
|
|
10308
11154
|
var FLAGGED_FILE = ".flagged_stories";
|
|
10309
11155
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
10310
11156
|
function retriesPath(dir) {
|
|
10311
|
-
return
|
|
11157
|
+
return join32(dir, RETRIES_FILE);
|
|
10312
11158
|
}
|
|
10313
11159
|
function flaggedPath(dir) {
|
|
10314
|
-
return
|
|
11160
|
+
return join32(dir, FLAGGED_FILE);
|
|
10315
11161
|
}
|
|
10316
11162
|
function readRetries(dir) {
|
|
10317
11163
|
const filePath = retriesPath(dir);
|
|
10318
|
-
if (!
|
|
11164
|
+
if (!existsSync34(filePath)) {
|
|
10319
11165
|
return /* @__PURE__ */ new Map();
|
|
10320
11166
|
}
|
|
10321
11167
|
const raw = readFileSync28(filePath, "utf-8");
|
|
@@ -10340,7 +11186,7 @@ function writeRetries(dir, retries) {
|
|
|
10340
11186
|
for (const [key, count] of retries) {
|
|
10341
11187
|
lines.push(`${key}=${count}`);
|
|
10342
11188
|
}
|
|
10343
|
-
|
|
11189
|
+
writeFileSync18(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
10344
11190
|
}
|
|
10345
11191
|
function resetRetry(dir, storyKey) {
|
|
10346
11192
|
if (storyKey) {
|
|
@@ -10355,7 +11201,7 @@ function resetRetry(dir, storyKey) {
|
|
|
10355
11201
|
}
|
|
10356
11202
|
function readFlaggedStories(dir) {
|
|
10357
11203
|
const filePath = flaggedPath(dir);
|
|
10358
|
-
if (!
|
|
11204
|
+
if (!existsSync34(filePath)) {
|
|
10359
11205
|
return [];
|
|
10360
11206
|
}
|
|
10361
11207
|
const raw = readFileSync28(filePath, "utf-8");
|
|
@@ -10363,7 +11209,7 @@ function readFlaggedStories(dir) {
|
|
|
10363
11209
|
}
|
|
10364
11210
|
function writeFlaggedStories(dir, stories) {
|
|
10365
11211
|
const filePath = flaggedPath(dir);
|
|
10366
|
-
|
|
11212
|
+
writeFileSync18(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
|
|
10367
11213
|
}
|
|
10368
11214
|
function removeFlaggedStory(dir, key) {
|
|
10369
11215
|
const stories = readFlaggedStories(dir);
|
|
@@ -10383,7 +11229,7 @@ function registerRetryCommand(program) {
|
|
|
10383
11229
|
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
11230
|
const opts = cmd.optsWithGlobals();
|
|
10385
11231
|
const isJson = opts.json === true;
|
|
10386
|
-
const dir =
|
|
11232
|
+
const dir = join33(process.cwd(), RALPH_SUBDIR);
|
|
10387
11233
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
10388
11234
|
if (isJson) {
|
|
10389
11235
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -10792,7 +11638,7 @@ function registerAuditCommand(program) {
|
|
|
10792
11638
|
}
|
|
10793
11639
|
|
|
10794
11640
|
// src/index.ts
|
|
10795
|
-
var VERSION = true ? "0.
|
|
11641
|
+
var VERSION = true ? "0.24.0" : "0.0.0-dev";
|
|
10796
11642
|
function createProgram() {
|
|
10797
11643
|
const program = new Command();
|
|
10798
11644
|
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 +11672,6 @@ if (!process.env["VITEST"]) {
|
|
|
10826
11672
|
program.parse(process.argv);
|
|
10827
11673
|
}
|
|
10828
11674
|
export {
|
|
10829
|
-
createProgram
|
|
11675
|
+
createProgram,
|
|
11676
|
+
parseStreamLine
|
|
10830
11677
|
};
|