Conversation
Co-Authored-By: Virgil <virgil@lethean.io>
RunOptions.Detach creates the process in its own process group (Setpgid) and uses context.Background() instead of the parent context. Detached processes survive parent death. Co-Authored-By: Virgil <virgil@lethean.io>
…nd registry Replace all os.ReadFile, os.WriteFile, os.MkdirAll, os.Remove calls in pidfile.go and registry.go with coreio.Local equivalents (Read, Write, EnsureDir, Delete). Add forge.lthn.ai/core/go-io v0.1.2 as a direct dependency. Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
Replace all 27 instances of fmt.Errorf/errors.New in production code with coreerr.E() from forge.lthn.ai/core/go-log for structured error context (op, message, cause). Promote go-log from indirect to direct dependency in go.mod. Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughRefactors error handling to use structured coreerr.E across services, replaces direct filesystem calls with coreio Local methods, adds a Detach option to process RunOptions (detached process groups), updates tests to expect errors instead of panics, and bumps multiple module dependencies plus a new CI workflow. Changes
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
global_test.go (1)
64-65:⚠️ Potential issue | 🟡 MinorUnchecked error return from
SetDefault.The return value of
SetDefault(svc)is ignored. Since the function now returns an error, consider usingrequire.NoError(t, SetDefault(svc))to ensure the happy path succeeds as expected.Proposed fix
- SetDefault(svc) + require.NoError(t, SetDefault(svc))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@global_test.go` around lines 64 - 65, SetDefault now returns an error but the test ignores it; update the test to assert the call succeeds by replacing the bare SetDefault(svc) with require.NoError(t, SetDefault(svc)) (and ensure the testify/require import is present), then keep the existing assert.Equal(t, svc, Default()) to verify the value.
🧹 Nitpick comments (1)
exec/exec.go (1)
157-163: Consider using a more descriptive location string.The location string
"wrapError"refers to the internal helper function rather than the calling context. This may reduce the usefulness of structured error tracing. Consider passing the location from the caller or using a more descriptive identifier like"exec.Command".Proposed enhancement
-func wrapError(err error, name string, args []string) error { +func wrapError(err error, name string, args []string, location string) error { cmdStr := name + " " + strings.Join(args, " ") if exitErr, ok := err.(*exec.ExitError); ok { - return coreerr.E("wrapError", fmt.Sprintf("command %q failed with exit code %d", cmdStr, exitErr.ExitCode()), err) + return coreerr.E(location, fmt.Sprintf("command %q failed with exit code %d", cmdStr, exitErr.ExitCode()), err) } - return coreerr.E("wrapError", fmt.Sprintf("failed to execute %q", cmdStr), err) + return coreerr.E(location, fmt.Sprintf("failed to execute %q", cmdStr), err) }Then update callers to pass their method name, e.g.,
wrapError(err, c.name, c.args, "Cmd.Run").🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@exec/exec.go` around lines 157 - 163, The helper wrapError currently hardcodes the location string "wrapError", reducing trace usefulness; change wrapError signature to accept a location parameter (e.g., wrapError(err error, name string, args []string, location string) error), use that location when calling coreerr.E instead of the literal, and update all callers (e.g., Cmd.Run, any call sites that pass err from exec.Command) to pass a descriptive location like "exec.Command" or "Cmd.Run". Keep the existing ExitError handling (checking exec.ExitError) and only replace the static "wrapError" identifier with the new location argument when constructing the coreerr.E call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@daemon.go`:
- Around line 103-105: Start() currently sets d.running true and may acquire
PID/health resources before calling d.opts.Registry.Register(entry); if Register
fails you must rollback those startup resources and reset d.running. Add a
deferred/cleanup path in Start (e.g. a rollback closure captured before setting
d.running) that on any early return sets d.running = false and releases any
acquired resources: remove PID file, deregister health checks, and call
d.opts.Registry.Unregister(entry) if registration succeeded; invoke that
rollback when d.opts.Registry.Register(entry) returns an error so the daemon
leaves no leftover PID/health state on failed start.
In `@service.go`:
- Around line 116-119: The assignment to cmd.SysProcAttr with Setpgid is
Unix-only and fails on Windows; replace the direct construction with a
platform-specific helper: when opts.Detach is true set cmd.SysProcAttr =
detachedSysProcAttr(). Implement detachedSysProcAttr() in OS-specific files
(e.g., sysproc_unix.go with //go:build unix returning
&syscall.SysProcAttr{Setpgid: true} and sysproc_windows.go with //go:build
windows returning &syscall.SysProcAttr{}), keeping the symbol names
detachedSysProcAttr, cmd, opts.Detach and SysProcAttr as referenced in
service.go.
---
Outside diff comments:
In `@global_test.go`:
- Around line 64-65: SetDefault now returns an error but the test ignores it;
update the test to assert the call succeeds by replacing the bare
SetDefault(svc) with require.NoError(t, SetDefault(svc)) (and ensure the
testify/require import is present), then keep the existing assert.Equal(t, svc,
Default()) to verify the value.
---
Nitpick comments:
In `@exec/exec.go`:
- Around line 157-163: The helper wrapError currently hardcodes the location
string "wrapError", reducing trace usefulness; change wrapError signature to
accept a location parameter (e.g., wrapError(err error, name string, args
[]string, location string) error), use that location when calling coreerr.E
instead of the literal, and update all callers (e.g., Cmd.Run, any call sites
that pass err from exec.Command) to pass a descriptive location like
"exec.Command" or "Cmd.Run". Keep the existing ExitError handling (checking
exec.ExitError) and only replace the static "wrapError" identifier with the new
location argument when constructing the coreerr.E call.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f5aa3c59-da7e-4cf3-8510-02c3214b6036
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (11)
daemon.goexec/exec.goglobal_test.gogo.modhealth.gopidfile.goprocess.goregistry.gorunner.goservice.gotypes.go
| if err := d.opts.Registry.Register(entry); err != nil { | ||
| return fmt.Errorf("registry: %w", err) | ||
| return coreerr.E("Daemon.Start", "registry", err) | ||
| } |
There was a problem hiding this comment.
Rollback daemon state when registry registration fails.
At Line 104, Start() returns after d.running is already true and startup resources may already be acquired. This can leave PID/health resources active after a failed start.
Suggested fix
if err := d.opts.Registry.Register(entry); err != nil {
+ if d.health != nil {
+ _ = d.health.Stop(context.Background())
+ }
+ if d.pid != nil {
+ _ = d.pid.Release()
+ }
+ d.running = false
return coreerr.E("Daemon.Start", "registry", err)
}📝 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.
| if err := d.opts.Registry.Register(entry); err != nil { | |
| return fmt.Errorf("registry: %w", err) | |
| return coreerr.E("Daemon.Start", "registry", err) | |
| } | |
| if err := d.opts.Registry.Register(entry); err != nil { | |
| if d.health != nil { | |
| _ = d.health.Stop(context.Background()) | |
| } | |
| if d.pid != nil { | |
| _ = d.pid.Release() | |
| } | |
| d.running = false | |
| return coreerr.E("Daemon.Start", "registry", err) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@daemon.go` around lines 103 - 105, Start() currently sets d.running true and
may acquire PID/health resources before calling d.opts.Registry.Register(entry);
if Register fails you must rollback those startup resources and reset d.running.
Add a deferred/cleanup path in Start (e.g. a rollback closure captured before
setting d.running) that on any early return sets d.running = false and releases
any acquired resources: remove PID file, deregister health checks, and call
d.opts.Registry.Unregister(entry) if registration succeeded; invoke that
rollback when d.opts.Registry.Register(entry) returns an error so the daemon
leaves no leftover PID/health state on failed start.
| // Detached processes get their own process group | ||
| if opts.Detach { | ||
| cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Build tags present in Go files:"
rg -n --type go '^//go:build' -C1
echo
echo "SysProcAttr/Setpgid usage:"
rg -n --type go 'SysProcAttr|Setpgid' -C2Repository: dAppCore/go-process
Length of output: 94
🏁 Script executed:
#!/bin/bash
# Find the service.go file
fd -name "service.go" -type fRepository: dAppCore/go-process
Length of output: 293
🏁 Script executed:
#!/bin/bash
# Find the service.go file
fd service.goRepository: dAppCore/go-process
Length of output: 73
🏁 Script executed:
#!/bin/bash
# Check the file size first
wc -l service.go
# Read the file with line numbers to see context around lines 116-119
cat -n service.goRepository: dAppCore/go-process
Length of output: 11329
🏁 Script executed:
#!/bin/bash
# Check for existing OS-specific file patterns
fd -type f -name '*_unix.go' -o -name '*_windows.go' -o -name '*_darwin.go' -o -name '*_linux.go'
echo "---"
# Check if there are any build tags in the codebase at all
find . -name "*.go" -type f -exec grep -l "//go:build" {} \;Repository: dAppCore/go-process
Length of output: 254
🏁 Script executed:
#!/bin/bash
# Check for existing OS-specific file patterns
find . -name '*_unix.go' -o -name '*_windows.go' -o -name '*_darwin.go' -o -name '*_linux.go'
echo "---"
# Check if there are any build tags in the codebase at all
find . -name "*.go" -exec grep -l "//go:build" {} \;Repository: dAppCore/go-process
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Check all Go files in the repo
ls -la *.go 2>/dev/null || echo "No .go files in root"
# List all Go files to understand structure
find . -name "*.go" -type f | head -20Repository: dAppCore/go-process
Length of output: 1638
🌐 Web query:
syscall.SysProcAttr Setpgid Windows Go Unix compatibility
💡 Result:
syscall.SysProcAttr.Setpgidis a Unix-only knob (it maps to the POSIXsetpgidbehavior). In Go’s UnixSysProcAttr, you can seeSetpgid boolandPgid intdocumented/implemented. [2]- On Windows,
syscall.SysProcAttris a different struct and does not haveSetpgid/Pgid(so&syscall.SysProcAttr{Setpgid:true}won’t compile on Windows). Windows’SysProcAttrinstead exposes fields likeCreationFlags,HideWindow, etc. [1]
What to do for cross-platform code (Go):
- Split per-OS code (recommended): put the
Setpgidsetup in*_unix.go(or*_linux.go,*_darwin.go, etc.) and a Windows alternative in*_windows.go. - If your goal on Windows is “don’t share the parent’s console control handling / be able to signal the child group”, the closest primitive is starting the process with
SysProcAttr.CreationFlags = syscall.CREATE_NEW_PROCESS_GROUP. [1][3]
Note: this is not equivalent to Unix process groups/signals; Windows doesn’t implement POSIX PGIDs/signals the same way.
🏁 Script executed:
#!/bin/bash
# Verify by checking Go stdlib documentation for SysProcAttr
# Check what fields are available and if this would cause compilation issues
python3 << 'EOF'
# Quick verification: Setpgid is platform-specific
info = """
In Go's syscall package:
- unix/linux/darwin: syscall.SysProcAttr has Setpgid field (bool)
- windows: syscall.SysProcAttr does NOT have Setpgid field
This means the code at line 118 will cause a compilation error on Windows:
'field Setpgid not defined on SysProcAttr type'
The code needs OS-specific handling as suggested in the review.
"""
print(info)
EOFRepository: dAppCore/go-process
Length of output: 403
Guard Setpgid assignment behind OS-specific handling.
Line 118 uses a Unix-only SysProcAttr field directly. This causes a compilation error on Windows, where syscall.SysProcAttr lacks the Setpgid field.
Suggested fix
- if opts.Detach {
- cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
- }
+ if opts.Detach {
+ cmd.SysProcAttr = detachedSysProcAttr()
+ }Use OS-specific helpers:
// sysproc_unix.go
//go:build unix
package process
import "syscall"
func detachedSysProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{Setpgid: true}
}// sysproc_windows.go
//go:build windows
package process
import "syscall"
func detachedSysProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service.go` around lines 116 - 119, The assignment to cmd.SysProcAttr with
Setpgid is Unix-only and fails on Windows; replace the direct construction with
a platform-specific helper: when opts.Detach is true set cmd.SysProcAttr =
detachedSysProcAttr(). Implement detachedSysProcAttr() in OS-specific files
(e.g., sysproc_unix.go with //go:build unix returning
&syscall.SysProcAttr{Setpgid: true} and sysproc_windows.go with //go:build
windows returning &syscall.SysProcAttr{}), keeping the symbol names
detachedSysProcAttr, cmd, opts.Detach and SysProcAttr as referenced in
service.go.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)
41-44: Prefer queued auto-merge behind protection checks.The current workflow merges immediately upon CodeRabbit approval (line 52) without waiting for any branch protection rules. Use the
--autoflag to enable auto-merge, which respects required status checks, required reviews, and merge queues before allowing the merge.Suggested safer merge mode
- run: gh pr merge ${{ github.event.pull_request.number }} --merge --delete-branch + run: gh pr merge ${{ github.event.pull_request.number }} --auto --merge --delete-branchAlso applies to: 52-54
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 41 - 44, The workflow currently performs an immediate merge on CodeRabbit approval; update the merge step (the step running the GitHub CLI `gh pr merge` command) to use the queued auto-merge mode by adding the `--auto` flag (and keep the desired merge method flag such as `--merge`/`--squash`/`--rebase` if present) so it respects branch protection, required checks and merge queues instead of merging immediately; locate the step that executes `gh pr merge` and replace its invocation with the `--auto` variant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/ci.yml:
- Line 17: Replace mutable action refs that use the `@dev` tag with immutable
commit SHA pins: locate occurrences of "dAppCore/build/actions/build/core@dev"
(and the second occurrence noted in the review) and change them to the
corresponding full commit SHAs (e.g., @<full-commit-sha>) obtained from the
dAppCore repository for the exact commit you want to lock to; update both
occurrences so CI uses the same immutable SHAs and commit the changes so
workflows are reproducible and hardened against supply-chain shifts.
- Around line 23-35: The job is not fork-safe: the checkout uses
github.event.pull_request.head.ref (in the actions/checkout@v4 step) which can
resolve incorrectly for forked PRs and the workflow runs even when GITHUB_TOKEN
is read-only for forks; update the job guard to only run when the PR head repo
equals the base repo (e.g., require
github.event.pull_request.head.repo.full_name == github.repository) so the job
skips forked PRs, and change the checkout to use
github.event.pull_request.head.sha instead of head.ref for deterministic
checkout of the exact commit.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 41-44: The workflow currently performs an immediate merge on
CodeRabbit approval; update the merge step (the step running the GitHub CLI `gh
pr merge` command) to use the queued auto-merge mode by adding the `--auto` flag
(and keep the desired merge method flag such as `--merge`/`--squash`/`--rebase`
if present) so it respects branch protection, required checks and merge queues
instead of merging immediately; locate the step that executes `gh pr merge` and
replace its invocation with the `--auto` variant.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b2e71d3c-7459-4da0-a75a-3dce1cb37dcd
📒 Files selected for processing (1)
.github/workflows/ci.yml
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: dAppCore/build/actions/build/core@dev |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Identify workflow actions pinned to mutable refs (branch/tag) instead of full commit SHAs.
fd -t f -e yml -e yaml .github/workflows | \
xargs rg -nP '^\s*-\s*uses:\s*[^@]+@(?!(?:[0-9a-fA-F]{40})\b)'Repository: dAppCore/go-process
Length of output: 438
🏁 Script executed:
# First, find and list YAML files in .github/workflows directory
fd . '.github/workflows' -t f -e yml -e yamlRepository: dAppCore/go-process
Length of output: 87
🏁 Script executed:
# Check specific lines in ci.yml if file exists
if [ -f ".github/workflows/ci.yml" ]; then
echo "=== Lines 1-40 of ci.yml ===";
head -n 40 ".github/workflows/ci.yml" | cat -n;
fiRepository: dAppCore/go-process
Length of output: 1286
🏁 Script executed:
# Check for other workflow files and count total lines in ci.yml
wc -l ".github/workflows/ci.yml"
# Search all workflow files for mutable refs (fixing the original command syntax)
fd . '.github/workflows' -t f \( -name '*.yml' -o -name '*.yaml' \) | \
xargs rg -nP '^\s*-\s*uses:\s*[^@]+@(?!(?:[0-9a-fA-F]{40})\b)' 2>/dev/null || echo "Search completed"Repository: dAppCore/go-process
Length of output: 355
🏁 Script executed:
# Search for all 'uses:' lines in workflow files
fd . '.github/workflows' -t f | \
xargs rg 'uses:' -A 0 -B 0
# Also check specifically for `@dev`, `@main`, `@v` tags (mutable refs)
rg '@(dev|main|master|latest|[a-z]+-[a-z]+)\b' '.github/workflows/ci.yml' -nRepository: dAppCore/go-process
Length of output: 364
Pin workflow actions to immutable SHAs.
Lines 17 and 36 use @dev, which is mutable. This makes CI behaviour non-reproducible and increases supply-chain risk.
Suggested hardening
- - uses: dAppCore/build/actions/build/core@dev
+ - uses: dAppCore/build/actions/build/core@<full_commit_sha>- - uses: dAppCore/build/actions/fix@dev
+ - uses: dAppCore/build/actions/fix@<full_commit_sha>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml at line 17, Replace mutable action refs that use
the `@dev` tag with immutable commit SHA pins: locate occurrences of
"dAppCore/build/actions/build/core@dev" (and the second occurrence noted in the
review) and change them to the corresponding full commit SHAs (e.g.,
@<full-commit-sha>) obtained from the dAppCore repository for the exact commit
you want to lock to; update both occurrences so CI uses the same immutable SHAs
and commit the changes so workflows are reproducible and hardened against
supply-chain shifts.
| if: > | ||
| github.event_name == 'pull_request_review' && | ||
| github.event.review.user.login == 'coderabbitai' && | ||
| github.event.review.state == 'changes_requested' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.ref }} | ||
| fetch-depth: 0 |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For GitHub Actions triggered by pull_request_review, what is the correct checkout configuration for forked pull requests, and can GITHUB_TOKEN push commits to fork branches?
💡 Result:
Correct actions/checkout for forked PRs on pull_request_review
For pull_request_review, the event payload includes the PR, so you should explicitly checkout the PR head repo + head SHA (not the default refs/pull/.../merge in the base repo):
- uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0This pattern (using head.repo.full_name + head.ref/head.sha) is the documented way to checkout code from a fork PR. [1] [2]
Can GITHUB_TOKEN push commits to fork branches?
No, not in the general case. On workflows triggered by a PR from a fork, GitHub adjusts the GITHUB_TOKEN so that any write permissions become read-only (unless a repo admin explicitly enables Send write tokens to workflows from pull requests). [3]
And even when you do have write permissions, GITHUB_TOKEN is for the workflow’s repository context; trying to git push back into a contributor’s fork typically fails with Permission ... denied to github-actions[bot]. [4]
If you need to push commits to the contributor’s fork branch, you generally must use different credentials that have write access to that fork (e.g., a PAT or GitHub App installation token with access to the fork). [3] [4]
Sources:
[1] actions/checkout README (checkout PR head SHA) (github.com)
[2] actions/checkout issue showing fork checkout config (repository + ref) (github.com)
[3] GitHub Docs: forked PR runs make GITHUB_TOKEN write perms read-only (unless “Send write tokens…” enabled) (docs.github.com)
[4] GitHub Community: pushing to a fork with GITHUB_TOKEN fails with permission denied (github.com)
Citations:
- 1: https://github.com/actions/checkout
- 2: Github Checkout action failing to find forked repository actions/checkout#551
- 3: https://docs.github.com/en/actions/reference/github_token-reference
- 4: https://github.com/orgs/community/discussions/26904
auto-fix is not fork-safe as written.
Line 34 checks out only head.ref without specifying the fork repository. For forked PRs, this often resolves against the base repo and fails. Additionally, GITHUB_TOKEN has write permissions reduced to read-only on forked PR workflows by default, preventing commits being pushed back to contributor branches.
Suggested guard + deterministic checkout
auto-fix:
if: >
github.event_name == 'pull_request_review' &&
github.event.review.user.login == 'coderabbitai' &&
- github.event.review.state == 'changes_requested'
+ github.event.review.state == 'changes_requested' &&
+ github.event.pull_request.head.repo.full_name == github.repository
...
- uses: actions/checkout@v4
with:
- ref: ${{ github.event.pull_request.head.ref }}
+ ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0The guard prevents this job from running on forked PRs, ensuring the workflow only executes where push access is available. Using head.sha instead of head.ref provides deterministic checkout behaviour.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml around lines 23 - 35, The job is not fork-safe: the
checkout uses github.event.pull_request.head.ref (in the actions/checkout@v4
step) which can resolve incorrectly for forked PRs and the workflow runs even
when GITHUB_TOKEN is read-only for forks; update the job guard to only run when
the PR head repo equals the base repo (e.g., require
github.event.pull_request.head.repo.full_name == github.repository) so the job
skips forked PRs, and change the checkout to use
github.event.pull_request.head.sha instead of head.ref for deterministic
checkout of the exact commit.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
…LAUDE.md — update any outdate...' (#1) from agent/dx-audit-and-fix--1--review-claude-md into main
Forge → GitHub Sync
Commits: 6
Files changed: 12
Automated sync from Forge (forge.lthn.ai) to GitHub mirror.
Co-Authored-By: Virgil virgil@lethean.io
Summary by CodeRabbit
New Features
Bug Fixes
Chores