diffbutler 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. diffbutler/__init__.py +1 -0
  2. diffbutler/__main__.py +6 -0
  3. diffbutler/config/__init__.py +1 -0
  4. diffbutler/config/default.toml +91 -0
  5. diffbutler/config/diagnostics.py +90 -0
  6. diffbutler/config/file.py +30 -0
  7. diffbutler/config/instance.py +55 -0
  8. diffbutler/config/settings.py +297 -0
  9. diffbutler/config/setup.py +92 -0
  10. diffbutler/config/transport.py +65 -0
  11. diffbutler/core/__init__.py +1 -0
  12. diffbutler/core/application.py +103 -0
  13. diffbutler/core/context.py +54 -0
  14. diffbutler/core/dashboard.py +393 -0
  15. diffbutler/core/doctor.py +200 -0
  16. diffbutler/core/lifecycle.py +70 -0
  17. diffbutler/core/managed.py +441 -0
  18. diffbutler/core/native.py +301 -0
  19. diffbutler/core/polling.py +118 -0
  20. diffbutler/core/process.py +64 -0
  21. diffbutler/core/repositories.py +260 -0
  22. diffbutler/core/reset.py +34 -0
  23. diffbutler/core/resources.py +50 -0
  24. diffbutler/core/review.py +331 -0
  25. diffbutler/core/setup.py +232 -0
  26. diffbutler/core/snapshot.py +70 -0
  27. diffbutler/core/sync.py +259 -0
  28. diffbutler/core/worker.py +205 -0
  29. diffbutler/core/workspaces.py +139 -0
  30. diffbutler/domain/__init__.py +1 -0
  31. diffbutler/domain/dashboard.py +63 -0
  32. diffbutler/domain/diff.py +285 -0
  33. diffbutler/domain/forge.py +123 -0
  34. diffbutler/domain/lifecycle.py +51 -0
  35. diffbutler/domain/polling.py +92 -0
  36. diffbutler/domain/process.py +29 -0
  37. diffbutler/domain/redaction.py +61 -0
  38. diffbutler/domain/resources.py +50 -0
  39. diffbutler/domain/review.py +167 -0
  40. diffbutler/domain/sync.py +196 -0
  41. diffbutler/forges/__init__.py +1 -0
  42. diffbutler/forges/base.py +39 -0
  43. diffbutler/forges/diagnostics.py +104 -0
  44. diffbutler/forges/github/__init__.py +1 -0
  45. diffbutler/forges/github/adapter.py +352 -0
  46. diffbutler/forges/github/auth.py +49 -0
  47. diffbutler/forges/github/http.py +326 -0
  48. diffbutler/forges/github/mapping.py +157 -0
  49. diffbutler/forges/github/setup.py +86 -0
  50. diffbutler/forges/registry.py +23 -0
  51. diffbutler/py.typed +0 -0
  52. diffbutler/reviewers/__init__.py +1 -0
  53. diffbutler/reviewers/base.py +101 -0
  54. diffbutler/reviewers/extraction.py +198 -0
  55. diffbutler/reviewers/native.py +238 -0
  56. diffbutler/reviewers/parsing.py +247 -0
  57. diffbutler/reviewers/presets.toml +12 -0
  58. diffbutler/reviewers/process.py +78 -0
  59. diffbutler/reviewers/prompt.py +148 -0
  60. diffbutler/reviewers/redaction.py +30 -0
  61. diffbutler/reviewers/resources.py +162 -0
  62. diffbutler/sources/__init__.py +1 -0
  63. diffbutler/sources/diagnostics.py +100 -0
  64. diffbutler/sources/fetch.py +285 -0
  65. diffbutler/sources/git.py +461 -0
  66. diffbutler/sources/remotes.py +82 -0
  67. diffbutler/store/__init__.py +1 -0
  68. diffbutler/store/base.py +88 -0
  69. diffbutler/store/dashboard.py +201 -0
  70. diffbutler/store/database.py +172 -0
  71. diffbutler/store/lifecycle.py +247 -0
  72. diffbutler/store/migrations/__init__.py +1 -0
  73. diffbutler/store/migrations/env.py +14 -0
  74. diffbutler/store/migrations/versions/0001_initial.py +147 -0
  75. diffbutler/store/migrations/versions/0002_monitoring.py +38 -0
  76. diffbutler/store/migrations/versions/0003_reviews.py +72 -0
  77. diffbutler/store/migrations/versions/0004_claims.py +22 -0
  78. diffbutler/store/migrations/versions/0005_results.py +27 -0
  79. diffbutler/store/migrations/versions/0006_anchors.py +17 -0
  80. diffbutler/store/migrations/versions/0007_lifecycle.py +36 -0
  81. diffbutler/store/migrations/versions/0008_processes.py +30 -0
  82. diffbutler/store/migrations/versions/0009_dashboard.py +43 -0
  83. diffbutler/store/migrations/versions/0010_run_reasons.py +17 -0
  84. diffbutler/store/migrations/versions/__init__.py +1 -0
  85. diffbutler/store/models.py +290 -0
  86. diffbutler/store/process.py +75 -0
  87. diffbutler/store/queue.py +289 -0
  88. diffbutler/store/review.py +583 -0
  89. diffbutler/ui/__init__.py +1 -0
  90. diffbutler/ui/cli/__init__.py +532 -0
  91. diffbutler/ui/cli/editor.py +33 -0
  92. diffbutler/ui/tui/__init__.py +1 -0
  93. diffbutler/ui/tui/_fake_agent.py +47 -0
  94. diffbutler/ui/tui/_foreground_exec.py +23 -0
  95. diffbutler/ui/tui/app.py +34 -0
  96. diffbutler/ui/tui/browse.py +119 -0
  97. diffbutler/ui/tui/browser.py +42 -0
  98. diffbutler/ui/tui/dashboard.py +926 -0
  99. diffbutler/ui/tui/foreground.py +103 -0
  100. diffbutler/ui/tui/handoff_probe.py +184 -0
  101. diffbutler-0.5.0.dist-info/METADATA +170 -0
  102. diffbutler-0.5.0.dist-info/RECORD +105 -0
  103. diffbutler-0.5.0.dist-info/WHEEL +4 -0
  104. diffbutler-0.5.0.dist-info/entry_points.txt +3 -0
  105. diffbutler-0.5.0.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,103 @@
1
+ """POSIX terminal ownership experiment; call only while the UI is suspended."""
2
+
3
+ import asyncio
4
+ import os
5
+ import signal
6
+ import sys
7
+ import termios
8
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
9
+ from pathlib import Path
10
+
11
+ from diffbutler.domain.lifecycle import ForegroundExitUncertain as ForegroundExitUncertain
12
+ from diffbutler.reviewers.native import stop_process_group
13
+
14
+
15
+ def _give_terminal(fd: int, process_group: int) -> None:
16
+ # Reclaiming the terminal is a background-process operation: block SIGTTOU.
17
+ previous = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
18
+ try:
19
+ os.tcsetpgrp(fd, process_group)
20
+ finally:
21
+ signal.signal(signal.SIGTTOU, previous)
22
+
23
+
24
+ async def run_foreground(
25
+ argv: Sequence[str],
26
+ *,
27
+ cwd: Path | None = None,
28
+ env: Mapping[str, str] | None = None,
29
+ started: Callable[[int], Awaitable[None]] | None = None,
30
+ ) -> int:
31
+ """Attach one process group, await without blocking asyncio, always restore TTY.
32
+
33
+ The caller serializes handoffs. This is not a reviewer sandbox or credential
34
+ boundary: the foreground command inherits the user's environment.
35
+ """
36
+ if not argv:
37
+ raise ValueError("An agent command is required")
38
+ fd = sys.stdin.fileno()
39
+ if not os.isatty(fd) or os.tcgetpgrp(fd) != os.getpgrp():
40
+ raise RuntimeError("Handoff requires ownership of a controlling terminal")
41
+ owner = os.tcgetpgrp(fd)
42
+ modes = termios.tcgetattr(fd)
43
+ read_gate, write_gate = os.pipe()
44
+ spawn: asyncio.Task[asyncio.subprocess.Process] | None = None
45
+ try:
46
+ spawn = asyncio.create_task(
47
+ asyncio.create_subprocess_exec(
48
+ sys.executable,
49
+ "-m",
50
+ "diffbutler.ui.tui._foreground_exec",
51
+ str(read_gate),
52
+ *argv,
53
+ stdin=fd,
54
+ stdout=sys.stdout.fileno(),
55
+ stderr=sys.stderr.fileno(),
56
+ pass_fds=(read_gate,),
57
+ process_group=0,
58
+ cwd=cwd,
59
+ env=env,
60
+ )
61
+ )
62
+ process = await asyncio.shield(spawn)
63
+ if started is not None:
64
+ await started(process.pid)
65
+ _give_terminal(fd, process.pid)
66
+ os.write(write_gate, b"1")
67
+ return await process.wait()
68
+ finally:
69
+
70
+ async def stop() -> None:
71
+ try:
72
+ if spawn is not None:
73
+ try:
74
+ process = await spawn
75
+ except OSError:
76
+ return
77
+ await stop_process_group(process.pid)
78
+ await process.wait()
79
+ finally:
80
+ os.close(read_gate)
81
+ os.close(write_gate)
82
+
83
+ stopping = asyncio.create_task(stop())
84
+ cancelled = False
85
+ try:
86
+ while True:
87
+ try:
88
+ await asyncio.shield(stopping)
89
+ break
90
+ except asyncio.CancelledError:
91
+ cancelled = True
92
+ if stopping.done():
93
+ stopping.result()
94
+ break
95
+ except Exception:
96
+ raise ForegroundExitUncertain(
97
+ "Foreground process exit could not be verified"
98
+ ) from None
99
+ finally:
100
+ _give_terminal(fd, owner)
101
+ termios.tcsetattr(fd, termios.TCSANOW, modes)
102
+ if cancelled:
103
+ raise asyncio.CancelledError
@@ -0,0 +1,184 @@
1
+ """Runnable terminal experiment. No forge access or automatic model calls."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import platform
8
+ import sys
9
+ import tempfile
10
+ import time
11
+ import uuid
12
+ from importlib.metadata import version
13
+ from pathlib import Path
14
+ from typing import ClassVar
15
+
16
+ from textual.app import ComposeResult
17
+ from textual.binding import BindingType
18
+ from textual.events import Resize
19
+ from textual.widgets import DataTable, Footer, Header, Static
20
+
21
+ from diffbutler.ui.tui.app import TerminalApp
22
+ from diffbutler.ui.tui.foreground import run_foreground
23
+
24
+
25
+ class HandoffProbe(TerminalApp):
26
+ """Three fake PRs, a live heartbeat, and an explicitly launched agent."""
27
+
28
+ TITLE = "DiffButler — terminal handoff experiment"
29
+ BINDINGS: ClassVar[list[BindingType]] = [
30
+ ("c", "handoff", "Resume agent"),
31
+ ("f", "fail", "Bad command"),
32
+ ("q", "quit", "Quit"),
33
+ ]
34
+ CSS = "DataTable { height: 8; } Static { margin: 1 2; }"
35
+
36
+ def __init__(self, directory: Path, command: list[str], events: Path) -> None:
37
+ super().__init__()
38
+ self.directory = directory
39
+ self.command = command
40
+ self.events_path = events
41
+ self.ticks = 0
42
+ self.attached = False
43
+ self.session = directory / "fake-session.json"
44
+ self.session.write_text(
45
+ json.dumps({"id": str(uuid.uuid4()), "resumes": 0, "seed": "synthetic source review"})
46
+ )
47
+
48
+ def record(self, event: str, **fields: object) -> None:
49
+ with self.events_path.open("a") as output:
50
+ output.write(
51
+ json.dumps(
52
+ {"event": event, "time": time.monotonic(), "ticks": self.ticks, **fields}
53
+ )
54
+ + "\n"
55
+ )
56
+
57
+ def compose(self) -> ComposeResult:
58
+ yield Header()
59
+ yield DataTable[str](cursor_type="row")
60
+ yield Static("Select a row, then v. The default agent is an offline fake.", id="result")
61
+ yield Static("", id="heartbeat")
62
+ yield Footer()
63
+
64
+ def on_mount(self) -> None:
65
+ table = self.query_one(DataTable[str])
66
+ table.add_columns("PR", "Status")
67
+ table.add_rows([("demo#1", "Ready"), ("demo#2", "Findings"), ("demo#3", "Waiting")])
68
+ table.focus()
69
+ self.run_worker(self.heartbeat())
70
+ self.run_worker(self.background_output())
71
+ self.record(
72
+ "ready",
73
+ system=platform.system(),
74
+ python=platform.python_version(),
75
+ textual=version("textual"),
76
+ )
77
+
78
+ def on_resize(self, event: Resize) -> None:
79
+ self.record("resize", width=event.size.width, height=event.size.height)
80
+
81
+ async def heartbeat(self) -> None:
82
+ while True:
83
+ await asyncio.sleep(0.1)
84
+ self.ticks += 1
85
+ self.record("tick", attached=self.attached)
86
+ if not self.attached:
87
+ self.query_one("#heartbeat", Static).update(f"Background ticks: {self.ticks}")
88
+
89
+ async def background_output(self) -> None:
90
+ with (self.directory / "background.log").open("wb") as output:
91
+ process = await asyncio.create_subprocess_exec(
92
+ sys.executable,
93
+ "-m",
94
+ "diffbutler.ui.tui._fake_agent",
95
+ "--ticker",
96
+ stdin=asyncio.subprocess.DEVNULL,
97
+ stdout=output,
98
+ stderr=output,
99
+ start_new_session=True,
100
+ )
101
+ try:
102
+ await process.wait()
103
+ finally:
104
+ if process.returncode is None:
105
+ process.kill()
106
+ await process.wait()
107
+
108
+ def action_handoff(self) -> None:
109
+ if not self.attached:
110
+ self.attached = True
111
+ self.run_worker(self.handoff(self.command))
112
+
113
+ def action_fail(self) -> None:
114
+ if not self.attached:
115
+ self.attached = True
116
+ self.run_worker(self.handoff(["/nonexistent/diffbutler-agent"]))
117
+
118
+ async def handoff(self, command: list[str]) -> None:
119
+ table = self.query_one(DataTable[str])
120
+ row = table.cursor_row
121
+ before = self.ticks
122
+ error: BaseException | None = None
123
+ result: int | None = None
124
+ self.record("handoff", row=row)
125
+ try:
126
+ with self.suspend():
127
+ # Textual 8.2.8 resumes *after* yield, not in a finally block.
128
+ # Defer all exceptions until outside the suspension context.
129
+ try:
130
+ result = await run_foreground(command)
131
+ except BaseException as caught:
132
+ error = caught
133
+ if isinstance(error, asyncio.CancelledError):
134
+ raise error
135
+ if error is not None and not isinstance(error, Exception):
136
+ raise error
137
+ status = f"Launch failed: {error}" if error else f"Agent exited: {result}"
138
+ self.query_one("#result", Static).update(
139
+ f"{status}. Background advanced {self.ticks - before} ticks. Press c to resume."
140
+ )
141
+ self.record(
142
+ "returned",
143
+ code=result,
144
+ error=str(error) if error else None,
145
+ row=table.cursor_row,
146
+ tick_delta=self.ticks - before,
147
+ foreground=os.tcgetpgrp(sys.stdin.fileno()) == os.getpgrp(),
148
+ background_bytes=(self.directory / "background.log").stat().st_size,
149
+ )
150
+ finally:
151
+ self.attached = False
152
+ self.record("released")
153
+
154
+
155
+ def main() -> None:
156
+ parser = argparse.ArgumentParser(description=__doc__)
157
+ parser.add_argument("--events", type=Path, help="Write diagnostic JSONL (no agent output)")
158
+ parser.add_argument(
159
+ "command",
160
+ nargs=argparse.REMAINDER,
161
+ help="Optional foreground argv after --, e.g. codex resume SESSION_ID",
162
+ )
163
+ args = parser.parse_args()
164
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
165
+ parser.error("Run in an interactive terminal; Linux and macOS are supported")
166
+ with tempfile.TemporaryDirectory(prefix="diffbutler-handoff-") as temporary:
167
+ directory = Path(temporary)
168
+ command = list[str](args.command)
169
+ if command[:1] == ["--"]:
170
+ command = command[1:]
171
+ if not command:
172
+ command = [
173
+ sys.executable,
174
+ "-m",
175
+ "diffbutler.ui.tui._fake_agent",
176
+ "--session",
177
+ str(directory / "fake-session.json"),
178
+ ]
179
+ events = Path(args.events) if args.events else directory / "events.jsonl"
180
+ HandoffProbe(directory, command, events).run()
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffbutler
3
+ Version: 0.5.0
4
+ Summary: A terminal dashboard for human-directed AI code reviews.
5
+ Keywords: code-review,github,terminal,ai
6
+ License-Expression: MIT
7
+ License-File: LICENSE.txt
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: 3.15
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: aiosqlite>=0.22.1
17
+ Requires-Dist: alembic>=1.19.1
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: pydantic>=2.13.5
20
+ Requires-Dist: pydantic-settings>=2.15.0
21
+ Requires-Dist: rich>=15.0.0
22
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.52
23
+ Requires-Dist: textual>=8.2.8
24
+ Requires-Dist: tomlkit>=0.15.1
25
+ Requires-Dist: typer>=0.27.2
26
+ Requires-Python: >=3.14
27
+ Description-Content-Type: text/markdown
28
+
29
+ # DiffButler
30
+
31
+ A terminal dashboard for human-directed AI code reviews on GitHub.
32
+
33
+ Keep track of what each pull request needs next: AI review, your review, or author
34
+ changes. Start an AI review, discuss its findings in the agent's own session, and
35
+ finish your review in the browser.
36
+
37
+ ![DiffButler dashboard showing pull requests and the next review action](diffbutler.png)
38
+
39
+ ## Install
40
+
41
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and Git.
42
+ From a local checkout of this repository, run:
43
+
44
+ ```sh
45
+ uv tool install --python 3.14 .
46
+ ```
47
+
48
+ uv manages Python and dependencies in an isolated environment. No virtual environment
49
+ activation is needed. If `diffbutler` is not on your PATH, run `uv tool update-shell`
50
+ and restart your shell.
51
+
52
+ Install and sign in to Codex CLI or Claude Code before requesting AI reviews.
53
+ The native presets support Codex CLI or Claude Code on Linux and macOS without version checks.
54
+ Reviews use your normal agent settings, including model and reasoning effort, with
55
+ background permission and output overrides. New conversations remain in the agent’s
56
+ own history after DiffButler cleans up its workspaces; existing private sessions
57
+ retain their original home for resume.
58
+ See [supported agents and validation](docs/native-reviews.md) for details.
59
+
60
+ ## Use
61
+
62
+ Use authenticated GitHub access, including for public repositories. If you already
63
+ use `gh auth login`, export its token before launching DiffButler:
64
+
65
+ ```sh
66
+ export GITHUB_TOKEN="$(gh auth token)"
67
+ ```
68
+
69
+ Alternatively, create a [fine-grained personal access token](https://github.com/settings/personal-access-tokens)
70
+ for the repositories you want to review with **Pull requests: Read-only** permission,
71
+ and set it as `GITHUB_TOKEN`. Anonymous access is a last resort for public repositories;
72
+ its low API quota makes it unsuitable for regular polling. See the
73
+ [authentication guide](docs/github.md#authentication-options) for host/account selection,
74
+ HTTPS versus SSH Git setup, and token permissions.
75
+
76
+ From the GitHub repository you want to review, launch:
77
+
78
+ ```sh
79
+ diffbutler
80
+ ```
81
+
82
+ First-run setup detects the local repository, looks up your GitHub username using
83
+ the selected token, asks for your preferred agent, and saves your choices. If username detection
84
+ is unavailable, setup asks for it. Choose the recommended token access for public repositories;
85
+ private repositories require it. Setup asks for the token's environment variable name
86
+ and validates access. It never saves the token.
87
+ New configs silently enable both Codex and Claude when found in `PATH`; setup asks
88
+ only which detected agent should be the default. Missing agents get commented-out
89
+ entries you can enable after installation.
90
+ Later launches reuse your configuration.
91
+
92
+ The dashboard shows active reviews, including followed PRs whose GitHub review
93
+ requests have disappeared after you posted feedback. Waiting-for-author reviews
94
+ stay visible across refreshes and restarts. If an approved PR receives new commits,
95
+ it reappears as `done+` at the end of the queue without starting another AI review.
96
+ Authenticated polling slows when API quota is scarce and speeds back up when it
97
+ recovers, accounting for refresh cost and time until the quota resets.
98
+
99
+ Select a PR with the arrow keys, then work through the review:
100
+
101
+ 1. Optionally press `n` to change the current configured AI reviewer for all PRs.
102
+ Press `r` to start a background AI review. Reviews start only when you request them.
103
+ The agent is instructed to read the PR description, discussion, and earlier review
104
+ threads and replies before reviewing the local code, reporting any unavailable context.
105
+ DiffButler caches full Git history and reviews changes from the PR merge base
106
+ to its head, excluding unrelated changes made later on the target branch.
107
+ 2. A successful review shows `approve` only when the AI explicitly reports no findings;
108
+ otherwise it shows `check`. Press `Enter` to read the results, or `c` to resume
109
+ the saved agent conversation.
110
+ If the agent exits normally but DiffButler cannot parse its output, the task also
111
+ shows `check`; use `c` to inspect the saved conversation or `L` for captured output.
112
+ Changing reviewers leaves that conversation available through `c`; pressing `r`
113
+ starts a new run with the current reviewer and replaces the current review details.
114
+ Ask questions and explicitly tell the agent which comments to post, if any.
115
+ 3. Exit the agent to return to the dashboard, which refreshes activity from GitHub.
116
+ Press `w` if you are waiting for author changes, or `o` to review in your browser.
117
+ 4. After approving on GitHub, press `m` to mark the task complete locally. DiffButler
118
+ also detects your approval, closure, or merge and cleans up owned session resources
119
+ while retaining review history.
120
+
121
+ `Space` performs the selected PR's next action: start a review, discuss findings,
122
+ or open the browser. An AI review never counts as your approval. Background analysis
123
+ uses the agent's [native permission controls](docs/native-reviews.md#native-permissions-and-authentication)
124
+ and read-only review instructions; source sent to the agent may go to its model provider.
125
+
126
+ - `/`: Filter the queue.
127
+ - `a`: Toggle monitoring all open PRs in configured repositories.
128
+ - `b`: Browse accessible PRs and follow one.
129
+ - `R`: Refresh from GitHub.
130
+ - `k`: Stop the selected PR's review and reset its local tracking and owned resources.
131
+ It disappears until a forge refresh rediscovers it; review history is retained.
132
+ - `h`: View completed review history.
133
+ - `?`: Show all keyboard shortcuts.
134
+ - `q`: Quit.
135
+
136
+ See the [dashboard guide](docs/dashboard.md) for all actions and
137
+ [configuration guide](docs/configuration.md) for settings.
138
+
139
+ Your own PRs are hidden from the review queue by default, including direct and team
140
+ requests. Set `include_own_prs = true` in `[ui]` to review them, such as PRs an agent
141
+ created using your account. PRs assigned to you are also hidden by default, even when
142
+ you are requested directly or through a team. Set `include_assigned_prs = true` in
143
+ `[ui]` to include them.
144
+
145
+ ## Reset
146
+
147
+ Close running DiffButler sessions, then run:
148
+
149
+ ```sh
150
+ diffbutler reset
151
+ ```
152
+
153
+ DiffButler refuses a second command or dashboard using the same data home, including
154
+ `reset`, while another instance is running. Its instance lock releases on process
155
+ exit; a leftover PID file does not block startup. See the
156
+ [locking details](docs/configuration.md#single-instance) before handling Git lock files
157
+ or resetting after a crash.
158
+
159
+ After confirmation, this permanently removes DiffButler's standard configuration,
160
+ data, and state directories, including review history, saved sessions, and diagnostic
161
+ logs. Custom files outside those directories are preserved. Use `diffbutler reset --force`
162
+ to skip confirmation. Run `diffbutler` again to repeat first-run setup.
163
+
164
+ ## Contributing
165
+
166
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks.
167
+
168
+ ## License
169
+
170
+ [MIT](LICENSE.txt).
@@ -0,0 +1,105 @@
1
+ diffbutler/__init__.py,sha256=NJV3PMyDUxERtwy3enDYnGmANp5J8w9oMQTFIGf8Ic4,50
2
+ diffbutler/__main__.py,sha256=Lso6m1-RzLUewvu8mw-fmB1XwRtbptBVdjYvIKoZegQ,115
3
+ diffbutler/config/__init__.py,sha256=AQ93W6L1AQalhhlxjMTuq0VIE6DY_oGtcW6SCD7QEQ8,44
4
+ diffbutler/config/default.toml,sha256=o92lw3YwcdGwrheP38uv6mILvbnBKiO1QCJIR5VsiMs,3165
5
+ diffbutler/config/diagnostics.py,sha256=jDqHJzfcuEd9c40Oqkb5LnGHpx5BQuMbqezmNlz_5Ww,2994
6
+ diffbutler/config/file.py,sha256=joa-7cIolowonyFxcRP1ioeCYrQ7vC7w5S06OHMiJkI,1176
7
+ diffbutler/config/instance.py,sha256=jJoMv06EGbOsO5llgWidzsp37-wbRu4F9rDOio_-pk4,2053
8
+ diffbutler/config/settings.py,sha256=0InAIv0QiqPVXmXTTwn8Pno6M-6RJdak79qK9UegRuA,10759
9
+ diffbutler/config/setup.py,sha256=FZIMtGOWaGqmohjAwnolQFLltMbd5XbPAjcCzDwP-kY,4163
10
+ diffbutler/config/transport.py,sha256=4FqFid7IzITNJ0Z5oq0kQGQMLt2PnviDuo9GpJdw_10,2846
11
+ diffbutler/core/__init__.py,sha256=pXkfhbiHggzoVmZW6eGwvK_6JEJCCB7aTgXvNUi3sK0,43
12
+ diffbutler/core/application.py,sha256=QQ8uBQLWMUYOnBfqo8ENZ0faBxzMtA5XkukpGqz4ZOk,3682
13
+ diffbutler/core/context.py,sha256=IPQofKZafYU0riM5l4rqD7MX2V-NZuxu_Cl8melYMPI,2415
14
+ diffbutler/core/dashboard.py,sha256=E0swv9ms_DJCFupp_Muod1VNuZayEc_FtmlqsOSMRjQ,16664
15
+ diffbutler/core/doctor.py,sha256=KT_svOkNlOYZ7HSqHFLSSuN3qpQx2Ykww22JPUvPD9M,7473
16
+ diffbutler/core/lifecycle.py,sha256=vjdWguWn9YwGoef8oXAkEV9a4QoH4xIQl_u0JoPujsE,2862
17
+ diffbutler/core/managed.py,sha256=3rtMAvNLsGcQaLAKjYyp8UEqW0oPHGxuW-4zglsRX7g,18966
18
+ diffbutler/core/native.py,sha256=3MNBsAEahdxE1zOIm_TY5C8Y5oTP7Mad7kgcouRqgDg,12533
19
+ diffbutler/core/polling.py,sha256=icvNRa4lu41H_rx7d7h4noAwXEq71fO5neh4EDTzEY4,4755
20
+ diffbutler/core/process.py,sha256=MPPVlIdd_tpKidIkXZtwvU5CyVgvS2KChs6j3NZqoos,2439
21
+ diffbutler/core/repositories.py,sha256=L4d73z6gGUN7G9azEcyUD-9XeN4UF6MteO1Zgo78NJE,11398
22
+ diffbutler/core/reset.py,sha256=qRVp2wkGFQK_ir1cwYQqbD8jixMB-80PWOUMNMeyX-I,1043
23
+ diffbutler/core/resources.py,sha256=EuAn0eGc-EYlOfebVuuzrWccZ8HmVpAbPzol7AfO-Pw,1661
24
+ diffbutler/core/review.py,sha256=YnCZ9nI6a-kr0Poi0_EUFhpywSBQ-5-aTye3CwsXGsc,11747
25
+ diffbutler/core/setup.py,sha256=FDBjWp_Tq036FRCUiJUxlzfQSgFBYhq1TsqsYz-xCiI,9312
26
+ diffbutler/core/snapshot.py,sha256=5GAffiprN3a3Ogk9Kxhp37LImngWc72UYxs8d_laju8,2505
27
+ diffbutler/core/sync.py,sha256=C8f8pjvDeGRFBUzeX17IchnJ85ksy72GcNUo_E-moKs,11496
28
+ diffbutler/core/worker.py,sha256=7CFULNLTnW1NZMq-K13BbbPMQEmkpqjwK1Zl-7-qNGU,8672
29
+ diffbutler/core/workspaces.py,sha256=LBcRiE38AJ4PD2xem90yySum1QU7BgyyryBhB28yrMw,5620
30
+ diffbutler/domain/__init__.py,sha256=oQI2BkzgjfgYC-74YMgfWk7wWIHvfqQsNuUy90ZmgSY,47
31
+ diffbutler/domain/dashboard.py,sha256=3_edpG1olIb91EOoS1S4Cq-EJUu_xL21LUxaQMwbth0,1392
32
+ diffbutler/domain/diff.py,sha256=E1QW82RYm0QjZTgeXLueJfFYfsVv2Xto_BtKCVlrpUQ,10104
33
+ diffbutler/domain/forge.py,sha256=D1SmLsQTChK5RQkSxaDZ9JfXqjcfg6t6lqnrLaMxH6E,2987
34
+ diffbutler/domain/lifecycle.py,sha256=Gcv-Vg-nLLRQ_KpOtbiA01LWaCCVKfLQgDUByHlEQt0,1053
35
+ diffbutler/domain/polling.py,sha256=qFL5x6wWhynVp1SQd0ttQ7nYzznhWvF4RmofdT8LX8U,3561
36
+ diffbutler/domain/process.py,sha256=1B8E75W-GFZdqGI-04EDsOTtTZUc50nM5fYWt5pjdLY,851
37
+ diffbutler/domain/redaction.py,sha256=SRN3PZ1uP-eSUCROB4x0O7w8gUINlqZmQ2jyeaHx0B8,2191
38
+ diffbutler/domain/resources.py,sha256=8Fplm5PMI6qHG1sPyZfT2vmpEc5_9BrRqlDKY0SlxDA,1713
39
+ diffbutler/domain/review.py,sha256=0kgZEUXY7teOnJDVy8Bku18sLNfGEXRY5QuZBGpAjwc,3976
40
+ diffbutler/domain/sync.py,sha256=4noDpNVYJNVpfDmya7GSpk6fm9qhgdgwnnIEJM_Ika0,6821
41
+ diffbutler/forges/__init__.py,sha256=IvwheQN2h9ZJP9k3zkdTDjNzGKh4jtq4yCE4yJf-bCo,48
42
+ diffbutler/forges/base.py,sha256=MyfxRymVIgL9Mud_ilFz1wTnDaXwEjXF12SAGQr-jus,1253
43
+ diffbutler/forges/diagnostics.py,sha256=OBoqo471wql9MgBtmlmdftUOgD3S1UdqNUvsFZMBLt0,3372
44
+ diffbutler/forges/github/__init__.py,sha256=8MHrBneaDew5CAsPWdCYAnbw_sZKnxzrYfLRfxHt_ww,60
45
+ diffbutler/forges/github/adapter.py,sha256=KTw3-BJLUIiJF2Ero3SrNihMuuRr9SyCi6t7OBBKirU,16143
46
+ diffbutler/forges/github/auth.py,sha256=7VU-xuvovtIH6JTnUR1kPtjjsfDlzJeDlXbt03EaQdw,1623
47
+ diffbutler/forges/github/http.py,sha256=o5BH-43llnNa7IbXDt12DSSGoM6NYZlSZiluV31bLE0,13799
48
+ diffbutler/forges/github/mapping.py,sha256=QmXkiuk48XQOrP48sb1Z2BAotMUPBY1tRXjBEcJ-cuM,4485
49
+ diffbutler/forges/github/setup.py,sha256=FTKbQr0bEh2g7oSGAf5GYSkBRT8jIsWmxR0VO6T9KEU,3159
50
+ diffbutler/forges/registry.py,sha256=6zOoUSQxKOkowXLYk6a9OhQqI4ICPrbOa-LYIs0o58A,884
51
+ diffbutler/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ diffbutler/reviewers/__init__.py,sha256=mFpOuwr_X82vIkMc4doTdxbxGs1gpKqQo8tmzMaYqbc,53
53
+ diffbutler/reviewers/base.py,sha256=EClCQV46bEuA0k7uPuhYpubMsGAWbTY3YDKmopQ35JU,2427
54
+ diffbutler/reviewers/extraction.py,sha256=xFudnP5r0Rv6Ximx1qGODl76akMw8b_ARDVi-FCOQFs,8071
55
+ diffbutler/reviewers/native.py,sha256=oVi9ophFaWcKi47BQvLHVvtM3KWkLTzU37cIGpqam0w,8630
56
+ diffbutler/reviewers/parsing.py,sha256=t58lbzs_cOUYFA4300lcW9r4rpBO9uPCNs64AZWImcU,8365
57
+ diffbutler/reviewers/presets.toml,sha256=Y-ce43kXzwgv_kKNaQe5mRp6V6CNyvHaRwEvkASnafM,1068
58
+ diffbutler/reviewers/process.py,sha256=lodapD4SLEYTh4IWYdNoGSmTFrqsZbMLRAZ9acxxz-U,2863
59
+ diffbutler/reviewers/prompt.py,sha256=Ok9Zp92_6acRcAQh7b1Tg-BV-CtMbHKD3DtFXKEPbAw,8351
60
+ diffbutler/reviewers/redaction.py,sha256=yfKSRQln0BUh-QuIcAqZ9FC_Bez4HZ7bJwG7jMywalI,952
61
+ diffbutler/reviewers/resources.py,sha256=5BAlWsqDeCXtTvudGudcMreDKqUoiPrbKt-KauvgyEk,6172
62
+ diffbutler/sources/__init__.py,sha256=0Pr4roOZeA53KsmarQnF1QQhPi2ryO7UuUt3vOP9s9I,82
63
+ diffbutler/sources/diagnostics.py,sha256=TOPbHlA8QKFuO8Zja6gkjUUd29OHTgLz6eK0WebvR0I,3975
64
+ diffbutler/sources/fetch.py,sha256=i3Ti0HPG33AlSzCZVEOZ4RdHTDrJh0wYTo8JZCVsqAA,10778
65
+ diffbutler/sources/git.py,sha256=qngDKIAPLh0wE2W1T9Zwysa-It3KW6_C5EOHtQaY6AI,18319
66
+ diffbutler/sources/remotes.py,sha256=H-h5Khxtl2kNol7OTR97xondyOKBTAKGFUeX-3DQNVM,2732
67
+ diffbutler/store/__init__.py,sha256=_LLUgyn-27dwXVdC0XD1yVfXS79AQMi6JXfofEJ3Ws4,41
68
+ diffbutler/store/base.py,sha256=a5A22SnsoYoYE5YL6BtDdE3ymd1JQC2ctAHt1KAgv7g,3355
69
+ diffbutler/store/dashboard.py,sha256=hxQBLmcgB_Xk8oS-RqvrzRDH-WDxTOXk73VQ9-41SY0,8752
70
+ diffbutler/store/database.py,sha256=AfidYgNNtyDyH-aidwWCdSDAMwasTMqpCviNwDTdYuQ,7334
71
+ diffbutler/store/lifecycle.py,sha256=KbwmIl1HxglZ4T97MOU2htBGaCuR5Xu3_9lJXyhVo_0,10255
72
+ diffbutler/store/migrations/__init__.py,sha256=MGLvwDONznC8aU97PWjZh3Bg_4zzPGtBiLMejK9lrmI,35
73
+ diffbutler/store/migrations/env.py,sha256=rMO6koeK3KoRQzfECIT5cQOM03ocvB8exJD-_ib1-Wk,379
74
+ diffbutler/store/migrations/versions/0001_initial.py,sha256=A_g6lPYp_avZFfO0JDCpSCaLL_sKaYqsQWTTpckbGtI,6856
75
+ diffbutler/store/migrations/versions/0002_monitoring.py,sha256=tRFc38qif6GVvvMRL5x4FeCsgOjdeK1Ct6cHKqdj9iE,1515
76
+ diffbutler/store/migrations/versions/0003_reviews.py,sha256=Ac3HWhuwaQiLqlygTGMjZ3S4xsr72OmI6izhzNJt2qo,3422
77
+ diffbutler/store/migrations/versions/0004_claims.py,sha256=mvKz4u7DrZ1teDvmtJutHK7Zbg6jvyKOAn5II32bNOs,515
78
+ diffbutler/store/migrations/versions/0005_results.py,sha256=fNV6OrGOORiJgEs8wC5vXEHL_WPGQ2oRtpTN8_9ddGM,875
79
+ diffbutler/store/migrations/versions/0006_anchors.py,sha256=i5YAbhdOd0d-1ugW08p-hp8B2TkSLvH9htz7Sx3jWyY,397
80
+ diffbutler/store/migrations/versions/0007_lifecycle.py,sha256=RduOH5hmHbC79yjQiI07AhV9xA6fliBW9ycN1cNnOcI,1153
81
+ diffbutler/store/migrations/versions/0008_processes.py,sha256=8MraDiTcdO480DNuDy62u2EMIgiCujw5fPN7LPBBa3U,880
82
+ diffbutler/store/migrations/versions/0009_dashboard.py,sha256=JUP7e1HUYCXaR8CttX7HjJ6cfaWDNuaazFVepGiM96w,1598
83
+ diffbutler/store/migrations/versions/0010_run_reasons.py,sha256=59T-43cgQ4Qzt_BWaJ-miXy7yv8st8TUhNO6LwWaotA,390
84
+ diffbutler/store/migrations/versions/__init__.py,sha256=rnLPy5gtzmh4IDg6W2EEHCZfoB6Eq8H1E-ipmCDRes8,37
85
+ diffbutler/store/models.py,sha256=iwfo9__htCBItOkc7SmY1mnsgvVL7i0C7j2xqt50Unk,13800
86
+ diffbutler/store/process.py,sha256=B4aXrdYNfuO_ecWkE56QcPrYc41vlQOK8XbWygjQv0Q,3143
87
+ diffbutler/store/queue.py,sha256=26AYdDmSsQwPQjT8leubTVOTjujIqqtRcXGy4NSZMDg,11673
88
+ diffbutler/store/review.py,sha256=YUCNRJHu1ocms9GOMO9br8QvRNpL3pV74Po8krEl6hk,25418
89
+ diffbutler/ui/__init__.py,sha256=JvD0okFiDU9fBbe_SO9nCq3gccohozMW2BxMKfYC-kw,38
90
+ diffbutler/ui/cli/__init__.py,sha256=impAB7LSOUy03inlHkj0LGG7eLJfN294yrfDZyslNiM,18628
91
+ diffbutler/ui/cli/editor.py,sha256=-T1wAwKLxv9a4rQn48KKNDNlAnmrahKaxHqipuJHeuQ,1116
92
+ diffbutler/ui/tui/__init__.py,sha256=k_PE3KbrP21uGbPZeO8eNte584DDkm7qfAOQCSHpUVE,58
93
+ diffbutler/ui/tui/_fake_agent.py,sha256=nutsOsYn5-_vgR5hdvn9K0DnpmWSEdwM9SMe9VtxV-s,1608
94
+ diffbutler/ui/tui/_foreground_exec.py,sha256=5rVMvG_51LO0iuR-uahPFJm24b_DFsHlnfEbuJmzxZ4,529
95
+ diffbutler/ui/tui/app.py,sha256=wznsCbrlZ44vzVBFePLOOAwdBHt-KwSQEOkG_RbNoDM,1375
96
+ diffbutler/ui/tui/browse.py,sha256=OKVNvjAmraycy8vfht8fZ92j5vT-l53pznOrA0oCIQk,4461
97
+ diffbutler/ui/tui/browser.py,sha256=KepB7qwZ-4Y_Mpo9JACiiydFVmO7HXMZ37a5N7e02BA,1279
98
+ diffbutler/ui/tui/dashboard.py,sha256=7zXZF12chxZCzhvXWHtU58EdJFTuWsexSisLPDdxpgg,38678
99
+ diffbutler/ui/tui/foreground.py,sha256=_j2CBkEzO1F8c8E64ajnvZysuOC4-9qxHXLt4gH1ioU,3505
100
+ diffbutler/ui/tui/handoff_probe.py,sha256=Rwbqq_rACFCSSV7o_azRN1H9kONRwQvn_6b51QjJU1U,6695
101
+ diffbutler-0.5.0.dist-info/licenses/LICENSE.txt,sha256=MpRzvP0oe5P1X7beCHWY3zbVmlBo-oppnt6NHDmnkTw,1080
102
+ diffbutler-0.5.0.dist-info/WHEEL,sha256=-i9oRNYVXXZJUIYl5zclLIg6onEb0NLibTX34uln84w,81
103
+ diffbutler-0.5.0.dist-info/entry_points.txt,sha256=dDGuGmpGj-YL3lvHsGgaPMRqtbe8G58f5drFwRXlIvQ,55
104
+ diffbutler-0.5.0.dist-info/METADATA,sha256=244pUzpTeaAB9hEjFtV-hNmXZj8dL5zn5BZwjSyBYpI,7918
105
+ diffbutler-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.13
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ diffbutler = diffbutler.ui.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DiffButler contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.