codeharness 0.19.5 → 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 +285 -13
- 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 +150 -7
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+-/;
|
|
@@ -2832,23 +2979,41 @@ function registerRunCommand(program) {
|
|
|
2832
2979
|
cwd: projectDir,
|
|
2833
2980
|
env
|
|
2834
2981
|
});
|
|
2982
|
+
let tickerInterval = null;
|
|
2835
2983
|
if (!quiet && child.stdout && child.stderr) {
|
|
2836
|
-
const
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
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
|
+
};
|
|
2843
3000
|
};
|
|
2844
|
-
child.stdout.on("data",
|
|
2845
|
-
child.stderr.on("data",
|
|
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);
|
|
2846
3009
|
}
|
|
2847
3010
|
const exitCode = await new Promise((resolve3, reject) => {
|
|
2848
3011
|
child.on("error", (err) => {
|
|
3012
|
+
if (tickerInterval) clearInterval(tickerInterval);
|
|
2849
3013
|
reject(err);
|
|
2850
3014
|
});
|
|
2851
3015
|
child.on("close", (code) => {
|
|
3016
|
+
if (tickerInterval) clearInterval(tickerInterval);
|
|
2852
3017
|
resolve3(code ?? 1);
|
|
2853
3018
|
});
|
|
2854
3019
|
});
|
|
@@ -4750,7 +4915,11 @@ function defaultState() {
|
|
|
4750
4915
|
iteration: 0,
|
|
4751
4916
|
cost: 0,
|
|
4752
4917
|
completed: [],
|
|
4753
|
-
failed: []
|
|
4918
|
+
failed: [],
|
|
4919
|
+
currentStory: null,
|
|
4920
|
+
currentPhase: null,
|
|
4921
|
+
lastAction: null,
|
|
4922
|
+
acProgress: null
|
|
4754
4923
|
},
|
|
4755
4924
|
actionItems: []
|
|
4756
4925
|
};
|
|
@@ -4774,7 +4943,16 @@ function getSprintState() {
|
|
|
4774
4943
|
try {
|
|
4775
4944
|
const raw = readFileSync15(fp, "utf-8");
|
|
4776
4945
|
const parsed = JSON.parse(raw);
|
|
4777
|
-
|
|
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);
|
|
4778
4956
|
} catch (err) {
|
|
4779
4957
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4780
4958
|
return fail2(`Failed to read sprint state: ${msg}`);
|
|
@@ -4801,6 +4979,43 @@ function computeSprintCounts(stories) {
|
|
|
4801
4979
|
}
|
|
4802
4980
|
return { total, done, failed, blocked, inProgress };
|
|
4803
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
|
+
}
|
|
4804
5019
|
|
|
4805
5020
|
// src/modules/sprint/selector.ts
|
|
4806
5021
|
var MAX_STORY_ATTEMPTS = 10;
|
|
@@ -5392,6 +5607,12 @@ function writeStateAtomic2(state) {
|
|
|
5392
5607
|
function computeSprintCounts2(stories) {
|
|
5393
5608
|
return computeSprintCounts(stories);
|
|
5394
5609
|
}
|
|
5610
|
+
function updateRunProgress2(update) {
|
|
5611
|
+
return updateRunProgress(update);
|
|
5612
|
+
}
|
|
5613
|
+
function clearRunProgress2() {
|
|
5614
|
+
return clearRunProgress();
|
|
5615
|
+
}
|
|
5395
5616
|
|
|
5396
5617
|
// src/modules/verify/validation-runner.ts
|
|
5397
5618
|
var MAX_VALIDATION_ATTEMPTS = 10;
|
|
@@ -10201,8 +10422,58 @@ function outputHuman(p, cycles, allPassed) {
|
|
|
10201
10422
|
fail("RELEASE GATE: FAIL");
|
|
10202
10423
|
}
|
|
10203
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
|
+
|
|
10204
10475
|
// src/index.ts
|
|
10205
|
-
var VERSION = true ? "0.
|
|
10476
|
+
var VERSION = true ? "0.20.0" : "0.0.0-dev";
|
|
10206
10477
|
function createProgram() {
|
|
10207
10478
|
const program = new Command();
|
|
10208
10479
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -10226,6 +10497,7 @@ function createProgram() {
|
|
|
10226
10497
|
registerTimeoutReportCommand(program);
|
|
10227
10498
|
registerValidateStateCommand(program);
|
|
10228
10499
|
registerValidateCommand(program);
|
|
10500
|
+
registerProgressCommand(program);
|
|
10229
10501
|
return program;
|
|
10230
10502
|
}
|
|
10231
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 ───────────────────────────────────────────────────────────────
|
|
@@ -624,11 +728,13 @@ execute_iteration() {
|
|
|
624
728
|
|
|
625
729
|
log_status "DEBUG" "Background PID: $claude_pid"
|
|
626
730
|
|
|
731
|
+
reset_poll_state
|
|
627
732
|
while kill -0 $claude_pid 2>/dev/null; do
|
|
628
733
|
progress_counter=$((progress_counter + 1))
|
|
629
734
|
if [[ -f "$output_file" && -s "$output_file" ]]; then
|
|
630
735
|
cp "$output_file" "$LIVE_LOG_FILE" 2>/dev/null
|
|
631
736
|
fi
|
|
737
|
+
poll_sprint_state_progress
|
|
632
738
|
sleep 10
|
|
633
739
|
done
|
|
634
740
|
|
|
@@ -792,6 +898,41 @@ The loop:
|
|
|
792
898
|
HELPEOF
|
|
793
899
|
}
|
|
794
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
|
+
|
|
795
936
|
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
796
937
|
|
|
797
938
|
main() {
|
|
@@ -880,15 +1021,17 @@ main() {
|
|
|
880
1021
|
# .story_retries and .flagged_stories are file-based — they persist automatically
|
|
881
1022
|
|
|
882
1023
|
log_status "SUCCESS" "Ralph loop starting"
|
|
883
|
-
log_status "
|
|
884
|
-
log_status "
|
|
885
|
-
log_status "
|
|
886
|
-
log_status "
|
|
887
|
-
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"
|
|
888
1029
|
|
|
889
1030
|
# Record loop start time for timeout
|
|
890
1031
|
loop_start_time=$(date +%s)
|
|
891
1032
|
|
|
1033
|
+
print_sprint_summary
|
|
1034
|
+
|
|
892
1035
|
local consecutive_failures=0
|
|
893
1036
|
local max_consecutive_failures=3
|
|
894
1037
|
|