codeharness 0.19.4 → 0.20.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 +292 -10
- package/package.json +1 -1
- package/patches/AGENTS.md +14 -1
- package/patches/observability/__tests__/catch-without-logging.ts +36 -0
- package/patches/observability/__tests__/error-path-no-log.ts +47 -0
- package/patches/observability/__tests__/function-no-debug-log.ts +54 -0
- package/patches/observability/catch-without-logging.ts +36 -0
- package/patches/observability/catch-without-logging.yaml +35 -0
- package/patches/observability/error-path-no-log.ts +47 -0
- package/patches/observability/error-path-no-log.yaml +68 -0
- package/patches/observability/function-no-debug-log.ts +54 -0
- package/patches/observability/function-no-debug-log.yaml +114 -0
- package/ralph/ralph.sh +156 -9
package/dist/index.js
CHANGED
|
@@ -1941,7 +1941,7 @@ async function scaffoldDocs(opts) {
|
|
|
1941
1941
|
}
|
|
1942
1942
|
|
|
1943
1943
|
// src/modules/infra/init-project.ts
|
|
1944
|
-
var HARNESS_VERSION = true ? "0.
|
|
1944
|
+
var HARNESS_VERSION = true ? "0.20.0" : "0.0.0-dev";
|
|
1945
1945
|
function failResult(opts, error) {
|
|
1946
1946
|
return {
|
|
1947
1947
|
status: "fail",
|
|
@@ -2693,6 +2693,153 @@ function generateRalphPrompt(config) {
|
|
|
2693
2693
|
return prompt;
|
|
2694
2694
|
}
|
|
2695
2695
|
|
|
2696
|
+
// src/lib/dashboard-formatter.ts
|
|
2697
|
+
function formatElapsed(ms) {
|
|
2698
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
2699
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
2700
|
+
const remainingSeconds = totalSeconds % 60;
|
|
2701
|
+
return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSeconds}s`;
|
|
2702
|
+
}
|
|
2703
|
+
var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
|
|
2704
|
+
var ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
2705
|
+
var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
|
|
2706
|
+
var ERROR_LINE = /\[ERROR\]\s+(.+)/;
|
|
2707
|
+
var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit.*flagging/;
|
|
2708
|
+
var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
|
|
2709
|
+
var INFO_SPRINT = /\[INFO\]\s+Sprint:\s+(.+)/;
|
|
2710
|
+
var INFO_PROGRESS = /\[INFO\]\s+Progress:\s+(.+)/;
|
|
2711
|
+
var INFO_STORY_PHASE = /\[INFO\]\s+Story\s+([\w-]+):\s+(create|dev|review|verify)(?:\s+\((.+)\))?/;
|
|
2712
|
+
var INFO_STORY_AC = /\[INFO\]\s+Story\s+([\w-]+):\s+verify\s+\(AC\s+(.+)\)/;
|
|
2713
|
+
var INFO_NEXT = /\[INFO\]\s+Next up:\s+([\w-]+)/;
|
|
2714
|
+
var DEBUG_LINE = /\[DEBUG\]/;
|
|
2715
|
+
var INFO_NOISE = /\[INFO\]\s+(Plugin:|Starting\s+|Sleeping\s|Capturing\s|Timeout report)/;
|
|
2716
|
+
var LOOP_LINE = /\[LOOP\]\s+(.+)/;
|
|
2717
|
+
var WARN_LINE = /\[WARN\]\s+(.+)/;
|
|
2718
|
+
var SUCCESS_STARTING = /\[SUCCESS\]\s+Ralph loop starting/;
|
|
2719
|
+
var SUCCESS_ALL_DONE = /\[SUCCESS\]\s+All stories complete\.(.+)/;
|
|
2720
|
+
var INFO_SESSION = /\[INFO\]\s+(━━━.*━━━)/;
|
|
2721
|
+
var DashboardFormatter = class {
|
|
2722
|
+
currentStory = null;
|
|
2723
|
+
currentPhase = null;
|
|
2724
|
+
phaseStartTime = null;
|
|
2725
|
+
/**
|
|
2726
|
+
* Parse a raw ralph output line and return formatted dashboard output,
|
|
2727
|
+
* or null to suppress the line.
|
|
2728
|
+
*/
|
|
2729
|
+
formatLine(rawLine) {
|
|
2730
|
+
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
2731
|
+
if (clean.length === 0) return null;
|
|
2732
|
+
if (DEBUG_LINE.test(clean)) return null;
|
|
2733
|
+
if (SUCCESS_STARTING.test(clean)) {
|
|
2734
|
+
return "--- Ralph loop starting ---";
|
|
2735
|
+
}
|
|
2736
|
+
const allDone = SUCCESS_ALL_DONE.exec(clean);
|
|
2737
|
+
if (allDone) {
|
|
2738
|
+
return `\u2713 All stories complete.${allDone[1]}`;
|
|
2739
|
+
}
|
|
2740
|
+
const success = SUCCESS_STORY.exec(clean);
|
|
2741
|
+
if (success) {
|
|
2742
|
+
const key = success[1];
|
|
2743
|
+
const rest = success[2].trim();
|
|
2744
|
+
const formatted = rest ? ` (${rest.replace(/^—\s*/, "")})` : "";
|
|
2745
|
+
this.currentStory = null;
|
|
2746
|
+
this.currentPhase = null;
|
|
2747
|
+
this.phaseStartTime = null;
|
|
2748
|
+
return `\u2713 Story ${key}: DONE${formatted}`;
|
|
2749
|
+
}
|
|
2750
|
+
const acMatch = INFO_STORY_AC.exec(clean);
|
|
2751
|
+
if (acMatch) {
|
|
2752
|
+
this.currentStory = acMatch[1];
|
|
2753
|
+
this.currentPhase = `verify (AC ${acMatch[2]})`;
|
|
2754
|
+
if (this.phaseStartTime === null) {
|
|
2755
|
+
this.phaseStartTime = Date.now();
|
|
2756
|
+
}
|
|
2757
|
+
return null;
|
|
2758
|
+
}
|
|
2759
|
+
const phaseMatch = INFO_STORY_PHASE.exec(clean);
|
|
2760
|
+
if (phaseMatch) {
|
|
2761
|
+
const newStory = phaseMatch[1];
|
|
2762
|
+
const newPhase = phaseMatch[2];
|
|
2763
|
+
if (newStory !== this.currentStory || newPhase !== this.currentPhase) {
|
|
2764
|
+
this.phaseStartTime = Date.now();
|
|
2765
|
+
}
|
|
2766
|
+
this.currentStory = newStory;
|
|
2767
|
+
this.currentPhase = phaseMatch[3] ? `${newPhase} (${phaseMatch[3]})` : newPhase;
|
|
2768
|
+
return null;
|
|
2769
|
+
}
|
|
2770
|
+
const sprint = INFO_SPRINT.exec(clean);
|
|
2771
|
+
if (sprint) {
|
|
2772
|
+
return `\u25C6 Sprint: ${sprint[1]}`;
|
|
2773
|
+
}
|
|
2774
|
+
const progress = INFO_PROGRESS.exec(clean);
|
|
2775
|
+
if (progress) {
|
|
2776
|
+
return `\u25C6 Progress: ${progress[1]}`;
|
|
2777
|
+
}
|
|
2778
|
+
const next = INFO_NEXT.exec(clean);
|
|
2779
|
+
if (next) {
|
|
2780
|
+
return `\u25C6 Next: ${next[1]}`;
|
|
2781
|
+
}
|
|
2782
|
+
if (INFO_NOISE.test(clean)) return null;
|
|
2783
|
+
const session = INFO_SESSION.exec(clean);
|
|
2784
|
+
if (session) {
|
|
2785
|
+
return session[1];
|
|
2786
|
+
}
|
|
2787
|
+
const errorMatch = ERROR_LINE.exec(clean);
|
|
2788
|
+
if (errorMatch) {
|
|
2789
|
+
const msg = errorMatch[1].trim();
|
|
2790
|
+
if (msg.startsWith("\u2717")) {
|
|
2791
|
+
return `\u2717 ${msg.replace(/^\u2717\s*/, "")}`;
|
|
2792
|
+
}
|
|
2793
|
+
return `\u2717 ${msg}`;
|
|
2794
|
+
}
|
|
2795
|
+
const retryExceeded = WARN_STORY_RETRY.exec(clean);
|
|
2796
|
+
if (retryExceeded) {
|
|
2797
|
+
return `\u2717 Story ${retryExceeded[1]}: FAIL \u2014 exceeded retry limit`;
|
|
2798
|
+
}
|
|
2799
|
+
const retrying = WARN_STORY_RETRYING.exec(clean);
|
|
2800
|
+
if (retrying) {
|
|
2801
|
+
return `\u25C6 Story ${retrying[1]}: retry ${retrying[2]}/${retrying[3]}`;
|
|
2802
|
+
}
|
|
2803
|
+
const loop = LOOP_LINE.exec(clean);
|
|
2804
|
+
if (loop) {
|
|
2805
|
+
return `\u25C6 ${loop[1]}`;
|
|
2806
|
+
}
|
|
2807
|
+
const warn2 = WARN_LINE.exec(clean);
|
|
2808
|
+
if (warn2) {
|
|
2809
|
+
return `! ${warn2[1]}`;
|
|
2810
|
+
}
|
|
2811
|
+
if (clean.startsWith("[")) {
|
|
2812
|
+
return clean;
|
|
2813
|
+
}
|
|
2814
|
+
return clean;
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Returns the current ticker line showing active story progress,
|
|
2818
|
+
* or null if no story is active.
|
|
2819
|
+
*/
|
|
2820
|
+
getTickerLine() {
|
|
2821
|
+
if (!this.currentStory || !this.currentPhase || this.phaseStartTime === null) {
|
|
2822
|
+
return null;
|
|
2823
|
+
}
|
|
2824
|
+
const elapsed = formatElapsed(Date.now() - this.phaseStartTime);
|
|
2825
|
+
return `\u25C6 ${this.currentStory} \u2014 ${this.currentPhase} (elapsed ${elapsed})`;
|
|
2826
|
+
}
|
|
2827
|
+
/** Get current tracked story (for testing) */
|
|
2828
|
+
getCurrentStory() {
|
|
2829
|
+
return this.currentStory;
|
|
2830
|
+
}
|
|
2831
|
+
/** Get current tracked phase (for testing) */
|
|
2832
|
+
getCurrentPhase() {
|
|
2833
|
+
return this.currentPhase;
|
|
2834
|
+
}
|
|
2835
|
+
/** Reset internal state */
|
|
2836
|
+
reset() {
|
|
2837
|
+
this.currentStory = null;
|
|
2838
|
+
this.currentPhase = null;
|
|
2839
|
+
this.phaseStartTime = null;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2696
2843
|
// src/commands/run.ts
|
|
2697
2844
|
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
2698
2845
|
var STORY_KEY_PATTERN = /^\d+-\d+-/;
|
|
@@ -2743,16 +2890,13 @@ function buildSpawnArgs(opts) {
|
|
|
2743
2890
|
if (opts.maxStoryRetries !== void 0) {
|
|
2744
2891
|
args.push("--max-story-retries", String(opts.maxStoryRetries));
|
|
2745
2892
|
}
|
|
2746
|
-
if (opts.live) {
|
|
2747
|
-
args.push("--live");
|
|
2748
|
-
}
|
|
2749
2893
|
if (opts.reset) {
|
|
2750
2894
|
args.push("--reset");
|
|
2751
2895
|
}
|
|
2752
2896
|
return args;
|
|
2753
2897
|
}
|
|
2754
2898
|
function registerRunCommand(program) {
|
|
2755
|
-
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("--
|
|
2899
|
+
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) => {
|
|
2756
2900
|
const globalOpts = cmd.optsWithGlobals();
|
|
2757
2901
|
const isJson = !!globalOpts.json;
|
|
2758
2902
|
const outputOpts = { json: isJson };
|
|
@@ -2820,7 +2964,7 @@ function registerRunCommand(program) {
|
|
|
2820
2964
|
timeout,
|
|
2821
2965
|
iterationTimeout,
|
|
2822
2966
|
calls,
|
|
2823
|
-
|
|
2967
|
+
quiet: options.quiet,
|
|
2824
2968
|
maxStoryRetries,
|
|
2825
2969
|
reset: options.reset
|
|
2826
2970
|
});
|
|
@@ -2829,16 +2973,47 @@ function registerRunCommand(program) {
|
|
|
2829
2973
|
env.CLAUDE_OUTPUT_FORMAT = "json";
|
|
2830
2974
|
}
|
|
2831
2975
|
try {
|
|
2976
|
+
const quiet = options.quiet;
|
|
2832
2977
|
const child = spawn("bash", args, {
|
|
2833
|
-
stdio: "inherit",
|
|
2978
|
+
stdio: quiet ? "ignore" : ["inherit", "pipe", "pipe"],
|
|
2834
2979
|
cwd: projectDir,
|
|
2835
2980
|
env
|
|
2836
2981
|
});
|
|
2982
|
+
let tickerInterval = null;
|
|
2983
|
+
if (!quiet && child.stdout && child.stderr) {
|
|
2984
|
+
const formatter = new DashboardFormatter();
|
|
2985
|
+
const makeFilterOutput = () => {
|
|
2986
|
+
let partial = "";
|
|
2987
|
+
return (data) => {
|
|
2988
|
+
const text = partial + data.toString();
|
|
2989
|
+
const parts = text.split("\n");
|
|
2990
|
+
partial = parts.pop() ?? "";
|
|
2991
|
+
for (const line of parts) {
|
|
2992
|
+
if (line.trim().length === 0) continue;
|
|
2993
|
+
const formatted = formatter.formatLine(line);
|
|
2994
|
+
if (formatted !== null) {
|
|
2995
|
+
process.stdout.write(`\r\x1B[K${formatted}
|
|
2996
|
+
`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
};
|
|
3001
|
+
child.stdout.on("data", makeFilterOutput());
|
|
3002
|
+
child.stderr.on("data", makeFilterOutput());
|
|
3003
|
+
tickerInterval = setInterval(() => {
|
|
3004
|
+
const tickerLine = formatter.getTickerLine();
|
|
3005
|
+
if (tickerLine) {
|
|
3006
|
+
process.stdout.write(`\r\x1B[K${tickerLine}`);
|
|
3007
|
+
}
|
|
3008
|
+
}, 1e4);
|
|
3009
|
+
}
|
|
2837
3010
|
const exitCode = await new Promise((resolve3, reject) => {
|
|
2838
3011
|
child.on("error", (err) => {
|
|
3012
|
+
if (tickerInterval) clearInterval(tickerInterval);
|
|
2839
3013
|
reject(err);
|
|
2840
3014
|
});
|
|
2841
3015
|
child.on("close", (code) => {
|
|
3016
|
+
if (tickerInterval) clearInterval(tickerInterval);
|
|
2842
3017
|
resolve3(code ?? 1);
|
|
2843
3018
|
});
|
|
2844
3019
|
});
|
|
@@ -4740,7 +4915,11 @@ function defaultState() {
|
|
|
4740
4915
|
iteration: 0,
|
|
4741
4916
|
cost: 0,
|
|
4742
4917
|
completed: [],
|
|
4743
|
-
failed: []
|
|
4918
|
+
failed: [],
|
|
4919
|
+
currentStory: null,
|
|
4920
|
+
currentPhase: null,
|
|
4921
|
+
lastAction: null,
|
|
4922
|
+
acProgress: null
|
|
4744
4923
|
},
|
|
4745
4924
|
actionItems: []
|
|
4746
4925
|
};
|
|
@@ -4764,7 +4943,16 @@ function getSprintState() {
|
|
|
4764
4943
|
try {
|
|
4765
4944
|
const raw = readFileSync15(fp, "utf-8");
|
|
4766
4945
|
const parsed = JSON.parse(raw);
|
|
4767
|
-
|
|
4946
|
+
const defaults = defaultState();
|
|
4947
|
+
const run = parsed.run;
|
|
4948
|
+
const state = {
|
|
4949
|
+
...parsed,
|
|
4950
|
+
run: {
|
|
4951
|
+
...defaults.run,
|
|
4952
|
+
...run
|
|
4953
|
+
}
|
|
4954
|
+
};
|
|
4955
|
+
return ok2(state);
|
|
4768
4956
|
} catch (err) {
|
|
4769
4957
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4770
4958
|
return fail2(`Failed to read sprint state: ${msg}`);
|
|
@@ -4791,6 +4979,43 @@ function computeSprintCounts(stories) {
|
|
|
4791
4979
|
}
|
|
4792
4980
|
return { total, done, failed, blocked, inProgress };
|
|
4793
4981
|
}
|
|
4982
|
+
function updateRunProgress(update) {
|
|
4983
|
+
const stateResult = getSprintState();
|
|
4984
|
+
if (!stateResult.success) {
|
|
4985
|
+
return fail2(stateResult.error);
|
|
4986
|
+
}
|
|
4987
|
+
const current = stateResult.data;
|
|
4988
|
+
const updatedRun = {
|
|
4989
|
+
...current.run,
|
|
4990
|
+
...update.currentStory !== void 0 && { currentStory: update.currentStory },
|
|
4991
|
+
...update.currentPhase !== void 0 && { currentPhase: update.currentPhase },
|
|
4992
|
+
...update.lastAction !== void 0 && { lastAction: update.lastAction },
|
|
4993
|
+
...update.acProgress !== void 0 && { acProgress: update.acProgress }
|
|
4994
|
+
};
|
|
4995
|
+
const updatedState = {
|
|
4996
|
+
...current,
|
|
4997
|
+
run: updatedRun
|
|
4998
|
+
};
|
|
4999
|
+
return writeStateAtomic(updatedState);
|
|
5000
|
+
}
|
|
5001
|
+
function clearRunProgress() {
|
|
5002
|
+
const stateResult = getSprintState();
|
|
5003
|
+
if (!stateResult.success) {
|
|
5004
|
+
return fail2(stateResult.error);
|
|
5005
|
+
}
|
|
5006
|
+
const current = stateResult.data;
|
|
5007
|
+
const updatedState = {
|
|
5008
|
+
...current,
|
|
5009
|
+
run: {
|
|
5010
|
+
...current.run,
|
|
5011
|
+
currentStory: null,
|
|
5012
|
+
currentPhase: null,
|
|
5013
|
+
lastAction: null,
|
|
5014
|
+
acProgress: null
|
|
5015
|
+
}
|
|
5016
|
+
};
|
|
5017
|
+
return writeStateAtomic(updatedState);
|
|
5018
|
+
}
|
|
4794
5019
|
|
|
4795
5020
|
// src/modules/sprint/selector.ts
|
|
4796
5021
|
var MAX_STORY_ATTEMPTS = 10;
|
|
@@ -5382,6 +5607,12 @@ function writeStateAtomic2(state) {
|
|
|
5382
5607
|
function computeSprintCounts2(stories) {
|
|
5383
5608
|
return computeSprintCounts(stories);
|
|
5384
5609
|
}
|
|
5610
|
+
function updateRunProgress2(update) {
|
|
5611
|
+
return updateRunProgress(update);
|
|
5612
|
+
}
|
|
5613
|
+
function clearRunProgress2() {
|
|
5614
|
+
return clearRunProgress();
|
|
5615
|
+
}
|
|
5385
5616
|
|
|
5386
5617
|
// src/modules/verify/validation-runner.ts
|
|
5387
5618
|
var MAX_VALIDATION_ATTEMPTS = 10;
|
|
@@ -10191,8 +10422,58 @@ function outputHuman(p, cycles, allPassed) {
|
|
|
10191
10422
|
fail("RELEASE GATE: FAIL");
|
|
10192
10423
|
}
|
|
10193
10424
|
|
|
10425
|
+
// src/commands/progress.ts
|
|
10426
|
+
function registerProgressCommand(program) {
|
|
10427
|
+
program.command("progress").description("Update live run progress in sprint-state.json").option("--story <key>", "Set run.currentStory").option("--phase <phase>", "Set run.currentPhase (create|dev|review|verify)").option("--action <text>", "Set run.lastAction").option("--ac-progress <progress>", 'Set run.acProgress (e.g., "4/12")').option("--clear", "Clear all run progress fields to null").action((opts, cmd) => {
|
|
10428
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
10429
|
+
const isJson = globalOpts.json;
|
|
10430
|
+
const validPhases = ["create", "dev", "review", "verify"];
|
|
10431
|
+
if (opts.phase !== void 0 && !validPhases.includes(opts.phase)) {
|
|
10432
|
+
fail(`Invalid phase "${opts.phase}". Must be one of: ${validPhases.join(", ")}`, { json: isJson });
|
|
10433
|
+
process.exitCode = 1;
|
|
10434
|
+
return;
|
|
10435
|
+
}
|
|
10436
|
+
if (opts.clear) {
|
|
10437
|
+
const result2 = clearRunProgress2();
|
|
10438
|
+
if (result2.success) {
|
|
10439
|
+
if (isJson) {
|
|
10440
|
+
jsonOutput({ status: "ok", cleared: true });
|
|
10441
|
+
} else {
|
|
10442
|
+
ok("Run progress cleared");
|
|
10443
|
+
}
|
|
10444
|
+
} else {
|
|
10445
|
+
fail(result2.error, { json: isJson });
|
|
10446
|
+
process.exitCode = 1;
|
|
10447
|
+
}
|
|
10448
|
+
return;
|
|
10449
|
+
}
|
|
10450
|
+
const update = {
|
|
10451
|
+
...opts.story !== void 0 && { currentStory: opts.story },
|
|
10452
|
+
...opts.phase !== void 0 && { currentPhase: opts.phase },
|
|
10453
|
+
...opts.action !== void 0 && { lastAction: opts.action },
|
|
10454
|
+
...opts.acProgress !== void 0 && { acProgress: opts.acProgress }
|
|
10455
|
+
};
|
|
10456
|
+
if (Object.keys(update).length === 0) {
|
|
10457
|
+
fail("No progress fields specified. Use --story, --phase, --action, --ac-progress, or --clear.", { json: isJson });
|
|
10458
|
+
process.exitCode = 1;
|
|
10459
|
+
return;
|
|
10460
|
+
}
|
|
10461
|
+
const result = updateRunProgress2(update);
|
|
10462
|
+
if (result.success) {
|
|
10463
|
+
if (isJson) {
|
|
10464
|
+
jsonOutput({ status: "ok", updated: update });
|
|
10465
|
+
} else {
|
|
10466
|
+
ok("Run progress updated");
|
|
10467
|
+
}
|
|
10468
|
+
} else {
|
|
10469
|
+
fail(result.error, { json: isJson });
|
|
10470
|
+
process.exitCode = 1;
|
|
10471
|
+
}
|
|
10472
|
+
});
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10194
10475
|
// src/index.ts
|
|
10195
|
-
var VERSION = true ? "0.
|
|
10476
|
+
var VERSION = true ? "0.20.0" : "0.0.0-dev";
|
|
10196
10477
|
function createProgram() {
|
|
10197
10478
|
const program = new Command();
|
|
10198
10479
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -10216,6 +10497,7 @@ function createProgram() {
|
|
|
10216
10497
|
registerTimeoutReportCommand(program);
|
|
10217
10498
|
registerValidateStateCommand(program);
|
|
10218
10499
|
registerValidateCommand(program);
|
|
10500
|
+
registerProgressCommand(program);
|
|
10219
10501
|
return program;
|
|
10220
10502
|
}
|
|
10221
10503
|
if (!process.env["VITEST"]) {
|
package/package.json
CHANGED
package/patches/AGENTS.md
CHANGED
|
@@ -17,7 +17,20 @@ patches/
|
|
|
17
17
|
retro/enforcement.md — Retrospective quality metrics
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Subdirectories map to BMAD workflow roles.
|
|
20
|
+
Subdirectories map to BMAD workflow roles (or analysis categories like `observability/`).
|
|
21
|
+
|
|
22
|
+
## Observability Module (`observability/`)
|
|
23
|
+
|
|
24
|
+
Semgrep YAML rules for static analysis of observability gaps. Each `.yaml` file is a standalone Semgrep config — no build step required. Deleting a rule file removes that check.
|
|
25
|
+
|
|
26
|
+
**Rules:**
|
|
27
|
+
- `catch-without-logging.yaml` — Detects catch blocks without error/warn logging (WARNING)
|
|
28
|
+
- `function-no-debug-log.yaml` — Detects functions without debug/info logging (INFO)
|
|
29
|
+
- `error-path-no-log.yaml` — Detects error paths (throw/return err) without preceding log (WARNING)
|
|
30
|
+
|
|
31
|
+
**Testing:** `semgrep --test patches/observability/` runs annotated test fixtures (`.ts` files alongside rules).
|
|
32
|
+
|
|
33
|
+
**Customization:** Edit YAML rules to add custom logger patterns (e.g., `logger.error(...)` for winston). Rules use `pattern-not` / `pattern-not-inside` to detect absence of logging.
|
|
21
34
|
|
|
22
35
|
## How Patches Work
|
|
23
36
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Test cases for catch-without-logging Semgrep rule
|
|
2
|
+
|
|
3
|
+
// ruleid: catch-without-logging
|
|
4
|
+
try { doSomething(); } catch (e) { /* no logging at all */ }
|
|
5
|
+
|
|
6
|
+
// ruleid: catch-without-logging
|
|
7
|
+
try {
|
|
8
|
+
riskyOperation();
|
|
9
|
+
} catch (err) {
|
|
10
|
+
cleanup();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ok: catch-without-logging
|
|
14
|
+
try { doSomething(); } catch (e) { console.error('failed', e); }
|
|
15
|
+
|
|
16
|
+
// ok: catch-without-logging
|
|
17
|
+
try {
|
|
18
|
+
riskyOperation();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.warn('operation failed', err);
|
|
21
|
+
cleanup();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ok: catch-without-logging
|
|
25
|
+
try {
|
|
26
|
+
riskyOperation();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.error('operation failed', err);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ok: catch-without-logging
|
|
32
|
+
try {
|
|
33
|
+
riskyOperation();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.warn('operation failed', err);
|
|
36
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Test cases for error-path-no-log Semgrep rule
|
|
2
|
+
|
|
3
|
+
function badThrow() {
|
|
4
|
+
// ruleid: error-path-no-log
|
|
5
|
+
throw new Error('something went wrong');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function badReturn() {
|
|
9
|
+
// ruleid: error-path-no-log
|
|
10
|
+
return err('something went wrong');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function goodThrow() {
|
|
14
|
+
// ok: error-path-no-log
|
|
15
|
+
console.error('about to throw');
|
|
16
|
+
throw new Error('something went wrong');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function goodReturn() {
|
|
20
|
+
// ok: error-path-no-log
|
|
21
|
+
console.error('returning error');
|
|
22
|
+
return err('something went wrong');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function goodThrowWithLogger() {
|
|
26
|
+
// ok: error-path-no-log
|
|
27
|
+
logger.error('about to throw');
|
|
28
|
+
throw new Error('something went wrong');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function goodReturnWithLogger() {
|
|
32
|
+
// ok: error-path-no-log
|
|
33
|
+
logger.error('returning error');
|
|
34
|
+
return err('something went wrong');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function goodThrowWithWarn() {
|
|
38
|
+
// ok: error-path-no-log
|
|
39
|
+
console.warn('about to throw');
|
|
40
|
+
throw new Error('something went wrong');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function goodReturnWithLoggerWarn() {
|
|
44
|
+
// ok: error-path-no-log
|
|
45
|
+
logger.warn('returning error');
|
|
46
|
+
return err('something went wrong');
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Test cases for function-no-debug-log Semgrep rule
|
|
2
|
+
|
|
3
|
+
// ruleid: function-no-debug-log
|
|
4
|
+
function processData(input: string) {
|
|
5
|
+
return input.trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ruleid: function-no-debug-log
|
|
9
|
+
function handleRequest(req: any) {
|
|
10
|
+
const result = compute(req);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ruleid: function-no-debug-log
|
|
15
|
+
const transformData = (input: string) => {
|
|
16
|
+
return input.toUpperCase();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ok: function-no-debug-log
|
|
20
|
+
function processDataWithLog(input: string) {
|
|
21
|
+
console.log('processing data', input);
|
|
22
|
+
return input.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ok: function-no-debug-log
|
|
26
|
+
function handleRequestWithDebug(req: any) {
|
|
27
|
+
console.debug('handling request', req);
|
|
28
|
+
const result = compute(req);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ok: function-no-debug-log
|
|
33
|
+
function serviceCall(params: any) {
|
|
34
|
+
logger.debug('service call', params);
|
|
35
|
+
return fetch(params.url);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ok: function-no-debug-log
|
|
39
|
+
function anotherService(params: any) {
|
|
40
|
+
logger.info('another service call', params);
|
|
41
|
+
return fetch(params.url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ok: function-no-debug-log
|
|
45
|
+
const transformWithLog = (input: string) => {
|
|
46
|
+
console.log('transforming', input);
|
|
47
|
+
return input.toUpperCase();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ok: function-no-debug-log
|
|
51
|
+
const arrowWithDebug = (input: string) => {
|
|
52
|
+
logger.debug('arrow function', input);
|
|
53
|
+
return input.toLowerCase();
|
|
54
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Test cases for catch-without-logging Semgrep rule
|
|
2
|
+
|
|
3
|
+
// ruleid: catch-without-logging
|
|
4
|
+
try { doSomething(); } catch (e) { /* no logging at all */ }
|
|
5
|
+
|
|
6
|
+
// ruleid: catch-without-logging
|
|
7
|
+
try {
|
|
8
|
+
riskyOperation();
|
|
9
|
+
} catch (err) {
|
|
10
|
+
cleanup();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ok: catch-without-logging
|
|
14
|
+
try { doSomething(); } catch (e) { console.error('failed', e); }
|
|
15
|
+
|
|
16
|
+
// ok: catch-without-logging
|
|
17
|
+
try {
|
|
18
|
+
riskyOperation();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.warn('operation failed', err);
|
|
21
|
+
cleanup();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ok: catch-without-logging
|
|
25
|
+
try {
|
|
26
|
+
riskyOperation();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.error('operation failed', err);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ok: catch-without-logging
|
|
32
|
+
try {
|
|
33
|
+
riskyOperation();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.warn('operation failed', err);
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: catch-without-logging
|
|
3
|
+
patterns:
|
|
4
|
+
- pattern: |
|
|
5
|
+
try { ... } catch ($ERR) { ... }
|
|
6
|
+
- pattern-not: |
|
|
7
|
+
try { ... } catch ($ERR) {
|
|
8
|
+
...
|
|
9
|
+
console.error(...)
|
|
10
|
+
...
|
|
11
|
+
}
|
|
12
|
+
- pattern-not: |
|
|
13
|
+
try { ... } catch ($ERR) {
|
|
14
|
+
...
|
|
15
|
+
console.warn(...)
|
|
16
|
+
...
|
|
17
|
+
}
|
|
18
|
+
- pattern-not: |
|
|
19
|
+
try { ... } catch ($ERR) {
|
|
20
|
+
...
|
|
21
|
+
logger.error(...)
|
|
22
|
+
...
|
|
23
|
+
}
|
|
24
|
+
- pattern-not: |
|
|
25
|
+
try { ... } catch ($ERR) {
|
|
26
|
+
...
|
|
27
|
+
logger.warn(...)
|
|
28
|
+
...
|
|
29
|
+
}
|
|
30
|
+
message: "Catch block without error logging — observability gap"
|
|
31
|
+
languages: [typescript, javascript]
|
|
32
|
+
severity: WARNING
|
|
33
|
+
metadata:
|
|
34
|
+
category: observability
|
|
35
|
+
cwe: "CWE-778: Insufficient Logging"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Test cases for error-path-no-log Semgrep rule
|
|
2
|
+
|
|
3
|
+
function badThrow() {
|
|
4
|
+
// ruleid: error-path-no-log
|
|
5
|
+
throw new Error('something went wrong');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function badReturn() {
|
|
9
|
+
// ruleid: error-path-no-log
|
|
10
|
+
return err('something went wrong');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function goodThrow() {
|
|
14
|
+
// ok: error-path-no-log
|
|
15
|
+
console.error('about to throw');
|
|
16
|
+
throw new Error('something went wrong');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function goodReturn() {
|
|
20
|
+
// ok: error-path-no-log
|
|
21
|
+
console.error('returning error');
|
|
22
|
+
return err('something went wrong');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function goodThrowWithLogger() {
|
|
26
|
+
// ok: error-path-no-log
|
|
27
|
+
logger.error('about to throw');
|
|
28
|
+
throw new Error('something went wrong');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function goodReturnWithLogger() {
|
|
32
|
+
// ok: error-path-no-log
|
|
33
|
+
logger.error('returning error');
|
|
34
|
+
return err('something went wrong');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function goodThrowWithWarn() {
|
|
38
|
+
// ok: error-path-no-log
|
|
39
|
+
console.warn('about to throw');
|
|
40
|
+
throw new Error('something went wrong');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function goodReturnWithLoggerWarn() {
|
|
44
|
+
// ok: error-path-no-log
|
|
45
|
+
logger.warn('returning error');
|
|
46
|
+
return err('something went wrong');
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: error-path-no-log
|
|
3
|
+
patterns:
|
|
4
|
+
- pattern-either:
|
|
5
|
+
- pattern: throw $ERR;
|
|
6
|
+
- pattern: return err(...);
|
|
7
|
+
- pattern-not-inside: |
|
|
8
|
+
{
|
|
9
|
+
...
|
|
10
|
+
console.error(...)
|
|
11
|
+
...
|
|
12
|
+
throw $ERR;
|
|
13
|
+
}
|
|
14
|
+
- pattern-not-inside: |
|
|
15
|
+
{
|
|
16
|
+
...
|
|
17
|
+
console.warn(...)
|
|
18
|
+
...
|
|
19
|
+
throw $ERR;
|
|
20
|
+
}
|
|
21
|
+
- pattern-not-inside: |
|
|
22
|
+
{
|
|
23
|
+
...
|
|
24
|
+
logger.error(...)
|
|
25
|
+
...
|
|
26
|
+
throw $ERR;
|
|
27
|
+
}
|
|
28
|
+
- pattern-not-inside: |
|
|
29
|
+
{
|
|
30
|
+
...
|
|
31
|
+
logger.warn(...)
|
|
32
|
+
...
|
|
33
|
+
throw $ERR;
|
|
34
|
+
}
|
|
35
|
+
- pattern-not-inside: |
|
|
36
|
+
{
|
|
37
|
+
...
|
|
38
|
+
console.error(...)
|
|
39
|
+
...
|
|
40
|
+
return err(...);
|
|
41
|
+
}
|
|
42
|
+
- pattern-not-inside: |
|
|
43
|
+
{
|
|
44
|
+
...
|
|
45
|
+
console.warn(...)
|
|
46
|
+
...
|
|
47
|
+
return err(...);
|
|
48
|
+
}
|
|
49
|
+
- pattern-not-inside: |
|
|
50
|
+
{
|
|
51
|
+
...
|
|
52
|
+
logger.error(...)
|
|
53
|
+
...
|
|
54
|
+
return err(...);
|
|
55
|
+
}
|
|
56
|
+
- pattern-not-inside: |
|
|
57
|
+
{
|
|
58
|
+
...
|
|
59
|
+
logger.warn(...)
|
|
60
|
+
...
|
|
61
|
+
return err(...);
|
|
62
|
+
}
|
|
63
|
+
message: "Error path without logging — observability gap"
|
|
64
|
+
languages: [typescript, javascript]
|
|
65
|
+
severity: WARNING
|
|
66
|
+
metadata:
|
|
67
|
+
category: observability
|
|
68
|
+
cwe: "CWE-778: Insufficient Logging"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Test cases for function-no-debug-log Semgrep rule
|
|
2
|
+
|
|
3
|
+
// ruleid: function-no-debug-log
|
|
4
|
+
function processData(input: string) {
|
|
5
|
+
return input.trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ruleid: function-no-debug-log
|
|
9
|
+
function handleRequest(req: any) {
|
|
10
|
+
const result = compute(req);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ruleid: function-no-debug-log
|
|
15
|
+
const transformData = (input: string) => {
|
|
16
|
+
return input.toUpperCase();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ok: function-no-debug-log
|
|
20
|
+
function processDataWithLog(input: string) {
|
|
21
|
+
console.log('processing data', input);
|
|
22
|
+
return input.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ok: function-no-debug-log
|
|
26
|
+
function handleRequestWithDebug(req: any) {
|
|
27
|
+
console.debug('handling request', req);
|
|
28
|
+
const result = compute(req);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ok: function-no-debug-log
|
|
33
|
+
function serviceCall(params: any) {
|
|
34
|
+
logger.debug('service call', params);
|
|
35
|
+
return fetch(params.url);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ok: function-no-debug-log
|
|
39
|
+
function anotherService(params: any) {
|
|
40
|
+
logger.info('another service call', params);
|
|
41
|
+
return fetch(params.url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ok: function-no-debug-log
|
|
45
|
+
const transformWithLog = (input: string) => {
|
|
46
|
+
console.log('transforming', input);
|
|
47
|
+
return input.toUpperCase();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ok: function-no-debug-log
|
|
51
|
+
const arrowWithDebug = (input: string) => {
|
|
52
|
+
logger.debug('arrow function', input);
|
|
53
|
+
return input.toLowerCase();
|
|
54
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: function-no-debug-log
|
|
3
|
+
patterns:
|
|
4
|
+
- pattern-either:
|
|
5
|
+
- pattern: |
|
|
6
|
+
function $FUNC(...) { ... }
|
|
7
|
+
- pattern: |
|
|
8
|
+
const $FUNC = (...) => { ... }
|
|
9
|
+
- pattern: |
|
|
10
|
+
let $FUNC = (...) => { ... }
|
|
11
|
+
- pattern: |
|
|
12
|
+
$FUNC(...) { ... }
|
|
13
|
+
- pattern-not: |
|
|
14
|
+
function $FUNC(...) {
|
|
15
|
+
...
|
|
16
|
+
console.log(...)
|
|
17
|
+
...
|
|
18
|
+
}
|
|
19
|
+
- pattern-not: |
|
|
20
|
+
function $FUNC(...) {
|
|
21
|
+
...
|
|
22
|
+
console.debug(...)
|
|
23
|
+
...
|
|
24
|
+
}
|
|
25
|
+
- pattern-not: |
|
|
26
|
+
function $FUNC(...) {
|
|
27
|
+
...
|
|
28
|
+
logger.debug(...)
|
|
29
|
+
...
|
|
30
|
+
}
|
|
31
|
+
- pattern-not: |
|
|
32
|
+
function $FUNC(...) {
|
|
33
|
+
...
|
|
34
|
+
logger.info(...)
|
|
35
|
+
...
|
|
36
|
+
}
|
|
37
|
+
- pattern-not: |
|
|
38
|
+
const $FUNC = (...) => {
|
|
39
|
+
...
|
|
40
|
+
console.log(...)
|
|
41
|
+
...
|
|
42
|
+
}
|
|
43
|
+
- pattern-not: |
|
|
44
|
+
const $FUNC = (...) => {
|
|
45
|
+
...
|
|
46
|
+
console.debug(...)
|
|
47
|
+
...
|
|
48
|
+
}
|
|
49
|
+
- pattern-not: |
|
|
50
|
+
const $FUNC = (...) => {
|
|
51
|
+
...
|
|
52
|
+
logger.debug(...)
|
|
53
|
+
...
|
|
54
|
+
}
|
|
55
|
+
- pattern-not: |
|
|
56
|
+
const $FUNC = (...) => {
|
|
57
|
+
...
|
|
58
|
+
logger.info(...)
|
|
59
|
+
...
|
|
60
|
+
}
|
|
61
|
+
- pattern-not: |
|
|
62
|
+
let $FUNC = (...) => {
|
|
63
|
+
...
|
|
64
|
+
console.log(...)
|
|
65
|
+
...
|
|
66
|
+
}
|
|
67
|
+
- pattern-not: |
|
|
68
|
+
let $FUNC = (...) => {
|
|
69
|
+
...
|
|
70
|
+
console.debug(...)
|
|
71
|
+
...
|
|
72
|
+
}
|
|
73
|
+
- pattern-not: |
|
|
74
|
+
let $FUNC = (...) => {
|
|
75
|
+
...
|
|
76
|
+
logger.debug(...)
|
|
77
|
+
...
|
|
78
|
+
}
|
|
79
|
+
- pattern-not: |
|
|
80
|
+
let $FUNC = (...) => {
|
|
81
|
+
...
|
|
82
|
+
logger.info(...)
|
|
83
|
+
...
|
|
84
|
+
}
|
|
85
|
+
- pattern-not: |
|
|
86
|
+
$FUNC(...) {
|
|
87
|
+
...
|
|
88
|
+
console.log(...)
|
|
89
|
+
...
|
|
90
|
+
}
|
|
91
|
+
- pattern-not: |
|
|
92
|
+
$FUNC(...) {
|
|
93
|
+
...
|
|
94
|
+
console.debug(...)
|
|
95
|
+
...
|
|
96
|
+
}
|
|
97
|
+
- pattern-not: |
|
|
98
|
+
$FUNC(...) {
|
|
99
|
+
...
|
|
100
|
+
logger.debug(...)
|
|
101
|
+
...
|
|
102
|
+
}
|
|
103
|
+
- pattern-not: |
|
|
104
|
+
$FUNC(...) {
|
|
105
|
+
...
|
|
106
|
+
logger.info(...)
|
|
107
|
+
...
|
|
108
|
+
}
|
|
109
|
+
message: "Function without debug/info logging — observability gap"
|
|
110
|
+
languages: [typescript, javascript]
|
|
111
|
+
severity: INFO
|
|
112
|
+
metadata:
|
|
113
|
+
category: observability
|
|
114
|
+
cwe: "CWE-778: Insufficient Logging"
|
package/ralph/ralph.sh
CHANGED
|
@@ -98,6 +98,14 @@ log_status() {
|
|
|
98
98
|
"LOOP") color=$PURPLE ;;
|
|
99
99
|
esac
|
|
100
100
|
|
|
101
|
+
# DEBUG level: log file only, no terminal output
|
|
102
|
+
if [[ "$level" == "DEBUG" ]]; then
|
|
103
|
+
if [[ -n "$LOG_DIR" ]]; then
|
|
104
|
+
echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
|
|
105
|
+
fi
|
|
106
|
+
return
|
|
107
|
+
fi
|
|
108
|
+
|
|
101
109
|
echo -e "${color}[$timestamp] [$level] $message${NC}" >&2
|
|
102
110
|
if [[ -n "$LOG_DIR" ]]; then
|
|
103
111
|
echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
|
|
@@ -444,6 +452,58 @@ detect_story_changes() {
|
|
|
444
452
|
done <<< "$after_snapshot"
|
|
445
453
|
}
|
|
446
454
|
|
|
455
|
+
# ─── Sprint State Progress Polling ─────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
# Previous state tracking for change detection
|
|
458
|
+
PREV_STORY=""
|
|
459
|
+
PREV_PHASE=""
|
|
460
|
+
PREV_AC_PROGRESS=""
|
|
461
|
+
PREV_LAST_ACTION=""
|
|
462
|
+
|
|
463
|
+
# Poll sprint-state.json for progress changes during background execution.
|
|
464
|
+
# Prints structured update lines when progress fields change.
|
|
465
|
+
poll_sprint_state_progress() {
|
|
466
|
+
local state_file="sprint-state.json"
|
|
467
|
+
[[ -f "$state_file" ]] || return 0
|
|
468
|
+
|
|
469
|
+
# Single jq call to extract all fields (avoids 4 process spawns per poll cycle)
|
|
470
|
+
local raw
|
|
471
|
+
raw=$(jq -r '[.run.currentStory // "", .run.currentPhase // "", .run.lastAction // "", .run.acProgress // ""] | join("\t")' "$state_file" 2>/dev/null) || return 0
|
|
472
|
+
[[ -n "$raw" ]] || return 0
|
|
473
|
+
|
|
474
|
+
local cur_story cur_phase cur_action cur_ac
|
|
475
|
+
IFS=$'\t' read -r cur_story cur_phase cur_action cur_ac <<< "$raw"
|
|
476
|
+
|
|
477
|
+
# Nothing to report if no story is active
|
|
478
|
+
[[ -z "$cur_story" ]] && return 0
|
|
479
|
+
|
|
480
|
+
# Detect changes and print structured updates
|
|
481
|
+
if [[ "$cur_story" != "$PREV_STORY" || "$cur_phase" != "$PREV_PHASE" ]]; then
|
|
482
|
+
if [[ -n "$cur_action" && "$cur_action" != "null" ]]; then
|
|
483
|
+
log_status "INFO" "Story ${cur_story}: ${cur_phase} (${cur_action})"
|
|
484
|
+
else
|
|
485
|
+
log_status "INFO" "Story ${cur_story}: ${cur_phase}"
|
|
486
|
+
fi
|
|
487
|
+
elif [[ "$cur_ac" != "$PREV_AC_PROGRESS" && -n "$cur_ac" && "$cur_ac" != "null" ]]; then
|
|
488
|
+
log_status "INFO" "Story ${cur_story}: verify (AC ${cur_ac})"
|
|
489
|
+
elif [[ "$cur_action" != "$PREV_LAST_ACTION" && -n "$cur_action" && "$cur_action" != "null" ]]; then
|
|
490
|
+
log_status "INFO" "Story ${cur_story}: ${cur_phase} (${cur_action})"
|
|
491
|
+
fi
|
|
492
|
+
|
|
493
|
+
PREV_STORY="$cur_story"
|
|
494
|
+
PREV_PHASE="$cur_phase"
|
|
495
|
+
PREV_AC_PROGRESS="$cur_ac"
|
|
496
|
+
PREV_LAST_ACTION="$cur_action"
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# Reset polling state between iterations
|
|
500
|
+
reset_poll_state() {
|
|
501
|
+
PREV_STORY=""
|
|
502
|
+
PREV_PHASE=""
|
|
503
|
+
PREV_AC_PROGRESS=""
|
|
504
|
+
PREV_LAST_ACTION=""
|
|
505
|
+
}
|
|
506
|
+
|
|
447
507
|
# ─── Progress Summary ───────────────────────────────────────────────────────
|
|
448
508
|
|
|
449
509
|
print_progress_summary() {
|
|
@@ -463,7 +523,51 @@ print_progress_summary() {
|
|
|
463
523
|
elapsed_fmt="${elapsed}s"
|
|
464
524
|
fi
|
|
465
525
|
|
|
466
|
-
|
|
526
|
+
# Read cost and failed stories from sprint-state.json (single jq call)
|
|
527
|
+
local cost=""
|
|
528
|
+
local cost_fmt=""
|
|
529
|
+
local failed_stories=""
|
|
530
|
+
if [[ -f "sprint-state.json" ]]; then
|
|
531
|
+
local state_data
|
|
532
|
+
state_data=$(jq -r '(.run.cost // 0 | tostring) + "\n" + ((.run.failed // []) | join("\n"))' "sprint-state.json" 2>/dev/null) || state_data=""
|
|
533
|
+
if [[ -n "$state_data" ]]; then
|
|
534
|
+
cost=$(head -1 <<< "$state_data")
|
|
535
|
+
failed_stories=$(tail -n +2 <<< "$state_data")
|
|
536
|
+
if [[ -n "$cost" && "$cost" != "0" && "$cost" != "null" ]]; then
|
|
537
|
+
cost_fmt=", cost: \$${cost}"
|
|
538
|
+
fi
|
|
539
|
+
fi
|
|
540
|
+
fi
|
|
541
|
+
|
|
542
|
+
log_status "INFO" "Progress: ${completed}/${total} done, ${remaining} remaining (iterations: ${loop_count}, elapsed: ${elapsed_fmt}${cost_fmt})"
|
|
543
|
+
|
|
544
|
+
# Show completed stories with ✓
|
|
545
|
+
if [[ -f "$SPRINT_STATUS_FILE" ]]; then
|
|
546
|
+
while IFS=: read -r key value; do
|
|
547
|
+
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
548
|
+
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
549
|
+
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
550
|
+
if [[ "$key" =~ ^[0-9]+-[0-9]+- && "$value" == "done" ]]; then
|
|
551
|
+
log_status "SUCCESS" " ✓ ${key}"
|
|
552
|
+
fi
|
|
553
|
+
done < "$SPRINT_STATUS_FILE"
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
# Show failed stories with ✗ from sprint-state.json
|
|
557
|
+
if [[ -n "$failed_stories" ]]; then
|
|
558
|
+
while IFS= read -r fkey; do
|
|
559
|
+
[[ -z "$fkey" ]] && continue
|
|
560
|
+
log_status "ERROR" " ✗ ${fkey}"
|
|
561
|
+
done <<< "$failed_stories"
|
|
562
|
+
fi
|
|
563
|
+
|
|
564
|
+
# Show flagged/blocked stories with ✕
|
|
565
|
+
if [[ -f "$FLAGGED_STORIES_FILE" ]]; then
|
|
566
|
+
while IFS= read -r bkey; do
|
|
567
|
+
[[ -z "$bkey" ]] && continue
|
|
568
|
+
log_status "WARN" " ✕ ${bkey} (blocked)"
|
|
569
|
+
done < "$FLAGGED_STORIES_FILE"
|
|
570
|
+
fi
|
|
467
571
|
|
|
468
572
|
# Show the next story in line (first non-done, non-flagged)
|
|
469
573
|
if [[ -f "$SPRINT_STATUS_FILE" ]]; then
|
|
@@ -541,7 +645,7 @@ load_platform_driver() {
|
|
|
541
645
|
CLAUDE_ALLOWED_TOOLS=$(IFS=','; echo "${VALID_TOOL_PATTERNS[*]}")
|
|
542
646
|
fi
|
|
543
647
|
|
|
544
|
-
log_status "
|
|
648
|
+
log_status "DEBUG" "Platform driver: $(driver_display_name) ($(driver_cli_binary))"
|
|
545
649
|
}
|
|
546
650
|
|
|
547
651
|
# ─── Execution ───────────────────────────────────────────────────────────────
|
|
@@ -588,8 +692,12 @@ execute_iteration() {
|
|
|
588
692
|
local deadline=$(( $(date +%s) + timeout_seconds ))
|
|
589
693
|
echo "$deadline" > "ralph/.iteration_deadline"
|
|
590
694
|
|
|
591
|
-
# DEBUG: log
|
|
592
|
-
|
|
695
|
+
# DEBUG: log command (truncate prompt content to avoid dumping entire prompt to terminal)
|
|
696
|
+
local cmd_summary="${CLAUDE_CMD_ARGS[*]}"
|
|
697
|
+
if [[ ${#cmd_summary} -gt 200 ]]; then
|
|
698
|
+
cmd_summary="${cmd_summary:0:200}... (truncated)"
|
|
699
|
+
fi
|
|
700
|
+
log_status "DEBUG" "Command: $cmd_summary"
|
|
593
701
|
log_status "DEBUG" "Output file: $output_file"
|
|
594
702
|
log_status "DEBUG" "LIVE_OUTPUT=$LIVE_OUTPUT, timeout=${timeout_seconds}s"
|
|
595
703
|
|
|
@@ -620,11 +728,13 @@ execute_iteration() {
|
|
|
620
728
|
|
|
621
729
|
log_status "DEBUG" "Background PID: $claude_pid"
|
|
622
730
|
|
|
731
|
+
reset_poll_state
|
|
623
732
|
while kill -0 $claude_pid 2>/dev/null; do
|
|
624
733
|
progress_counter=$((progress_counter + 1))
|
|
625
734
|
if [[ -f "$output_file" && -s "$output_file" ]]; then
|
|
626
735
|
cp "$output_file" "$LIVE_LOG_FILE" 2>/dev/null
|
|
627
736
|
fi
|
|
737
|
+
poll_sprint_state_progress
|
|
628
738
|
sleep 10
|
|
629
739
|
done
|
|
630
740
|
|
|
@@ -788,6 +898,41 @@ The loop:
|
|
|
788
898
|
HELPEOF
|
|
789
899
|
}
|
|
790
900
|
|
|
901
|
+
# ─── Sprint Summary ──────────────────────────────────────────────────────────
|
|
902
|
+
|
|
903
|
+
# Print a compact sprint summary at startup
|
|
904
|
+
print_sprint_summary() {
|
|
905
|
+
local counts
|
|
906
|
+
counts=$(get_task_counts)
|
|
907
|
+
local total=${counts%% *}
|
|
908
|
+
local completed=${counts##* }
|
|
909
|
+
local remaining=$((total - completed))
|
|
910
|
+
|
|
911
|
+
# Find next story
|
|
912
|
+
local next_story=""
|
|
913
|
+
local next_status=""
|
|
914
|
+
if [[ -f "$SPRINT_STATUS_FILE" ]]; then
|
|
915
|
+
while IFS=: read -r key value; do
|
|
916
|
+
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
917
|
+
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
918
|
+
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
919
|
+
if [[ "$key" =~ ^[0-9]+-[0-9]+- && "$value" != "done" ]]; then
|
|
920
|
+
if ! is_story_flagged "$key"; then
|
|
921
|
+
next_story="$key"
|
|
922
|
+
next_status="$value"
|
|
923
|
+
break
|
|
924
|
+
fi
|
|
925
|
+
fi
|
|
926
|
+
done < "$SPRINT_STATUS_FILE"
|
|
927
|
+
fi
|
|
928
|
+
|
|
929
|
+
if [[ -n "$next_story" ]]; then
|
|
930
|
+
log_status "INFO" "Sprint: ${completed}/${total} done, ${remaining} remaining — next: ${next_story} (${next_status})"
|
|
931
|
+
else
|
|
932
|
+
log_status "INFO" "Sprint: ${completed}/${total} done, ${remaining} remaining"
|
|
933
|
+
fi
|
|
934
|
+
}
|
|
935
|
+
|
|
791
936
|
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
792
937
|
|
|
793
938
|
main() {
|
|
@@ -876,15 +1021,17 @@ main() {
|
|
|
876
1021
|
# .story_retries and .flagged_stories are file-based — they persist automatically
|
|
877
1022
|
|
|
878
1023
|
log_status "SUCCESS" "Ralph loop starting"
|
|
879
|
-
log_status "
|
|
880
|
-
log_status "
|
|
881
|
-
log_status "
|
|
882
|
-
log_status "
|
|
883
|
-
log_status "
|
|
1024
|
+
log_status "DEBUG" "Plugin: $PLUGIN_DIR"
|
|
1025
|
+
log_status "DEBUG" "Max iterations: $MAX_ITERATIONS | Timeout: $((LOOP_TIMEOUT_SECONDS / 3600))h"
|
|
1026
|
+
log_status "DEBUG" "Prompt: $PROMPT_FILE"
|
|
1027
|
+
log_status "DEBUG" "Sprint status: $SPRINT_STATUS_FILE"
|
|
1028
|
+
log_status "DEBUG" "Max story retries: $MAX_STORY_RETRIES"
|
|
884
1029
|
|
|
885
1030
|
# Record loop start time for timeout
|
|
886
1031
|
loop_start_time=$(date +%s)
|
|
887
1032
|
|
|
1033
|
+
print_sprint_summary
|
|
1034
|
+
|
|
888
1035
|
local consecutive_failures=0
|
|
889
1036
|
local max_consecutive_failures=3
|
|
890
1037
|
|