chore(docs): rewrite CLAUDE.md, archive outdated docs, update README - #729
Conversation
Migration documentation deleted (completed work). Remaining docs moved to docs/outdated/. CLAUDE.md rewritten to reflect current ports & adapters architecture, Zustand store patterns, layer rules, and compilation pipeline. README Node.js version constraint updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 4 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughExpanded and replaced repository guidance in Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant PR as "PR (editor repo)"
participant GH as "GitHub Actions (ci-sync)"
participant Filter as "paths-filter step"
participant Surface as "surface-sync job"
participant Deps as "deps-sync job"
participant Script as "script-sync job"
participant Tooling as "tooling-sync job"
participant Web as "openplc-web checkout & compare scripts"
PR->>GH: PR triggers workflow
GH->>Filter: run path detection on editor tree
Filter-->>GH: outputs (shared, deps, script, tooling) flags
GH->>Surface: run only if shared == 'true'
Surface->>Web: conditional checkout (shared == 'true')
Surface->>Web: run compare-surface logic
GH->>Deps: run only if deps == 'true' and CROSS_REPO_TOKEN
Deps->>Web: conditional checkout (deps == 'true')
Deps->>Web: run scripts/compare-dependencies.py
GH->>Script: run only if script == 'true' and CROSS_REPO_TOKEN
Script->>Web: conditional checkout (script == 'true')
Script->>Web: run scripts/compare-surface-definitions.py
GH->>Tooling: run only if tooling == 'true' and CROSS_REPO_TOKEN
Tooling->>Web: conditional checkout (tooling == 'true')
Tooling->>Web: run scripts/compare-tooling.py
Note right of GH: Final report step runs only when shared == 'true'
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLAUDE.md`:
- Line 37: In CLAUDE.md update the unlabeled fenced code blocks (the
triple-backtick blocks beginning at the positions referenced) to include
explicit language identifiers (e.g., ```text, ```bash, or ```typescript) so
markdownlint MD040 warnings are resolved; locate the backtick fences shown in
the diff and change their opening fences to include the appropriate language
token for each block (for example replace ``` with ```text for the src/ and
assets blocks, and similarly add a suitable language for the
src/frontend/components/ and PLCProjectData flow block).
- Around line 42-43: The CLAUDE.md store summary is incorrect: it says “19
slices” and lists slice names (e.g., clipboard) that don't match the actual
RootState in src/frontend/store/index.ts (the RootState composed of 17 slices).
Update the documentation to reflect the current store contract by opening
RootState (identify the slice exports and keys in the store's combine/configure
call in index.ts) and change the count from 19 to 17 and mirror the exact slice
keys/names shown in RootState (remove or rename entries like clipboard if they
no longer exist); ensure lines previously referencing slice details (around the
section that lists slices) match the current slice names and ordering.
- Around line 50-51: The docs currently point the compilation orchestration to
the wrong location; update the documentation lines that reference the
editor/compiler path to instead reference the CompilerModule implemented in the
main modules compiler directory (the module named CompilerModule that implements
the IEC 61131-3 pipeline). Replace the incorrect module path/string in the
README/docs with one that clearly identifies CompilerModule in the main/modules
compiler area so contributors are directed to the actual implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e1082d70-60c6-4355-91f2-2dcdcebe2b04
📒 Files selected for processing (19)
CLAUDE.mdREADME.mddocs/migration-state.mddocs/migration-tracker.mddocs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.mddocs/outdated/HEADLESS_SETUP.mddocs/outdated/dead-code-inventory.mddocs/outdated/debugger-opcua-shared-utilities.mddocs/outdated/external-binaries-strategy.mddocs/outdated/name-type-linking-design.mddocs/outdated/opcua-server-configuration/01-design-overview.mddocs/outdated/opcua-server-configuration/02-ui-screen-specifications.mddocs/outdated/opcua-server-configuration/03-json-configuration-mapping.mddocs/outdated/opcua-server-configuration/04-implementation-phases.mddocs/outdated/opcua-server-configuration/README.mddocs/outdated/s7comm-server-implementation.mddocs/outdated/unified-frontend-serialization.mddocs/outdated/variable-id-audit.mddocs/ports/WIRING.md
💤 Files with no reviewable changes (3)
- docs/migration-state.md
- docs/migration-tracker.md
- docs/ports/WIRING.md
Remove paths filter from ci-sync workflow trigger so the required check always runs and reports a status. Uses dorny/paths-filter inside the job to skip the expensive comparison when no shared surface files changed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
.github/workflows/ci-sync.yml (1)
15-25: Consider moving checkout after the filter to skip unnecessary clone time on docs-only PRs.Line 15 currently checks out
editorbefore determining whether shared surfaces changed. Thedorny/paths-filter@v3action uses GitHub's REST API for pull request events and does not require the repository to be checked out—you can safely run the filter first, then checkout only whenshared == 'true'. This saves clone time for PRs that modify only documentation or non-shared code.Suggested refactor
- - name: Checkout editor repo - uses: actions/checkout@v4 - with: - path: editor - - name: Check for shared surface changes uses: dorny/paths-filter@v3 id: filter with: - working-directory: editor filters: | shared: - 'src/frontend/**' - 'src/middleware/shared/**' - 'src/backend/shared/**' - 'src/__architecture__/**' - 'scripts/compare-surfaces.py' - '.github/workflows/ci-sync.yml' + + - name: Checkout editor repo + if: steps.filter.outputs.shared == 'true' + uses: actions/checkout@v4 + with: + path: editor🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci-sync.yml around lines 15 - 25, Move the "Checkout editor repo" step to after the "Check for shared surface changes" step and run the dorny/paths-filter@v3 step (id: filter) before any checkout; the paths-filter action does not require the repo to be checked out, so keep the existing working-directory and filters on the filter step and conditionally execute the actions/checkout@v4 "Checkout editor repo" step only when the filter output indicates shared == 'true' (use the filter step outputs in the checkout step's if condition). Ensure the step names/ids ("Checkout editor repo", "Check for shared surface changes", id: filter) and the filters input remain unchanged except for relocating the checkout and adding the conditional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In @.github/workflows/ci-sync.yml:
- Around line 15-25: Move the "Checkout editor repo" step to after the "Check
for shared surface changes" step and run the dorny/paths-filter@v3 step (id:
filter) before any checkout; the paths-filter action does not require the repo
to be checked out, so keep the existing working-directory and filters on the
filter step and conditionally execute the actions/checkout@v4 "Checkout editor
repo" step only when the filter output indicates shared == 'true' (use the
filter step outputs in the checkout step's if condition). Ensure the step
names/ids ("Checkout editor repo", "Check for shared surface changes", id:
filter) and the filters input remain unchanged except for relocating the
checkout and adding the conditional.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c08b7886-9f61-4bf5-a4f1-161d6941ace4
📒 Files selected for processing (1)
.github/workflows/ci-sync.yml
dorny/paths-filter uses the GitHub API for PR file detection, not local git. The working-directory param is unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The paths filter causes the sync check to not report a status on PRs that don't touch shared surfaces, breaking required status checks. Replace with a gh api call that detects shared surface changes at runtime and exits early with success when none are found. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gh api fails with 'Resource not accessible by integration' in the same way dorny/paths-filter did. Replace with a local git diff against the base branch after checkout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split the sync workflow into three independent jobs that all run on every PR but early-exit when their respective files are unchanged: - surface-sync: shared UI surface files (existing) - deps-sync: shared production dependency versions in package.json - script-sync: SURFACES definition parity in compare-surfaces.py Also switch path detection from gh api to local git diff to avoid 'Resource not accessible by integration' errors, and remove self-referential paths (ci-sync.yml, compare-surfaces.py) from the surface-sync filter since they are not shared surfaces. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the embedded Python from deps-sync and script-sync workflow jobs into dedicated scripts under scripts/: - compare-dependencies.py: shared production dependency version check - compare-surface-definitions.py: SURFACES list parity check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New job checks that lint, formatter, and TypeScript configurations produce equivalent output on shared surfaces: - .prettierrc and scripts/* must be byte-identical - ESLint rules/plugins must match (ignores can differ per-repo) - TypeScript key compiler options (strict, jsx, paths, etc.) must match Runs on every PR, early-exits when no config/script files changed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
scripts/compare-tooling.py (2)
207-210: Consider explicit encoding for file reads.The
open()calls here (and elsewhere in the script) rely on the platform default encoding. For consistency and to avoid potential issues on non-UTF-8 systems, consider specifyingencoding="utf-8".♻️ Proposed fix
- with open(web_file) as f: + with open(web_file, encoding="utf-8") as f: web_config = parse_jsonc(f.read()) - with open(editor_file) as f: + with open(editor_file, encoding="utf-8") as f: editor_config = parse_jsonc(f.read())🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/compare-tooling.py` around lines 207 - 210, The file reads use open(web_file) and open(editor_file) without an explicit encoding; update both calls (and other open(...) usages in this script) to specify encoding="utf-8" so parse_jsonc receives UTF-8 text consistently—modify the open calls that produce web_config and editor_config (and any similar file reads) to open(..., encoding="utf-8").
70-77: Regex may not handle complex ignores arrays.The pattern
\{\s*ignores:\s*\[.*?\],?\s*\}uses non-greedy matching which won't correctly handle nested structures within the ignores array (e.g., arrays of arrays or objects). For typical ESLint ignore patterns (simple string arrays), this should work fine.If you expect more complex configurations in the future, consider a proper JS parser or more robust stripping logic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/compare-tooling.py` around lines 70 - 77, The current strip_eslint_ignores function uses a non-greedy regex that can break on nested brackets/objects inside the ignores array; replace the regex approach with a small parser: in strip_eslint_ignores locate the "{", the "ignores" key and its following "[" then scan forward counting nested brackets/braces/strings to find the correct matching "]" and the closing "}" and remove that entire block; alternatively call out to a JS/JSON parser if the config is valid JS/JSON. Keep function name strip_eslint_ignores and ensure it returns the cleaned text.scripts/compare-surface-definitions.py (1)
20-28: Missing error handling for file operations and AST parsing.The function can raise unhandled exceptions:
FileNotFoundErrorif the target file doesn't existSyntaxErrorfromast.parse()if the file has invalid Python syntaxValueError/SyntaxErrorfromast.literal_eval()ifSURFACEScontains non-literal valuesConsider wrapping in try-except and returning
None(or a more descriptive error) on failure, similar to howmain()already handlesNonereturns.♻️ Proposed error handling
def extract_surfaces(filepath: Path) -> list[str] | None: + try: - with open(filepath) as f: + with open(filepath, encoding="utf-8") as f: tree = ast.parse(f.read()) + except (FileNotFoundError, SyntaxError) as e: + print(f"::warning::Could not parse {filepath}: {e}") + return None for node in ast.walk(tree): if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name) and target.id == "SURFACES": - return sorted(ast.literal_eval(node.value)) + try: + return sorted(ast.literal_eval(node.value)) + except (ValueError, SyntaxError): + return None return None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/compare-surface-definitions.py` around lines 20 - 28, The extract_surfaces function lacks error handling for file I/O and AST operations; wrap the file read, ast.parse, and ast.literal_eval calls inside a try/except that catches FileNotFoundError, SyntaxError, ValueError (and TypeError) and return None on failure (or optionally log the exception), ensuring extract_surfaces consistently returns None on error like main expects; keep the search for the SURFACES assignment and the sorted return intact but move those operations inside the try block so any parsing/evaluation errors are handled gracefully.scripts/compare-dependencies.py (1)
34-37: Missing error handling for file operations.If
package.jsonis missing or contains invalid JSON, the script will crash with an unhandled exception. Consider adding try-except with informative error messages consistent with the other comparison scripts.♻️ Proposed error handling
+ try: - with open(args.web_root / "package.json") as f: + with open(args.web_root / "package.json", encoding="utf-8") as f: web_deps = json.load(f).get("dependencies", {}) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"::error::Could not read web package.json: {e}") + return 1 + + try: - with open(args.editor_root / "package.json") as f: + with open(args.editor_root / "package.json", encoding="utf-8") as f: editor_deps = json.load(f).get("dependencies", {}) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"::error::Could not read editor package.json: {e}") + return 1🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/compare-dependencies.py` around lines 34 - 37, Wrap the package.json reads in a try/except that catches FileNotFoundError and json.JSONDecodeError when opening args.web_root / "package.json" and args.editor_root / "package.json" (the code that produces web_deps and editor_deps); on error, emit an informative message consistent with other comparison scripts (include which path failed and the exception message) and exit with a non‑zero status (or fallback to empty {} if that matches existing behavior). Ensure you update both the web_deps and editor_deps loading blocks and use the same logging/exit pattern used elsewhere in the repository..github/workflows/ci-sync.yml (1)
348-360: Tooling filter may be overly broad forscripts/.The regex
^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/)matches any file underscripts/, which will triggertooling-synceven for unrelated script changes (e.g., if you add a build script later).Consider narrowing to specific tooling-related scripts:
♻️ Suggested more precise filter
- if echo "$CHANGED" | grep -qE '^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/)'; then + if echo "$CHANGED" | grep -qE '^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/compare-)'; thenThis would only match comparison scripts (
compare-*.py) rather than all scripts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci-sync.yml around lines 348 - 360, The tooling-change filter in the "Check for tooling changes" step (id: filter) is too broad because the regex ^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/) matches any file under scripts/ and will trigger tooling-sync for unrelated script edits; update the git-diff check (the CHANGED determination) to narrow the scripts pattern to only tooling-related files (e.g., replace the scripts/ alternative with a specific glob like scripts/compare-*.py or explicit filenames used for tooling) so that only intended tooling script changes cause changed=true while keeping the existing checks for .prettierrc, eslint.config.*, and tsconfig.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/compare-tooling.py`:
- Around line 85-114: The parse_jsonc function uses text.index(...) for skipping
comments which raises ValueError on missing delimiters; replace those index
calls with text.find(...) and handle -1 results: for the single-line comment
case in parse_jsonc (where it currently does i = text.index('\n', i) if '\n' in
text[i:] else len(text)), use end = text.find('\n', i) and set i = len(text) if
end == -1; for the block comment case (end = text.index('*/', i)) use end =
text.find('*/', i) and either set i = len(text) to treat the rest as a comment
or raise a clear ValueError/ParseError like "Unterminated block comment in
parse_jsonc" so malformed input doesn't crash with a raw ValueError; update the
code paths inside parse_jsonc accordingly.
---
Nitpick comments:
In @.github/workflows/ci-sync.yml:
- Around line 348-360: The tooling-change filter in the "Check for tooling
changes" step (id: filter) is too broad because the regex
^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/) matches any file under
scripts/ and will trigger tooling-sync for unrelated script edits; update the
git-diff check (the CHANGED determination) to narrow the scripts pattern to only
tooling-related files (e.g., replace the scripts/ alternative with a specific
glob like scripts/compare-*.py or explicit filenames used for tooling) so that
only intended tooling script changes cause changed=true while keeping the
existing checks for .prettierrc, eslint.config.*, and tsconfig.
In `@scripts/compare-dependencies.py`:
- Around line 34-37: Wrap the package.json reads in a try/except that catches
FileNotFoundError and json.JSONDecodeError when opening args.web_root /
"package.json" and args.editor_root / "package.json" (the code that produces
web_deps and editor_deps); on error, emit an informative message consistent with
other comparison scripts (include which path failed and the exception message)
and exit with a non‑zero status (or fallback to empty {} if that matches
existing behavior). Ensure you update both the web_deps and editor_deps loading
blocks and use the same logging/exit pattern used elsewhere in the repository.
In `@scripts/compare-surface-definitions.py`:
- Around line 20-28: The extract_surfaces function lacks error handling for file
I/O and AST operations; wrap the file read, ast.parse, and ast.literal_eval
calls inside a try/except that catches FileNotFoundError, SyntaxError,
ValueError (and TypeError) and return None on failure (or optionally log the
exception), ensuring extract_surfaces consistently returns None on error like
main expects; keep the search for the SURFACES assignment and the sorted return
intact but move those operations inside the try block so any parsing/evaluation
errors are handled gracefully.
In `@scripts/compare-tooling.py`:
- Around line 207-210: The file reads use open(web_file) and open(editor_file)
without an explicit encoding; update both calls (and other open(...) usages in
this script) to specify encoding="utf-8" so parse_jsonc receives UTF-8 text
consistently—modify the open calls that produce web_config and editor_config
(and any similar file reads) to open(..., encoding="utf-8").
- Around line 70-77: The current strip_eslint_ignores function uses a non-greedy
regex that can break on nested brackets/objects inside the ignores array;
replace the regex approach with a small parser: in strip_eslint_ignores locate
the "{", the "ignores" key and its following "[" then scan forward counting
nested brackets/braces/strings to find the correct matching "]" and the closing
"}" and remove that entire block; alternatively call out to a JS/JSON parser if
the config is valid JS/JSON. Keep function name strip_eslint_ignores and ensure
it returns the cleaned text.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 440ae5b3-f4de-469d-a3ba-f24351912c89
📒 Files selected for processing (4)
.github/workflows/ci-sync.ymlscripts/compare-dependencies.pyscripts/compare-surface-definitions.pyscripts/compare-tooling.py
| def parse_jsonc(text: str) -> dict: | ||
| """Parse JSON with comments (// and /* */) and trailing commas.""" | ||
| # Strip comments while preserving strings containing // | ||
| result = [] | ||
| i = 0 | ||
| while i < len(text): | ||
| if text[i] == '"': | ||
| j = i + 1 | ||
| while j < len(text): | ||
| if text[j] == '\\': | ||
| j += 2 | ||
| continue | ||
| if text[j] == '"': | ||
| j += 1 | ||
| break | ||
| j += 1 | ||
| result.append(text[i:j]) | ||
| i = j | ||
| elif text[i:i+2] == '//': | ||
| i = text.index('\n', i) if '\n' in text[i:] else len(text) | ||
| elif text[i:i+2] == '/*': | ||
| end = text.index('*/', i) | ||
| i = end + 2 | ||
| else: | ||
| result.append(text[i]) | ||
| i += 1 | ||
| text = "".join(result) | ||
| # Strip trailing commas before } or ] | ||
| text = re.sub(r",\s*([}\]])", r"\1", text) | ||
| return json.loads(text) |
There was a problem hiding this comment.
parse_jsonc can raise ValueError on malformed input.
The text.index() calls on lines 104 and 106 raise ValueError if the substring is not found:
- Line 104: Single-line comment (
//) at EOF without trailing newline - Line 106: Unclosed block comment (
/*without*/)
While valid JSONC files won't have these issues, a corrupted or truncated file could crash the script.
🛡️ Proposed defensive fix
elif text[i:i+2] == '//':
- i = text.index('\n', i) if '\n' in text[i:] else len(text)
+ newline_pos = text.find('\n', i)
+ i = newline_pos if newline_pos != -1 else len(text)
elif text[i:i+2] == '/*':
- end = text.index('*/', i)
+ end = text.find('*/', i)
+ if end == -1:
+ # Unclosed block comment - skip to end
+ i = len(text)
+ continue
i = end + 2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/compare-tooling.py` around lines 85 - 114, The parse_jsonc function
uses text.index(...) for skipping comments which raises ValueError on missing
delimiters; replace those index calls with text.find(...) and handle -1 results:
for the single-line comment case in parse_jsonc (where it currently does i =
text.index('\n', i) if '\n' in text[i:] else len(text)), use end =
text.find('\n', i) and set i = len(text) if end == -1; for the block comment
case (end = text.index('*/', i)) use end = text.find('*/', i) and either set i =
len(text) to treat the rest as a comment or raise a clear ValueError/ParseError
like "Unterminated block comment in parse_jsonc" so malformed input doesn't
crash with a raw ValueError; update the code paths inside parse_jsonc
accordingly.
Add noFallthroughCasesInSwitch, noUnusedLocals, and noUnusedParameters to match web's tsconfig.app.json so both repos type-check shared surfaces with the same compiler options. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The deps-sync, script-sync, and tooling-sync jobs were checking out the companion repo at the base branch, causing failures when both repos have coordinated changes on matching feature branches. Now tries head branch -> base branch -> main, so cross-repo feature branches are compared against each other. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Test plan
Summary by CodeRabbit
Documentation
Chores
Tools