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 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.19.4" : "0.0.0-dev";
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("--live", "Show live output streaming", 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) => {
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
- live: options.live,
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
- return ok2(parsed);
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.19.4" : "0.0.0-dev";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.19.4",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
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
- log_status "INFO" "Progress: ${completed}/${total} done, ${remaining} remaining (iterations: ${loop_count}, elapsed: ${elapsed_fmt})"
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 "INFO" "Platform driver: $(driver_display_name) ($(driver_cli_binary))"
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 the command being run
592
- log_status "DEBUG" "Command: ${CLAUDE_CMD_ARGS[*]}"
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 "INFO" "Plugin: $PLUGIN_DIR"
880
- log_status "INFO" "Max iterations: $MAX_ITERATIONS | Timeout: $((LOOP_TIMEOUT_SECONDS / 3600))h"
881
- log_status "INFO" "Prompt: $PROMPT_FILE"
882
- log_status "INFO" "Sprint status: $SPRINT_STATUS_FILE"
883
- log_status "INFO" "Max story retries: $MAX_STORY_RETRIES"
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