whobrokeit 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +142 -2
- package/README.tr.md +143 -0
- package/dist/agent.js +185 -0
- package/dist/cli.js +230 -0
- package/dist/demo.js +225 -0
- package/dist/engine.js +377 -0
- package/dist/errors.js +9 -0
- package/dist/exec.js +88 -0
- package/dist/explain.js +140 -0
- package/dist/git.js +39 -0
- package/dist/report.js +392 -0
- package/dist/repro.js +134 -0
- package/dist/sandbox.js +179 -0
- package/package.json +40 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Berkan Atik
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# whobrokeit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Find the commit that broke it, and prove it.**
|
|
4
|
+
|
|
5
|
+
English · [Türkçe](README.tr.md)
|
|
6
|
+
|
|
7
|
+
Describe a bug in one sentence. An AI agent writes a test that shows it, whobrokeit walks your git history to the first commit where that test fails, and proves the result: the test fails at that commit and passes at its parent. Then it explains what went wrong and suggests a fix, which it shows only after the test passes with it.
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="assets/demo.gif" alt="whobrokeit finds the commit that broke subtotal(), in 37 seconds" width="860">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx whobrokeit demo # try it on a generated repo with a hidden bug (no AI needed)
|
|
15
|
+
npx whobrokeit demo --ai # the same, with an AI agent writing the test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Use it
|
|
19
|
+
|
|
20
|
+
Describe the bug, from anywhere inside your repo:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
whobrokeit "Adding 3 mugs costs the same as 1 mug."
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This needs [Claude Code](https://github.com/anthropics/claude-code) or [Codex](https://github.com/openai/codex) installed and logged in. whobrokeit uses whichever it finds.
|
|
27
|
+
|
|
28
|
+
Or bring your own failing test, and no AI is involved:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
whobrokeit --test "npm test -- cart.test.js"
|
|
32
|
+
whobrokeit --test "pytest tests/test_cart.py -x"
|
|
33
|
+
whobrokeit --test "node repro.mjs" --with repro.mjs # a test you haven't committed yet
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Any command works. Exit code 0 means it works, 125 means this commit can't be tested (skip it), anything else means broken. Add `--explain` to get the explanation and a fix in this mode too.
|
|
37
|
+
|
|
38
|
+
## On GitHub issues
|
|
39
|
+
|
|
40
|
+
whobrokeit can also run as a GitHub Action. When you label an issue `whobrokeit`, or comment `/whobrokeit`, it looks for the commit that broke it and replies on the issue with the culprit, the proof and a fix it has tested.
|
|
41
|
+
|
|
42
|
+
Add an `ANTHROPIC_API_KEY` secret to the repo, then save this as `.github/workflows/whobrokeit.yml`:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
name: whobrokeit
|
|
46
|
+
|
|
47
|
+
on:
|
|
48
|
+
issues:
|
|
49
|
+
types: [labeled]
|
|
50
|
+
issue_comment:
|
|
51
|
+
types: [created]
|
|
52
|
+
|
|
53
|
+
permissions:
|
|
54
|
+
contents: read
|
|
55
|
+
issues: write
|
|
56
|
+
|
|
57
|
+
jobs:
|
|
58
|
+
whobrokeit:
|
|
59
|
+
# Only people who can label issues, or members and collaborators who comment /whobrokeit.
|
|
60
|
+
if: >-
|
|
61
|
+
(github.event_name == 'issues' && github.event.label.name == 'whobrokeit') ||
|
|
62
|
+
(github.event_name == 'issue_comment' && !github.event.issue.pull_request &&
|
|
63
|
+
startsWith(github.event.comment.body, '/whobrokeit') &&
|
|
64
|
+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
timeout-minutes: 60
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0 # whobrokeit needs the history
|
|
71
|
+
persist-credentials: false
|
|
72
|
+
- uses: actions/setup-node@v4
|
|
73
|
+
with:
|
|
74
|
+
node-version: 22 # whatever your project uses
|
|
75
|
+
- uses: BerkanAtik/whobrokeit@v0
|
|
76
|
+
with:
|
|
77
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
By default it works from the issue's title and body. Text after `/whobrokeit` replaces them, so you can put the bug more precisely: `/whobrokeit Adding 3 mugs costs the same as 1 mug.`
|
|
81
|
+
|
|
82
|
+
| Input | |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `anthropic-api-key` | Anthropic API key. Or `claude-code-oauth-token`, made with `claude setup-token`, to use a Claude subscription. |
|
|
85
|
+
| `bug` | What is broken. Default: the comment text after `/whobrokeit`, else the issue. |
|
|
86
|
+
| `test` | Your own failing test command. No AI writes the test then. |
|
|
87
|
+
| `explain` | `auto` (default), `true` or `false`. |
|
|
88
|
+
| `agent`, `model`, `good`, `bad`, `setup`, `timeout` | As on the command line. `setup: none` turns installs off. |
|
|
89
|
+
| `working-directory` | The repository to search, if it is not the workspace root. |
|
|
90
|
+
| `comment` | Set to `false` to skip the issue comment. The report is in the job summary either way. |
|
|
91
|
+
|
|
92
|
+
Outputs: `exit-code`, `commit` (short hash of the culprit) and `report` (path to the Markdown report). The job fails only on errors. A bug it could not reproduce is reported on the issue like any other result.
|
|
93
|
+
|
|
94
|
+
Read an issue before you start a run on it. The agent only reads code, but whobrokeit runs the test it writes, and issue text can try to steer the agent. The tests whobrokeit runs never get the AI key or the GitHub token in their environment, and the token can't change anything but comments. Code on the runner runs as the same user, though, so use an API key with a spending limit.
|
|
95
|
+
|
|
96
|
+
## What it does
|
|
97
|
+
|
|
98
|
+
1. **Reproduce.** The agent reads your code and writes a small script that prints a `WHOBROKEIT_BUG:` line when it sees the bug. whobrokeit runs it at `HEAD` and only goes on if it fails there for that reason. A crash without that line counts as "can't tell", so a test that calls a function older commits don't have never blames the wrong commit.
|
|
99
|
+
2. **Find a commit where it still worked.** It walks back 1, 2, 4, 8… commits until the test passes. Or pass `--good <ref>`.
|
|
100
|
+
3. **Bisect.** `git bisect` narrows it down to the first bad commit, skipping commits the test can't tell about.
|
|
101
|
+
4. **Prove it.** It runs the test again at the culprit (fails) and at its parent (passes).
|
|
102
|
+
5. **Explain and fix.** The agent says why the change broke things and proposes an edit. whobrokeit applies it, runs the test, and shows the fix only if the test passes. It saves it as a patch you can `git apply`.
|
|
103
|
+
|
|
104
|
+
## Tested on real bugs
|
|
105
|
+
|
|
106
|
+
I ran it on 12 closed regression issues from commander, semver, postcss, luxon, marked, express, markdown-it, fastify and yargs. It got only the issue text, exactly as the reporter wrote it. It proved the commit the maintainers had identified in **11 of 12** cases, in about 100 seconds each. The cases, the one miss and how to rerun everything are in [bench/](bench).
|
|
107
|
+
|
|
108
|
+
## Safe to run on your repo
|
|
109
|
+
|
|
110
|
+
- **Your working copy is never touched.** Every check runs in a temporary `git worktree`, which is removed at the end. Uncommitted changes, open files and your current branch stay as they are.
|
|
111
|
+
- **The agent can only read.** Claude Code runs with the Read, Grep and Glob tools only, and with your hooks, plugins and MCP servers turned off. Codex runs in its read-only sandbox. whobrokeit runs the test the agent writes, so check it if you don't trust the repo.
|
|
112
|
+
- **Nothing is taken on faith.** The test has to fail at `HEAD` for the reported reason, the culprit has to be proven against its parent, and a fix has to make the test pass.
|
|
113
|
+
- **Dependencies are installed per commit** when `package.json` or a lockfile changes (npm, pnpm, yarn, bun). Use `--setup "<cmd>"` for anything else, or `--no-setup`.
|
|
114
|
+
|
|
115
|
+
## Options
|
|
116
|
+
|
|
117
|
+
| Option | |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `-t, --test <cmd>` | A command that fails while the bug is present. |
|
|
120
|
+
| `--agent <name>` | Who writes the test: `claude`, `codex`, or any command that reads a prompt on stdin and answers on stdout. Default: `claude`, else `codex`. |
|
|
121
|
+
| `--model <name>` | Model for the agent, e.g. `sonnet`. |
|
|
122
|
+
| `--explain` / `--no-explain` | Explain the culprit and test a fix. On by default when you describe a bug, off with `--test`. |
|
|
123
|
+
| `-w, --with <file>` | Copy this file from your working copy into every commit tested. Repeatable. |
|
|
124
|
+
| `-g, --good <ref>` | A commit where it still worked. Default: found automatically. |
|
|
125
|
+
| `-b, --bad <ref>` | A commit where it is broken. Default: `HEAD`. |
|
|
126
|
+
| `--setup <cmd>` / `--no-setup` | Install command, re-run when dependency files change. Default: auto. |
|
|
127
|
+
| `--timeout <sec>` | Time limit for each test run. Default: 600, or 300 for agent-written tests. |
|
|
128
|
+
| `--report <file>` | Also write a Markdown report, ready to paste into an issue or PR. |
|
|
129
|
+
| `--json` | Print the result as JSON. |
|
|
130
|
+
|
|
131
|
+
Exit codes: `0` found and proven · `1` error · `2` bug not reproduced · `3` no working commit found · `4` inconclusive.
|
|
132
|
+
|
|
133
|
+
## Good to know
|
|
134
|
+
|
|
135
|
+
- Requires Node.js 20+ and git. No other dependencies.
|
|
136
|
+
- The test can be in any language your project uses. Automatic dependency install covers JavaScript package managers; for other stacks, use `--setup`.
|
|
137
|
+
- With an agent, a run usually makes two requests: one to write the test, one to explain the culprit (each is retried once if its result doesn't hold up). On the demo repo the whole run takes 30 to 60 seconds.
|
|
138
|
+
- If the project commits no lockfile, old commits get today's versions of their dependencies. A bug that depends on a dependency's version can then be traced to the wrong commit.
|
|
139
|
+
- After a run, the agent's test and the fix are in `.git/whobrokeit/`, outside your working tree.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE)
|
package/README.tr.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# whobrokeit
|
|
2
|
+
|
|
3
|
+
**Hatayı getiren commit'i bulur ve kanıtlar.**
|
|
4
|
+
|
|
5
|
+
[English](README.md) · Türkçe
|
|
6
|
+
|
|
7
|
+
Hatayı tek cümleyle anlatırsın. Bir yapay zekâ ajanı hatayı gösteren bir test yazar; whobrokeit git geçmişinde bu testin ilk başarısız olduğu commit'e kadar gider ve sonucu kanıtlar: test o commit'te başarısız, bir öncekinde başarılıdır. Ardından neyin bozulduğunu açıklar ve bir düzeltme önerir. Düzeltmeyi ancak test onunla geçtikten sonra gösterir.
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="assets/demo.gif" alt="whobrokeit, subtotal() fonksiyonunu bozan commit'i 37 saniyede buluyor" width="860">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx whobrokeit demo # içine hata gizlenmiş örnek bir repoda dene (yapay zekâ gerekmez)
|
|
15
|
+
npx whobrokeit demo --ai # aynısı, testi yapay zekâ ajanı yazar
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Kullanım
|
|
19
|
+
|
|
20
|
+
Reponun içindeyken hatayı anlat:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
whobrokeit "Sepete 3 kupa ekleyince fiyat 1 kupa kadar çıkıyor."
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Bunun için [Claude Code](https://github.com/anthropics/claude-code) ya da [Codex](https://github.com/openai/codex) kurulu ve giriş yapılmış olmalı. whobrokeit hangisini bulursa onu kullanır. Açıklama, hatayı hangi dilde yazdıysan o dilde gelir.
|
|
27
|
+
|
|
28
|
+
Ya da başarısız olan testi kendin ver, o zaman yapay zekâ hiç devreye girmez:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
whobrokeit --test "npm test -- cart.test.js"
|
|
32
|
+
whobrokeit --test "pytest tests/test_cart.py -x"
|
|
33
|
+
whobrokeit --test "node repro.mjs" --with repro.mjs # henüz commit'lemediğin bir test
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Her komut olur. Çıkış kodu 0 çalışıyor, 125 bu commit test edilemiyor (atla), diğer her şey bozuk demektir. Bu modda da açıklama ve düzeltme istersen `--explain` ekle.
|
|
37
|
+
|
|
38
|
+
## GitHub issue'larında
|
|
39
|
+
|
|
40
|
+
whobrokeit bir GitHub Action olarak da çalışır. Bir issue'ya `whobrokeit` etiketini koyduğunda ya da `/whobrokeit` yorumunu yazdığında hatayı getiren commit'i arar. Sonra issue'ya bir yorum yazar: suçlu commit, kanıtı ve test edilmiş bir düzeltme.
|
|
41
|
+
|
|
42
|
+
Repoya `ANTHROPIC_API_KEY` adında bir secret ekle, sonra şunu `.github/workflows/whobrokeit.yml` olarak kaydet:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
name: whobrokeit
|
|
46
|
+
|
|
47
|
+
on:
|
|
48
|
+
issues:
|
|
49
|
+
types: [labeled]
|
|
50
|
+
issue_comment:
|
|
51
|
+
types: [created]
|
|
52
|
+
|
|
53
|
+
permissions:
|
|
54
|
+
contents: read
|
|
55
|
+
issues: write
|
|
56
|
+
|
|
57
|
+
jobs:
|
|
58
|
+
whobrokeit:
|
|
59
|
+
# Yalnız issue etiketleyebilenler ya da /whobrokeit yazan üyeler ve katkıcılar.
|
|
60
|
+
if: >-
|
|
61
|
+
(github.event_name == 'issues' && github.event.label.name == 'whobrokeit') ||
|
|
62
|
+
(github.event_name == 'issue_comment' && !github.event.issue.pull_request &&
|
|
63
|
+
startsWith(github.event.comment.body, '/whobrokeit') &&
|
|
64
|
+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
timeout-minutes: 60
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0 # whobrokeit geçmişe ihtiyaç duyar
|
|
71
|
+
persist-credentials: false
|
|
72
|
+
- uses: actions/setup-node@v4
|
|
73
|
+
with:
|
|
74
|
+
node-version: 22 # projen hangisini kullanıyorsa
|
|
75
|
+
- uses: BerkanAtik/whobrokeit@v0
|
|
76
|
+
with:
|
|
77
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Varsayılan olarak issue'nun başlığı ve metniyle çalışır. `/whobrokeit`'ten sonra bir metin yazarsan onların yerine o kullanılır. Böylece hatayı daha net anlatabilirsin: `/whobrokeit Sepete 3 kupa ekleyince fiyat 1 kupa kadar çıkıyor.`
|
|
81
|
+
|
|
82
|
+
| Girdi | |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `anthropic-api-key` | Anthropic API anahtarı. Claude aboneliğini kullanmak için onun yerine `claude setup-token` ile alınan `claude-code-oauth-token`. |
|
|
85
|
+
| `bug` | Bozuk olan şey. Varsayılan: yorumda `/whobrokeit`'ten sonraki metin, o da yoksa issue. |
|
|
86
|
+
| `test` | Başarısız olan kendi test komutun. O zaman testi yapay zekâ yazmaz. |
|
|
87
|
+
| `explain` | `auto` (varsayılan), `true` ya da `false`. |
|
|
88
|
+
| `agent`, `model`, `good`, `bad`, `setup`, `timeout` | Komut satırındakiyle aynı. `setup: none` kurulumu kapatır. |
|
|
89
|
+
| `working-directory` | Aranacak repo, çalışma alanının kökü değilse. |
|
|
90
|
+
| `comment` | Issue'ya yorum yazmasın istersen `false`. Rapor her durumda job özetinde de olur. |
|
|
91
|
+
|
|
92
|
+
Çıktılar: `exit-code`, `commit` (suçlu commit'in kısa hash'i) ve `report` (Markdown raporun yolu). Job yalnız bir hata olursa başarısız olur. Yeniden üretilemeyen bir hata da diğer sonuçlar gibi issue'ya yazılır.
|
|
93
|
+
|
|
94
|
+
Bir issue'da çalıştırmadan önce issue'yu oku. Ajan yalnız kodu okur, ama yazdığı testi whobrokeit çalıştırır, ve issue metni ajanı yönlendirmeye çalışabilir. whobrokeit'in çalıştırdığı testlerin ortamında AI anahtarı da GitHub token'ı da olmaz, token da yorumlar dışında hiçbir şeyi değiştiremez. Yine de runner'daki kod aynı kullanıcıyla çalışır; harcama sınırı olan bir API anahtarı kullan.
|
|
95
|
+
|
|
96
|
+
## Ne yapar
|
|
97
|
+
|
|
98
|
+
1. **Hatayı yeniden üretir.** Ajan kodu okur ve hatayı görünce `WHOBROKEIT_BUG:` satırı basan küçük bir betik yazar. whobrokeit onu `HEAD`'de çalıştırır, yalnız bu sebeple başarısız olursa devam eder. Bu satır olmadan gelen bir çökme "belli değil" sayılır; eski commit'lerde olmayan bir fonksiyonu çağıran test yüzünden yanlış commit suçlanmaz.
|
|
99
|
+
2. **Hâlâ çalıştığı bir commit bulur.** Test geçene kadar 1, 2, 4, 8… commit geri gider. İstersen `--good <ref>` ile kendin verirsin.
|
|
100
|
+
3. **Bisect yapar.** `git bisect` ile ilk bozuk commit'e iner, testin karar veremediği commit'leri atlar.
|
|
101
|
+
4. **Kanıtlar.** Testi suçlu commit'te (başarısız) ve bir öncekinde (başarılı) yeniden çalıştırır.
|
|
102
|
+
5. **Açıklar ve düzeltir.** Ajan değişikliğin neyi bozduğunu anlatır ve bir düzenleme önerir. whobrokeit düzenlemeyi uygular, testi çalıştırır, yalnız test geçerse gösterir. `git apply` ile uygulanabilen bir patch olarak da kaydeder.
|
|
103
|
+
|
|
104
|
+
## Gerçek hatalarda denendi
|
|
105
|
+
|
|
106
|
+
commander, semver, postcss, luxon, marked, express, markdown-it, fastify ve yargs projelerinden kapanmış 12 regresyon issue'sunda çalıştırdım. Araca yalnız issue metni verildi, raporu yazan kişinin yazdığı haliyle. **12 vakanın 11'inde** proje sahiplerinin belirlediği commit'i kanıtladı; her biri yaklaşık 100 saniye sürdü. Vakalar, tutmayan tek vaka ve hepsini yeniden çalıştırma adımları [bench/](bench) klasöründe.
|
|
107
|
+
|
|
108
|
+
## Reponda güvenle çalışır
|
|
109
|
+
|
|
110
|
+
- **Çalışma kopyana dokunmaz.** Her kontrol geçici bir `git worktree` içinde yapılır ve sonunda silinir. Commit'lenmemiş değişikliklerin, açık dosyaların ve bulunduğun branch olduğu gibi kalır.
|
|
111
|
+
- **Ajan yalnız okuyabilir.** Claude Code yalnız Read, Grep ve Glob araçlarıyla, hook'ların, eklentilerin ve MCP sunucuların kapalıyken çalışır. Codex kendi salt okunur sandbox'ında çalışır. Ajanın yazdığı testi whobrokeit çalıştırır; güvenmediğin bir repoda bu testi kontrol et.
|
|
112
|
+
- **Hiçbir şey sözle kabul edilmez.** Test `HEAD`'de bildirilen sebeple başarısız olmalı, suçlu commit bir öncekiyle karşılaştırılarak kanıtlanmalı, düzeltme de testi geçirmeli.
|
|
113
|
+
- **Bağımlılıklar commit başına kurulur:** `package.json` ya da lockfile değiştiğinde (npm, pnpm, yarn, bun). Başka bir şey için `--setup "<komut>"`, hiç kurmamak için `--no-setup`.
|
|
114
|
+
|
|
115
|
+
## Seçenekler
|
|
116
|
+
|
|
117
|
+
| Seçenek | |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `-t, --test <cmd>` | Hata varken başarısız olan bir komut. |
|
|
120
|
+
| `--agent <name>` | Testi kim yazar: `claude`, `codex` ya da stdin'den talimat alıp stdout'a cevap yazan herhangi bir komut. Varsayılan: `claude`, yoksa `codex`. |
|
|
121
|
+
| `--model <name>` | Ajanın modeli, örn. `sonnet`. |
|
|
122
|
+
| `--explain` / `--no-explain` | Suçlu commit'i açıkla ve düzeltme dene. Hatayı anlattığında açık, `--test` ile kapalı. |
|
|
123
|
+
| `-w, --with <file>` | Bu dosyayı çalışma kopyandan test edilen her commit'e kopyala. Birden çok verilebilir. |
|
|
124
|
+
| `-g, --good <ref>` | Hâlâ çalıştığı bir commit. Varsayılan: otomatik bulunur. |
|
|
125
|
+
| `-b, --bad <ref>` | Bozuk olduğu bir commit. Varsayılan: `HEAD`. |
|
|
126
|
+
| `--setup <cmd>` / `--no-setup` | Kurulum komutu, bağımlılık dosyaları değişince yeniden çalışır. Varsayılan: otomatik. |
|
|
127
|
+
| `--timeout <sec>` | Her test çalıştırması için süre sınırı. Varsayılan: 600, ajanın yazdığı testlerde 300. |
|
|
128
|
+
| `--report <file>` | Ayrıca issue ya da PR'a yapıştırmaya hazır bir Markdown rapor yaz. |
|
|
129
|
+
| `--json` | Sonucu JSON olarak bas. |
|
|
130
|
+
|
|
131
|
+
Çıkış kodları: `0` bulundu ve kanıtlandı · `1` hata · `2` hata yeniden üretilemedi · `3` çalışan commit bulunamadı · `4` sonuçsuz.
|
|
132
|
+
|
|
133
|
+
## Bilmekte fayda var
|
|
134
|
+
|
|
135
|
+
- Node.js 20+ ve git gerekir. Başka bağımlılık yok.
|
|
136
|
+
- Test, projenin kullandığı herhangi bir dilde olabilir. Otomatik bağımlılık kurulumu JavaScript paket yöneticilerini kapsar; diğerleri için `--setup` kullan.
|
|
137
|
+
- Ajanla bir çalıştırma genelde iki istek yapar: biri testi yazmak, biri suçlu commit'i açıklamak için (sonuç tutmazsa her biri bir kez daha denenir). Demo reposunda tüm süreç 30–60 saniye sürer.
|
|
138
|
+
- Proje lockfile commit'lemiyorsa eski commit'ler bağımlılıklarının bugünkü sürümleriyle kurulur. Bir bağımlılığın sürümüne bağlı hatalarda yanlış commit bulunabilir.
|
|
139
|
+
- İş bitince ajanın yazdığı test ve düzeltme `.git/whobrokeit/` içinde durur, çalışma klasörünün dışında.
|
|
140
|
+
|
|
141
|
+
## Lisans
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE)
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
4
|
+
import { UserError } from './errors.js';
|
|
5
|
+
import { runCommand } from './exec.js';
|
|
6
|
+
const PRESETS = ['claude', 'codex'];
|
|
7
|
+
const LABELS = { claude: 'Claude Code', codex: 'Codex' };
|
|
8
|
+
/**
|
|
9
|
+
* `undefined` picks the first installed preset. A preset name picks that one.
|
|
10
|
+
* Anything else is a custom command: it gets the prompt on stdin and answers on stdout.
|
|
11
|
+
*/
|
|
12
|
+
export async function pickAgent(choice, cwd, model) {
|
|
13
|
+
if (model !== undefined && !/^[\w.:/@[\]-]+$/.test(model))
|
|
14
|
+
throw new UserError(`--model ${model} is not a valid model name.`);
|
|
15
|
+
if (choice !== undefined && !PRESETS.includes(choice)) {
|
|
16
|
+
if (model !== undefined)
|
|
17
|
+
throw new UserError('--model only works with --agent claude or --agent codex.');
|
|
18
|
+
return customAgent(choice);
|
|
19
|
+
}
|
|
20
|
+
const candidates = choice ? [choice] : [...PRESETS];
|
|
21
|
+
for (const name of candidates) {
|
|
22
|
+
const probe = await runCommand(`${name} --version`, cwd, 30_000);
|
|
23
|
+
if (probe.code === 0)
|
|
24
|
+
return name === 'claude' ? claudeAgent(model) : codexAgent(model);
|
|
25
|
+
}
|
|
26
|
+
if (choice) {
|
|
27
|
+
throw new UserError(`${LABELS[choice]} (\`${choice}\`) is not installed or not on your PATH.`);
|
|
28
|
+
}
|
|
29
|
+
throw new UserError('Describing a bug in plain words needs Claude Code or Codex installed.', 'Install one of them, use --agent "<command>" for another tool, or pass your own test with --test "<command>".');
|
|
30
|
+
}
|
|
31
|
+
function claudeAgent(model) {
|
|
32
|
+
const label = LABELS.claude;
|
|
33
|
+
const command = [
|
|
34
|
+
'claude -p',
|
|
35
|
+
'--output-format stream-json --verbose',
|
|
36
|
+
// Read-only tools, no hooks, plugins or MCP servers, nothing left in the session list.
|
|
37
|
+
'--tools Read,Grep,Glob',
|
|
38
|
+
'--permission-prompts none',
|
|
39
|
+
'--safe-mode --strict-mcp-config',
|
|
40
|
+
'--no-session-persistence',
|
|
41
|
+
model ? `--model ${model}` : '',
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(' ');
|
|
45
|
+
return {
|
|
46
|
+
label,
|
|
47
|
+
async ask(prompt, cwd, { timeoutMs, onActivity }) {
|
|
48
|
+
let result;
|
|
49
|
+
const lines = lineSplitter((line) => {
|
|
50
|
+
if (!line.startsWith('{'))
|
|
51
|
+
return;
|
|
52
|
+
let event;
|
|
53
|
+
try {
|
|
54
|
+
event = JSON.parse(line);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === 'assistant') {
|
|
60
|
+
for (const part of event.message?.content ?? []) {
|
|
61
|
+
if (part.type === 'tool_use')
|
|
62
|
+
onActivity?.(describeTool(part.name, part.input ?? {}, cwd));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (event.type === 'result') {
|
|
66
|
+
result = { text: event.result ?? '', isError: Boolean(event.is_error), subtype: event.subtype };
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// The user's `language` setting survives --safe-mode and beats the prompt's "write in the bug
|
|
70
|
+
// report's language" rule, so a Turkish setting turned English reports into Turkish answers.
|
|
71
|
+
const dir = await mkdtemp(join(tmpdir(), 'whobrokeit-claude-'));
|
|
72
|
+
const settings = join(dir, 'settings.json');
|
|
73
|
+
let run;
|
|
74
|
+
try {
|
|
75
|
+
await writeFile(settings, JSON.stringify({ language: '' }));
|
|
76
|
+
run = await runCommand(`${command} --settings "${settings}"`, cwd, timeoutMs, { input: prompt, onStdout: lines.push });
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
lines.flush();
|
|
82
|
+
// Its own error message (a bad key, no credit) says more than the exit code does.
|
|
83
|
+
if (!result?.isError || run.timedOut)
|
|
84
|
+
failIfBroken(label, run, timeoutMs, result === undefined);
|
|
85
|
+
if (result?.isError) {
|
|
86
|
+
throw new UserError(`${label} stopped with an error: ${result.text || result.subtype || 'unknown error'}`);
|
|
87
|
+
}
|
|
88
|
+
return result?.text ?? '';
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function codexAgent(model) {
|
|
93
|
+
const label = LABELS.codex;
|
|
94
|
+
return {
|
|
95
|
+
label,
|
|
96
|
+
async ask(prompt, cwd, { timeoutMs }) {
|
|
97
|
+
const dir = await mkdtemp(join(tmpdir(), 'whobrokeit-codex-'));
|
|
98
|
+
const answerFile = join(dir, 'answer.txt');
|
|
99
|
+
try {
|
|
100
|
+
const command = [
|
|
101
|
+
'codex exec',
|
|
102
|
+
'--sandbox read-only --skip-git-repo-check --ephemeral --color never',
|
|
103
|
+
model ? `-m ${model}` : '',
|
|
104
|
+
`-o "${answerFile}"`,
|
|
105
|
+
'-',
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join(' ');
|
|
109
|
+
const run = await runCommand(command, cwd, timeoutMs, { input: prompt });
|
|
110
|
+
const answer = await readFile(answerFile, 'utf8').catch(() => null);
|
|
111
|
+
failIfBroken(label, run, timeoutMs, answer === null);
|
|
112
|
+
return answer ?? '';
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function customAgent(command) {
|
|
121
|
+
return {
|
|
122
|
+
label: command,
|
|
123
|
+
async ask(prompt, cwd, { timeoutMs }) {
|
|
124
|
+
let stdout = '';
|
|
125
|
+
const run = await runCommand(command, cwd, timeoutMs, { input: prompt, onStdout: (text) => (stdout += text) });
|
|
126
|
+
failIfBroken(command, run, timeoutMs, false);
|
|
127
|
+
return stdout;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function failIfBroken(label, run, timeoutMs, noAnswer) {
|
|
132
|
+
if (run.timedOut) {
|
|
133
|
+
throw new UserError(`${label} did not answer within ${Math.round(timeoutMs / 60_000)} minutes.`);
|
|
134
|
+
}
|
|
135
|
+
if (run.code === 0 && !noAnswer)
|
|
136
|
+
return;
|
|
137
|
+
// Drop the JSON event stream; keep what a human would read.
|
|
138
|
+
const said = run.output
|
|
139
|
+
.split('\n')
|
|
140
|
+
.map((line) => line.trim())
|
|
141
|
+
.filter((line) => line && !line.startsWith('{'))
|
|
142
|
+
.slice(-8)
|
|
143
|
+
.join('\n');
|
|
144
|
+
throw new UserError(`${label} failed (exit code ${run.code}).${said ? `\n${said}` : ''}`, 'Check that it works on its own in this terminal (is it logged in?), or pass your own test with --test.');
|
|
145
|
+
}
|
|
146
|
+
function describeTool(name, input, cwd) {
|
|
147
|
+
const str = (key) => (typeof input[key] === 'string' ? input[key] : '');
|
|
148
|
+
const where = (path) => {
|
|
149
|
+
if (!path)
|
|
150
|
+
return '';
|
|
151
|
+
const rel = isAbsolute(path) ? relative(cwd, path) : path;
|
|
152
|
+
if (!rel)
|
|
153
|
+
return '.';
|
|
154
|
+
return rel.startsWith('..') ? path : rel.split(sep).join('/');
|
|
155
|
+
};
|
|
156
|
+
switch (name) {
|
|
157
|
+
case 'Read':
|
|
158
|
+
return `reading ${where(str('file_path'))}`;
|
|
159
|
+
case 'Grep':
|
|
160
|
+
return `searching for "${str('pattern')}"${str('path') ? ` in ${where(str('path'))}` : ''}`;
|
|
161
|
+
case 'Glob':
|
|
162
|
+
return `looking for ${str('pattern')}${str('path') ? ` in ${where(str('path'))}` : ''}`;
|
|
163
|
+
default:
|
|
164
|
+
return `using ${name ?? 'a tool'}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Turns arbitrary stdout chunks into whole lines. */
|
|
168
|
+
function lineSplitter(onLine) {
|
|
169
|
+
let buffer = '';
|
|
170
|
+
return {
|
|
171
|
+
push: (text) => {
|
|
172
|
+
buffer += text;
|
|
173
|
+
let newline;
|
|
174
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
175
|
+
onLine(buffer.slice(0, newline).trim());
|
|
176
|
+
buffer = buffer.slice(newline + 1);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
flush: () => {
|
|
180
|
+
if (buffer.trim())
|
|
181
|
+
onLine(buffer.trim());
|
|
182
|
+
buffer = '';
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|