Skip to content

fix(beads-rust): unwrap br JSON envelope before filtering#366

Open
thunter009 wants to merge 3 commits intosubsy:mainfrom
thunter009:fix/beads-rust-json-envelope
Open

fix(beads-rust): unwrap br JSON envelope before filtering#366
thunter009 wants to merge 3 commits intosubsy:mainfrom
thunter009:fix/beads-rust-json-envelope

Conversation

@thunter009
Copy link
Contributor

@thunter009 thunter009 commented Mar 23, 2026

Summary

  • br list --json now returns {"issues": [...]} instead of a bare array
  • beads-rust tracker called .filter() directly on the parsed object, crashing with tasksJson.filter is not a function
  • Added unwrapBrJson() helper that handles both envelope and bare-array formats
  • Applied to getTasks(), getEpics(), and getNextTask()

Test plan

  • Verified ralph-tui run --tracker beads-rust --epic bd-3r0r initializes without error
  • Unit tests for unwrapBrJson with both formats

Fixes #365

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved tool parameter display with fallback handling for non-standard field formats
    • Enhanced parsing robustness to gracefully handle flexible JSON response structures from external integrations
    • Better tool input event fallback handling for edge cases
  • Tests

    • Extended test coverage for tool parameter formatting with unknown field scenarios
    • Added validation for fallback display logic in tool output formatting

thunter009 and others added 3 commits March 17, 2026 19:30
Tool call display showed bare [read] and [apply_patch] with no arguments
when input field names didn't match the hardcoded set. Add fallback that
extracts the first useful string value from input, prioritizing paths.
Also check event.part.input as fallback in OpenCode parser.

Closes subsy#362

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The length guard prevented long string values (e.g. apply_patch bodies)
from being considered for fallback display, causing blank output.
The existing 120-char truncation already keeps output short.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
br list/ready --json now returns {"issues": [...]} instead of a bare
array. Add unwrapBrJson() helper that handles both formats.

Fixes subsy#365

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 23, 2026

@thunter009 is attempting to deploy a commit to the plgeek Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

Walkthrough

The PR addresses tool-call display formatting with fallback logic for unknown fields, updates tool input event parsing with fallback support, and fixes br JSON output parsing to handle both bare arrays and envelope objects with an issues key.

Changes

Cohort / File(s) Summary
Agent output formatting
src/plugins/agents/builtin/opencode.ts, src/plugins/agents/output-formatting.ts, src/plugins/agents/output-formatting.test.ts
Added fallback display extraction logic for tool calls with unknown input fields, prioritising path-like values and capping output to 120 characters. Updated opencode.ts event parsing to use fallback input source when primary source is absent. Added comprehensive test coverage for fallback behaviour in both formatted and segment-based output.
Beads-rust tracker JSON handling
src/plugins/trackers/builtin/beads-rust/index.ts
Introduced unwrapBrJson() helper to normalise br JSON outputs that may be either a raw array or an object envelope with an issues key. Updated getTasks(), getEpics(), and getNextTask() to use this unwrapping function, resolving crashes when br outputs enveloped JSON instead of bare arrays.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes to output-formatting.ts and output-formatting.test.ts for tool-call fallback display, and changes to opencode.ts for tool_use event parsing, are out of scope relative to the linked issue #365 which concerns only beads-rust JSON envelope unwrapping. Separate unrelated display/fallback and tool-use event parsing changes into a distinct PR to maintain focus on the beads-rust JSON envelope fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarises the primary change—unwrapping br JSON envelope in the beads-rust tracker—matching the main objective from linked issue #365.
Linked Issues check ✅ Passed The PR directly addresses all coding requirements from issue #365: the unwrapBrJson() helper is implemented to handle both envelope and bare-array formats, and all three affected methods (getTasks, getEpics, getNextTask) are updated to use it.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 72.72727% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.39%. Comparing base (fcea670) to head (b9f3be4).

Files with missing lines Patch % Lines
src/plugins/agents/output-formatting.ts 68.29% 13 Missing ⚠️
src/plugins/agents/builtin/opencode.ts 33.33% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #366      +/-   ##
==========================================
+ Coverage   47.35%   47.39%   +0.04%     
==========================================
  Files         111      111              
  Lines       36446    36496      +50     
==========================================
+ Hits        17258    17297      +39     
- Misses      19188    19199      +11     
Files with missing lines Coverage Δ
src/plugins/trackers/builtin/beads-rust/index.ts 69.25% <100.00%> (+0.35%) ⬆️
src/plugins/agents/builtin/opencode.ts 72.88% <33.33%> (-0.17%) ⬇️
src/plugins/agents/output-formatting.ts 64.35% <68.29%> (+1.76%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/agents/output-formatting.ts`:
- Around line 149-170: The extractFallbackDisplay function should skip
candidates that exceed the display cap instead of truncating and should
normalise values to a single line before evaluation; update
extractFallbackDisplay to, for each string property in input (using the existing
path-priority logic in extractFallbackDisplay), first take only the first line
(split on '\n' or '\r\n'), then ignore any candidate whose single-line length is
greater than the cap (120) rather than truncating, and only return the first
acceptable bestPath or bestShort; if none remain return undefined. Ensure you
still prefer path-like values (startsWith '/', './', or '~') and only perform
the single-line/length checks when selecting candidates.

In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 165-173: The function unwrapBrJson currently returns [] for
unknown payload shapes which hides contract drift; update unwrapBrJson to throw
a descriptive error instead of returning an empty array when the input doesn't
match the expected array or { issues: [...] } shape — include the unexpected
value (e.g., JSON.stringify(parsed)) and mention the expected shapes in the
error message so callers (and CI) will surface malformed/changed br payloads;
keep the existing Array.isArray(parsed) and issues-branch behavior and only
replace the final return [] with the thrown error referencing unwrapBrJson and
BrTaskJson.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8a5af67-f18d-42a0-8e34-93ee79335fc3

📥 Commits

Reviewing files that changed from the base of the PR and between fcea670 and b9f3be4.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • src/plugins/agents/builtin/opencode.ts
  • src/plugins/agents/output-formatting.test.ts
  • src/plugins/agents/output-formatting.ts
  • src/plugins/trackers/builtin/beads-rust/index.ts

Comment on lines +149 to +170
function extractFallbackDisplay(input: Record<string, unknown>): string | undefined {
// Priority: look for path-like values first, then any short string
let bestPath: string | undefined;
let bestShort: string | undefined;

for (const [, value] of Object.entries(input)) {
if (typeof value !== 'string' || value.length === 0) continue;

// Path-like values get priority
if (value.startsWith('/') || value.startsWith('./') || value.startsWith('~')) {
if (!bestPath) bestPath = value;
} else if (!bestShort) {
bestShort = value;
}
}

const result = bestPath ?? bestShort;
if (!result) return undefined;

// Truncate if needed
return result.length > 120 ? result.slice(0, 120) + '...' : result;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fallback selection should skip oversized values and normalise multiline input.

extractFallbackDisplay currently truncates long values, but the intended behaviour is to avoid displaying long opaque payloads entirely. Also, preserving embedded newlines can break single-line tool-call rendering. Please normalise to first line and skip candidates beyond the display cap.

💡 Proposed fix
 function extractFallbackDisplay(input: Record<string, unknown>): string | undefined {
   // Priority: look for path-like values first, then any short string
   let bestPath: string | undefined;
   let bestShort: string | undefined;
 
   for (const [, value] of Object.entries(input)) {
-    if (typeof value !== 'string' || value.length === 0) continue;
+    if (typeof value !== 'string') continue;
+    const candidate = value.split(/\r?\n/, 1)[0]?.trim();
+    if (!candidate) continue;
+    if (candidate.length > 120) continue;
 
     // Path-like values get priority
-    if (value.startsWith('/') || value.startsWith('./') || value.startsWith('~')) {
-      if (!bestPath) bestPath = value;
+    if (
+      candidate.startsWith('/') ||
+      candidate.startsWith('./') ||
+      candidate.startsWith('~') ||
+      /^[A-Za-z]:[\\/]/.test(candidate)
+    ) {
+      if (!bestPath) bestPath = candidate;
     } else if (!bestShort) {
-      bestShort = value;
+      bestShort = candidate;
     }
   }
 
-  const result = bestPath ?? bestShort;
-  if (!result) return undefined;
-
-  // Truncate if needed
-  return result.length > 120 ? result.slice(0, 120) + '...' : result;
+  return bestPath ?? bestShort;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function extractFallbackDisplay(input: Record<string, unknown>): string | undefined {
// Priority: look for path-like values first, then any short string
let bestPath: string | undefined;
let bestShort: string | undefined;
for (const [, value] of Object.entries(input)) {
if (typeof value !== 'string' || value.length === 0) continue;
// Path-like values get priority
if (value.startsWith('/') || value.startsWith('./') || value.startsWith('~')) {
if (!bestPath) bestPath = value;
} else if (!bestShort) {
bestShort = value;
}
}
const result = bestPath ?? bestShort;
if (!result) return undefined;
// Truncate if needed
return result.length > 120 ? result.slice(0, 120) + '...' : result;
}
function extractFallbackDisplay(input: Record<string, unknown>): string | undefined {
// Priority: look for path-like values first, then any short string
let bestPath: string | undefined;
let bestShort: string | undefined;
for (const [, value] of Object.entries(input)) {
if (typeof value !== 'string') continue;
const candidate = value.split(/\r?\n/, 1)[0]?.trim();
if (!candidate) continue;
if (candidate.length > 120) continue;
// Path-like values get priority
if (
candidate.startsWith('/') ||
candidate.startsWith('./') ||
candidate.startsWith('~') ||
/^[A-Za-z]:[\\/]/.test(candidate)
) {
if (!bestPath) bestPath = candidate;
} else if (!bestShort) {
bestShort = candidate;
}
}
return bestPath ?? bestShort;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/agents/output-formatting.ts` around lines 149 - 170, The
extractFallbackDisplay function should skip candidates that exceed the display
cap instead of truncating and should normalise values to a single line before
evaluation; update extractFallbackDisplay to, for each string property in input
(using the existing path-priority logic in extractFallbackDisplay), first take
only the first line (split on '\n' or '\r\n'), then ignore any candidate whose
single-line length is greater than the cap (120) rather than truncating, and
only return the first acceptable bestPath or bestShort; if none remain return
undefined. Ensure you still prefer path-like values (startsWith '/', './', or
'~') and only perform the single-line/length checks when selecting candidates.

Comment on lines +165 to +173
function unwrapBrJson(parsed: unknown): BrTaskJson[] {
if (Array.isArray(parsed)) {
return parsed as BrTaskJson[];
}
if (parsed && typeof parsed === 'object' && 'issues' in parsed && Array.isArray((parsed as Record<string, unknown>).issues)) {
return (parsed as Record<string, unknown>).issues as BrTaskJson[];
}
return [];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid silently treating unrecognised br payloads as “no tasks”.

At Line 172, falling back to [] for unknown shapes can hide CLI contract drift as a legitimate empty result. In practice this can quietly stop scheduling rather than surfacing a diagnosable failure.

💡 Proposed fix
 function unwrapBrJson(parsed: unknown): BrTaskJson[] {
   if (Array.isArray(parsed)) {
     return parsed as BrTaskJson[];
   }
-  if (parsed && typeof parsed === 'object' && 'issues' in parsed && Array.isArray((parsed as Record<string, unknown>).issues)) {
-    return (parsed as Record<string, unknown>).issues as BrTaskJson[];
+  if (parsed && typeof parsed === 'object') {
+    const issues = (parsed as Record<string, unknown>).issues;
+    if (Array.isArray(issues)) {
+      return issues as BrTaskJson[];
+    }
   }
-  return [];
+  throw new TypeError(
+    'Unexpected br JSON shape: expected BrTaskJson[] or { issues: BrTaskJson[] }'
+  );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function unwrapBrJson(parsed: unknown): BrTaskJson[] {
if (Array.isArray(parsed)) {
return parsed as BrTaskJson[];
}
if (parsed && typeof parsed === 'object' && 'issues' in parsed && Array.isArray((parsed as Record<string, unknown>).issues)) {
return (parsed as Record<string, unknown>).issues as BrTaskJson[];
}
return [];
}
function unwrapBrJson(parsed: unknown): BrTaskJson[] {
if (Array.isArray(parsed)) {
return parsed as BrTaskJson[];
}
if (parsed && typeof parsed === 'object') {
const issues = (parsed as Record<string, unknown>).issues;
if (Array.isArray(issues)) {
return issues as BrTaskJson[];
}
}
throw new TypeError(
'Unexpected br JSON shape: expected BrTaskJson[] or { issues: BrTaskJson[] }'
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 165 - 173, The
function unwrapBrJson currently returns [] for unknown payload shapes which
hides contract drift; update unwrapBrJson to throw a descriptive error instead
of returning an empty array when the input doesn't match the expected array or {
issues: [...] } shape — include the unexpected value (e.g.,
JSON.stringify(parsed)) and mention the expected shapes in the error message so
callers (and CI) will surface malformed/changed br payloads; keep the existing
Array.isArray(parsed) and issues-branch behavior and only replace the final
return [] with the thrown error referencing unwrapBrJson and BrTaskJson.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

beads-rust tracker crashes: br list --json returns envelope object, not array

1 participant