New RPC endpoint to get detailed progress for a molecule (parent issue
with child steps). Returns moleculeID, title, assignee, and list of
steps with their status (done/current/ready/blocked) and timestamps.
Used when user expands a worker in the activity feed TUI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New RPC endpoint to get all workers and their current molecule/step in one call.
Returns: assignee, moleculeID, moleculeTitle, currentStep, totalSteps, stepTitle,
lastActivity, status. Enables activity feed TUI to show worker state without
multiple round trips.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Title and Assignee fields to MutationEvent struct so activity feeds
can display meaningful context without extra lookups. Updated emitMutation
signature to accept these values and modified all callers:
- Create: passes issue.Title and issue.Assignee directly
- Update/Close: moved emitMutation after GetIssue to access enriched data
- Delete: uses existing issue lookup before deletion
- Dep/Label/Comment ops: passes empty strings (would require extra lookup)
Fixes bd-gqxd
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add gate operation constants (OpGateCreate, OpGateList, OpGateShow,
OpGateClose, OpGateWait) to protocol.go
- Add Gate*Args and Gate*Result types to protocol.go
- Add gate handler methods (handleGateCreate, handleGateList,
handleGateShow, handleGateClose, handleGateWait) to server_issues_epics.go
- Register gate handlers in handleRequest switch
- Add gate client methods (GateCreate, GateList, GateShow, GateClose,
GateWait) to client.go
- Update cmd/bd/gate.go to use daemon client when available, falling
back to direct store access
Gate commands now work with the daemon, eliminating the need for
--no-daemon flag.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add emitRichMutation() function for events with metadata
- handleClose now emits MutationStatus with old/new status
- handleUpdate detects status changes and emits MutationStatus
- Add comprehensive tests for rich mutation events
Also:
- Add activity.go test coverage (bd-3jcw):
- Tests for parseDurationString, filterEvents, formatEvent
- Tests for all mutation type displays
- Fix silent error handling in --follow mode (bd-csnr):
- Track consecutive daemon failures
- Show warning after 5 failures (rate-limited to 30s)
- Show reconnection message on recovery
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds 'waits-for' dependency type for dynamic molecule bonding:
- DepWaitsFor blocks an issue until spawner's children are closed
- Two gate types: all-children (wait for all) or any-children (first)
- Updated blocked_cache.go CTE to handle waits-for dependencies
- Added --waits-for and --waits-for-gate flags to bd create command
- Added WaitsForMeta struct for gate metadata storage
- Full test coverage for all gate types and dynamic child scenarios
This enables patrol molecules to wait for dynamically-bonded arms to
complete before proceeding (Christmas Ornament pattern).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Filters issues by parent issue ID using parent-child dependencies.
Example: bd list --parent=bd-xyz --status=open
Changes:
- Add ParentID field to IssueFilter type
- Add --parent flag to list command
- Forward parent filter through RPC
- Implement filtering in SQLite and memory storage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add --auto-pull flag to control whether the daemon periodically pulls from
remote to check for updates from other clones.
Configuration precedence:
1. --auto-pull CLI flag (highest)
2. BEADS_AUTO_PULL environment variable
3. daemon.auto_pull in database config
4. Default: true when sync.branch is configured
When auto_pull is enabled, the daemon creates a remoteSyncTicker that
periodically calls doAutoImport() to pull remote changes. When disabled,
users must manually run 'git pull' to sync remote changes.
Changes:
- cmd/bd/daemon.go: Add --auto-pull flag and config reading logic
- cmd/bd/daemon_event_loop.go: Gate remoteSyncTicker on autoPull parameter
- cmd/bd/daemon_lifecycle.go: Add auto-pull to status output and spawn args
- internal/rpc/protocol.go: Add AutoPull field to StatusResponse
- internal/rpc/server_core.go: Add autoPull to Server struct and SetConfig
- internal/rpc/server_routing_validation_diagnostics.go: Include in status
- Tests updated to pass autoPull parameter
Closes #TBD
Wisp = ephemeral vapor produced by the Steam Engine (Gas Town).
This aligns with the metaphor:
- Claude = Fire
- Claude Code = Steam
- Gas Town = Steam Engine
- Wisps = ephemeral vapor it produces
Changes:
- types.Issue.Ephemeral → types.Issue.Wisp
- types.IssueFilter.Ephemeral → types.IssueFilter.Wisp
- JSON field: "ephemeral" → "wisp"
- CLI flag: --ephemeral → --wisp (bd cleanup)
- All tests updated
Note: SQLite column remains "ephemeral" (no migration needed).
This is a breaking change for JSON consumers using 0.33.0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add `bd molecule list` to list template molecules
- Add `bd molecule show` to show molecule details
- Add `bd molecule instantiate` to create work items from templates
- Exclude templates from `bd list` by default (use --include-templates)
- Reject mutations (update/close/delete) to template issues
- Add IncludeTemplates to RPC ListArgs for daemon mode
Templates are marked with is_template=true and are read-only.
Use `bd molecule instantiate` to create editable work items.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(unix): handle Statfs field types for disk space check
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
* fix(freebsd): build disk space check without type mismatch
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
---------
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Enables bd ready --type=merge-request for MQ integration.
The Engineer can now query for ready merge-requests.
Files changed:
- cmd/bd/ready.go: Add -t/--type flag
- internal/types/types.go: Add Type field to WorkFilter
- internal/rpc/protocol.go: Add Type to ReadyArgs
- internal/storage/sqlite/ready.go: Filter by issue_type column
Add ability to filter issues by their pinned status in bd list command.
- Add pinned column to issues table via migration 023
- Add Pinned field to Issue struct and IssueFilter
- Update all storage layer queries to include pinned column
- Add --pinned flag to show only pinned issues
- Add --no-pinned flag to exclude pinned issues
- Update RPC layer to forward pinned filter to daemon mode
- Add pinned to allowedUpdateFields for bd update support
Resolves: beads-p8e
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pin.go and unpin.go to cmd/bd/ for managing pinned issues.
- bd pin <id> sets Pinned=true on an issue
- bd unpin <id> sets Pinned=false on an issue
Also adds Pinned field support to RPC UpdateArgs for daemon mode.
🤝 Slit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This is the final phase of the Edge Schema Consolidation. It removes
the deprecated edge fields (RepliesTo, RelatesTo, DuplicateOf, SupersededBy)
from the Issue struct and all related code.
Changes:
- Remove edge fields from types.Issue struct
- Remove edge field scanning from queries.go and transaction.go
- Update graph_links_test.go to use dependency API exclusively
- Update relate.go to use AddDependency/RemoveDependency
- Update show.go with helper functions for thread traversal via deps
- Update mail_test.go to verify thread links via dependencies
- Add migration 022 to drop columns from issues table
- Fix cycle detection to allow bidirectional relates-to links
- Fix migration 022 to disable foreign keys before table recreation
All edge relationships now use the dependencies table exclusively.
The old Issue fields are fully removed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Commands relate, unrelate, duplicate, and supersede now properly
use RPC Update when daemonClient is available, instead of always
calling store.UpdateIssue() directly and bypassing the daemon.
Added RelatesTo, DuplicateOf, and SupersededBy fields to UpdateArgs
in the RPC protocol, and updated server_issues_epics.go to handle them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Three changes to fix deleted issues resurrecting during bd sync:
1. daemon handleDelete now uses CreateTombstone instead of DeleteIssue
- internal/rpc/server_issues_epics.go
2. sync.go exportToJSONL now includes IncludeTombstones:true
- cmd/bd/sync.go
3. server_export_import_auto.go handleExport and auto-export now include
tombstones in SearchIssues filter
- internal/rpc/server_export_import_auto.go
Also adds README.md documentation for sync.branch mode (bd-dsdh):
- Explains "always dirty" working tree behavior
- Shell alias tip: gs='git status -- ":!.beads/"'
- When to use bd sync --merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- bd mail reply: reply to messages with thread linking via replies_to
- bd show --thread: display full conversation threads
- bd relate/unrelate: bidirectional relates_to links for knowledge graph
- bd duplicate --of: mark issues as duplicates with auto-close
- bd supersede --with: mark issues as superseded with auto-close
- Hooks system: on_create, on_update, on_close, on_message in .beads/hooks/
- RPC protocol: added Sender, Ephemeral, RepliesTo fields to CreateArgs/UpdateArgs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two swarm merge bugs fixed:
1. StatusResponse in protocol.go lost daemon config fields (AutoCommit, AutoPush,
LocalMode, SyncInterval, DaemonMode) during merge 60ae6586. These were added in
4e87ae18 but lost when merging bd-zbyb-gusher which had an outdated protocol.go.
2. Duplicate --type flag definition on updateCmd in show.go causing panic on startup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Allow changing an issue type (bug, feature, task, epic, chore) via the
bd update command. The storage layer already supported issue_type in
allowedUpdateFields, this just exposes it through the CLI.
Changes:
- Add --type/-t flag to updateCmd in show.go
- Add IssueType field to UpdateArgs in protocol.go
- Handle issue_type in updatesFromArgs in server_issues_epics.go
- Add validation using ParseIssueType before update
Example usage:
bd update ab-xyz --type epic
Fixes: #522
Add auto-commit, auto-push, local mode, sync interval, and daemon mode
to the status output when querying a running daemon.
This helps users understand the current daemon configuration without
having to check logs or remember what flags were used at startup.
Changes:
- Add config fields to StatusResponse in protocol.go
- Add SetConfig() method to Server for daemon to set its config
- Update handleStatus() to include config in response
- Update showDaemonStatus() to query and display config via RPC
- Add comprehensive test coverage for new functionality
Co-authored-by: Christian Catalan <crcatala@gmail.com>
Allows changing issue type (task/epic/bug/feature/chore) via bd update --type.
Storage layer already supported it, this adds CLI and RPC support.
Fixes GH#522.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable daemon RPC support for delete operations to trigger auto-sync.
This PR adds delete operation support to the RPC daemon, ensuring that delete operations emit mutation events and trigger auto-sync like other mutating operations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replaces manual working directory save/restore patterns
with Go's built-in `t.Chdir()` helper across 23 test files.
The manual pattern involved calling `os.Getwd()` to save
the original directory, using `defer os.Chdir(origWd)` for
restoration, and manually handling errors during directory
changes. This boilerplate has been replaced with single
`t.Chdir(path)` calls that handle cleanup automatically.
The `t.Chdir()` method automatically restores the working
directory when the test completes, eliminating the need for
manual defer statements and error handling.
Total:
~75 instances replaced (assuming Claude's math is right)
Co-authored-by: Claude <noreply@anthropic.com>
The estimated_minutes field existed in the Issue schema but wasn't exposed
via CLI. This adds:
- --estimate / -e flag to bd create (e.g., bd create "Task" --estimate 120)
- --estimate / -e flag to bd update (e.g., bd update bd-xyz --estimate 60)
- EstimatedMinutes field to RPC CreateArgs and UpdateArgs
- Server-side handling in handleCreate and updatesFromArgs
- Validation for non-negative values
The value is specified in minutes and is useful for planning and
prioritization. The vscode-beads extension already has an Estimate column
that can now be populated.
Fixes#443🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add -u/--unassigned flag to bd ready command to show only issues
with no assignee. This supports the SCAVENGE protocol where polecats
query the 'Salvage Yard' for unassigned ready work.
Changes:
- Add NoAssignee field to WorkFilter struct
- Update SQLite GetReadyWork to filter by empty/null assignee
- Add --unassigned/-u flag to ready command
- Update RPC protocol and daemon handler
- Add test for NoAssignee filter functionality
Fixes: gt-3rp
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Adds the ability to filter ready work for issues with no assignee,
which is useful for the SCAVENGE protocol in Gas Town where polecats
need to query the "Salvage Yard" for unclaimed work.
Changes:
- Add Unassigned bool field to types.WorkFilter
- Add --unassigned/-u flag to bd ready command
- Update SQL query in GetReadyWork to filter for NULL/empty assignee
- Add Unassigned field to RPC ReadyArgs for daemon support
- Add tests for the new functionality
Closes: gt-3rp
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract shared JSONL file discovery logic to internal/utils/path.go.
Both autoimport and beads packages now use this shared implementation.
Changes:
- Add utils.FindJSONLInDir with common logic
- Update autoimport.go to use utils.FindJSONLInDir
- Update beads.go to delegate to utils.FindJSONLInDir
- Update server_export_import_auto.go to use utils.FindJSONLInDir
- Move FindJSONLInDir test to utils/path_test.go
- Fix pre-existing duplicate countIssuesInJSONLFile in init.go
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add FindJSONLInDir helper that correctly prefers issues.jsonl over other
.jsonl files. Previously, glob patterns could return deletions.jsonl or
merge artifacts (beads.base.jsonl, etc.) first alphabetically, causing
issue data to be written to the wrong file.
This fixes the root cause of deletions.jsonl corruption where full issue
objects were written instead of deletion records, leading to all issues
being purged during sync.
Changes:
- Add FindJSONLInDir() in internal/autoimport with proper file selection
- Update AutoImportIfNewer() to use FindJSONLInDir
- Update CheckStaleness() to use FindJSONLInDir
- Update triggerExport() in RPC server to use FindJSONLInDir
- Add comprehensive tests for FindJSONLInDir
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Re-check socket existence after lock check to avoid stale socket state
- If socket is initially missing but daemon lock is held, re-check socket to handle daemon startup race
- Add test TestTryConnectWithTimeout_SocketExistenceRecheck to verify fix
Fixes bd-4owj
Implements flexible error handling for export operations with four policies:
- strict: Fail-fast on any error (default for user exports)
- best-effort: Skip errors with warnings (default for auto-exports)
- partial: Retry then skip with manifest tracking
- required-core: Fail on core data, skip enrichments
Key features:
- Per-project configuration via `bd config set export.error_policy`
- Separate policy for auto-exports: `auto_export.error_policy`
- Retry with exponential backoff (configurable attempts/delay)
- Optional export manifests documenting completeness
- Per-issue encoding error handling
This allows users to choose the right trade-off between data integrity
and system availability for their specific project needs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem**: Export operations called GetLabels() and GetIssueComments()
in a loop for each issue, creating N+1 query pattern. For 100 issues
this created 201 queries instead of 3-5.
**Solution**:
- Added GetCommentsForIssues() batch method to storage interface
- Implemented batch method in SQLite and memory storage backends
- Updated handleExport() and triggerExport() to use batch queries
- Added comprehensive tests for batch operations
**Impact**: Query count reduced from ~201 to ~3-5 for 100 issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements bd-au0.2, completing all P0 tasks in the command standardization epic.
Changes:
- Add --add-label, --remove-label, --set-labels flags to bd update
- Support multiple labels via repeatable flags
- Implement in both daemon and direct modes
- Add comprehensive tests for all label operations
The bd update command now supports:
bd update <id> --add-label <label> # Add one or more labels
bd update <id> --remove-label <label> # Remove one or more labels
bd update <id> --set-labels <labels> # Replace all labels
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements a new 'bd count' command that provides efficient issue counting
with filtering and grouping capabilities.
Features:
- Basic count: Returns total count of issues matching filters
- All filtering options from 'bd list' (status, priority, type, assignee, labels, dates, etc.)
- Grouping via --by-* flags: status, priority, type, assignee, label
- JSON output support for both simple and grouped counts
- Both daemon and direct mode support
Implementation:
- Added OpCount operation and CountArgs to RPC protocol
- Added Count() method to RPC client
- Implemented handleCount() server-side handler with optimized bulk label fetching
- Created cmd/bd/count.go with full CLI implementation
Performance optimization:
- Pre-fetches all labels in a single query when using --by-label to avoid N+1 queries
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated all test files to pass context.Background() as the first parameter
to sqlite.New() calls to match the updated function signature.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Multiple test files were still using the old sqlite.New(path) signature
instead of the new sqlite.New(ctx, path) signature. This was causing
compilation failures in the test suite.
Fixed files:
- internal/importer/importer_test.go
- internal/importer/external_ref_test.go
- internal/importer/timestamp_test.go
- internal/rpc/limits_test.go
- internal/rpc/list_filters_test.go
- internal/rpc/rpc_test.go
- internal/rpc/status_test.go
- internal/syncbranch/syncbranch_test.go
Identified and tagged obviously-slow integration tests with
`//go:build integration` to exclude them from default test runs.
This is step 1 of fixing test performance. The real fix is in
bd-1rh: refactoring tests to use shared DB setup instead of
creating 279 separate databases.
Tagged files:
- cmd/bd: 8 files (CLI tests, git ops, performance benchmarks)
- internal: 8 files (integration tests, E2E tests)
Issues:
- bd-1rh: Main issue tracking test performance
- bd-c49: Audit all tests and create grouping plan (next step)
- bd-y6d: POC refactor of create_test.go
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Complete implementation of signal-aware context propagation for graceful
cancellation across all commands and storage operations.
Key changes:
1. Signal-aware contexts (bd-rtp):
- Added rootCtx/rootCancel in main.go using signal.NotifyContext()
- Set up in PersistentPreRun, cancelled in PersistentPostRun
- Daemon uses same pattern in runDaemonLoop()
- Handles SIGINT/SIGTERM for graceful shutdown
2. Context propagation (bd-yb8):
- All commands now use rootCtx instead of context.Background()
- sqlite.New() receives context for cancellable operations
- Database operations respect context cancellation
- Storage layer propagates context through all queries
3. Cancellation tests (bd-2o2):
- Added import_cancellation_test.go with comprehensive tests
- Added export cancellation test in export_test.go
- Tests verify database integrity after cancellation
- All cancellation tests passing
Fixes applied during review:
- Fixed rootCtx lifecycle (removed premature defer from PersistentPreRun)
- Fixed test context contamination (reset rootCtx in test cleanup)
- Fixed export tests missing context setup
Impact:
- Pressing Ctrl+C during import/export now cancels gracefully
- No database corruption or hanging transactions
- Clean shutdown of all operations
Tested:
- go build ./cmd/bd ✓
- go test ./cmd/bd -run TestImportCancellation ✓
- go test ./cmd/bd -run TestExportCommand ✓
- Manual Ctrl+C testing verified
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address critical resource leaks and error handling issues
Fixes 5 critical and high-priority issues identified in codebase analysis:
1. bd-vavh: Fix row iterator resource leak in recursive dependency queries
- Move defer rows.Close() to execute on all code paths
- Previously leaked connections on scan errors
- Location: internal/storage/sqlite/sqlite.go:1121-1145
2. bd-qhws: Configure database connection pool limits for daemon mode
- Set MaxOpenConns to runtime.NumCPU() + 1 for file-based databases
- Prevents connection exhaustion under concurrent RPC load
- Only affects daemon mode (long-running server)
- Location: internal/storage/sqlite/sqlite.go:108-125
3. bd-jo38: Add WaitGroup tracking to FileWatcher goroutines
- Track goroutines with sync.WaitGroup for graceful shutdown
- Wait for goroutines to finish before cleanup in Close()
- Prevents race condition on debouncer access during shutdown
- Location: cmd/bd/daemon_watcher.go (Start, startPolling, Close)
4. bd-2d5r: Fix silent error handling in RPC response writing
- writeResponse now returns errors instead of ignoring them
- Prevents sending partial JSON and client hangs
- Closes connection on marshal/write errors
- Location: internal/rpc/server_lifecycle_conn.go:227-246
5. bd-zqmb: Fix goroutine leak in daemon restart
- Add 10-second timeout to daemon Wait() goroutine
- Kill process if it doesn't fork within timeout
- Prevents goroutine accumulation on restart failures
- Location: cmd/bd/daemons.go:250-268
All changes follow Go best practices and maintain backward compatibility.
* Add feature request for .beads/README.md generation during init
Created bd-m7ge to automatically generate a promotional/documentation
README in the .beads directory when running 'bd init'. This will help
advertise Beads in open source repositories and provide quick reference
documentation for developers using AI coding agents.
The README will include:
- Brief explanation of Beads (AI-native issue tracking)
- Link to steveyegge/beads repository
- Quick reference of essential commands
- Compelling messaging to encourage adoption
When using 'bd create --parent <id>', the system now automatically
creates a parent-child dependency linking the child to the parent.
This fixes epic status reporting which was showing zero children.
Fixes#318 (bd-jijf)
Changes:
- cmd/bd/create.go: Add parent-child dependency after issue creation
- internal/rpc/server_issues_epics.go: Add same fix for daemon mode
Tested:
- Created epic with children, verified epic status shows correct count
- Verified closing all children makes epic eligible for closure
- All tests pass
Amp-Thread-ID: https://ampcode.com/threads/T-f1a1aee1-03bd-4e62-a63c-c1d339f8300b
Co-authored-by: Amp <amp@ampcode.com>