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 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.5" : "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+-/;
@@ -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 filterOutput = (data) => {
2837
- const lines = data.toString().split("\n");
2838
- for (const line of lines) {
2839
- if (line.includes("[DEBUG]")) continue;
2840
- if (line.trim().length === 0) continue;
2841
- process.stdout.write(line + "\n");
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", filterOutput);
2845
- child.stderr.on("data", filterOutput);
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
- 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);
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.19.5" : "0.0.0-dev";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.19.5",
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 ───────────────────────────────────────────────────────────────
@@ -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 "INFO" "Plugin: $PLUGIN_DIR"
884
- log_status "INFO" "Max iterations: $MAX_ITERATIONS | Timeout: $((LOOP_TIMEOUT_SECONDS / 3600))h"
885
- log_status "INFO" "Prompt: $PROMPT_FILE"
886
- log_status "INFO" "Sprint status: $SPRINT_STATUS_FILE"
887
- 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"
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