whobrokeit 0.1.0 → 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/README.md CHANGED
@@ -35,6 +35,64 @@ whobrokeit --test "node repro.mjs" --with repro.mjs # a test you haven't commi
35
35
 
36
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
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
+
38
96
  ## What it does
39
97
 
40
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.
package/README.tr.md CHANGED
@@ -35,6 +35,64 @@ whobrokeit --test "node repro.mjs" --with repro.mjs # henüz commit'lemediğin
35
35
 
36
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
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
+
38
96
  ## Ne yapar
39
97
 
40
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.
package/dist/agent.js CHANGED
@@ -79,7 +79,9 @@ function claudeAgent(model) {
79
79
  await rm(dir, { recursive: true, force: true }).catch(() => { });
80
80
  }
81
81
  lines.flush();
82
- failIfBroken(label, run, timeoutMs, result === undefined);
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);
83
85
  if (result?.isError) {
84
86
  throw new UserError(`${label} stopped with an error: ${result.text || result.subtype || 'unknown error'}`);
85
87
  }
package/dist/engine.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, realpath } from 'node:fs/promises';
2
2
  import { isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { UserError } from './errors.js';
4
4
  import { applyEdits, buildExplainPrompt, parseExplanation } from './explain.js';
@@ -344,9 +344,12 @@ async function resolveCommit(repoRoot, input, flag) {
344
344
  return result.stdout.trim();
345
345
  }
346
346
  async function readOverlays(cwd, repoRoot, files) {
347
- const root = resolve(repoRoot);
347
+ // Real paths on both sides: the folder may be reached through a symlink (/tmp on macOS) or a
348
+ // short Windows name, while git reports the repository's real location.
349
+ const root = await realpath(repoRoot).catch(() => resolve(repoRoot));
350
+ const base = await realpath(cwd).catch(() => resolve(cwd));
348
351
  return Promise.all(files.map(async (file) => {
349
- const absolute = resolve(cwd, file);
352
+ const absolute = resolve(base, file);
350
353
  const rel = relative(root, absolute);
351
354
  if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
352
355
  throw new UserError(`--with ${file} must be a file inside the repository.`);
package/dist/exec.js CHANGED
@@ -1,4 +1,17 @@
1
1
  import { spawn } from 'node:child_process';
2
+ /** Credentials whobrokeit or its agent may need, but the tested code never does. */
3
+ const SECRET_ENV = [
4
+ 'ANTHROPIC_API_KEY',
5
+ 'ANTHROPIC_AUTH_TOKEN',
6
+ 'CLAUDE_CODE_OAUTH_TOKEN',
7
+ 'OPENAI_API_KEY',
8
+ 'CODEX_API_KEY',
9
+ 'GITHUB_TOKEN',
10
+ 'GH_TOKEN',
11
+ 'ACTIONS_RUNTIME_TOKEN',
12
+ 'ACTIONS_ID_TOKEN_REQUEST_TOKEN',
13
+ 'ACTIONS_ID_TOKEN_REQUEST_URL',
14
+ ];
2
15
  const MAX_OUTPUT = 200_000;
3
16
  const active = new Set();
4
17
  /** Runs a shell command and captures combined stdout/stderr. Never rejects. */
@@ -9,6 +22,12 @@ export function runCommand(command, cwd, timeoutMs, options = {}) {
9
22
  delete env.FORCE_COLOR;
10
23
  // Set when whobrokeit itself runs under `node --test`; a nested `node --test` would then exit 0.
11
24
  delete env.NODE_TEST_CONTEXT;
25
+ if (options.untrusted) {
26
+ for (const name of Object.keys(env)) {
27
+ if (SECRET_ENV.includes(name.toUpperCase()))
28
+ delete env[name];
29
+ }
30
+ }
12
31
  const child = spawn(command, {
13
32
  cwd,
14
33
  env,
package/dist/repro.js CHANGED
@@ -78,40 +78,40 @@ const ATTEMPT_OUTPUT_LINES = 60;
78
78
  export function buildPrompt(input) {
79
79
  const { bug, commit, attempts, maxAttempts } = input;
80
80
  const parts = [
81
- `You are helping whobrokeit find the commit that introduced a bug. Your only job: write one small script that shows the bug at the current commit. whobrokeit will then run that script on older commits with git bisect.
82
-
83
- ## Bug report
84
-
85
- <bug_report>
86
- ${bug.trim()}
87
- </bug_report>
88
-
89
- The repository is checked out in the current directory at commit ${commit.short} ("${commit.subject}"). You can read and search the files, but you cannot change them or run anything. whobrokeit runs your script and tells you what happened.
90
-
91
- ## The script
92
-
93
- It will run unchanged on many older commits, so it must follow this contract exactly:
94
-
95
- - The behavior is correct: exit 0.
96
- - The bug is present: print one line that starts with \`${BUG_MARKER}\` and says what was expected and what actually happened, then exit 1.
97
- - The check cannot run on this version of the code, because something it needs (a module, function or option) does not exist yet: exit 125. Never use 125 for the bug itself.
98
- - If the bug shows up as an exception or a crash, catch it around the one call that triggers it and report it as the bug, but only when it is the error from the report: check its message or type, and rethrow anything else. Never put imports or setup inside that try, because on an older commit they can fail for unrelated reasons.
99
- - Any other failure, like a crash in the script itself, counts as "can't tell" rather than as the bug.
100
-
101
- Keep it short (usually under 40 lines) and deterministic: no network, no sleeping, no dependence on today's date or on random values, done in a few seconds. Use only the language runtime, the project's own code and the dependencies it already has (they are installed before the script runs). Load the code under test through stable entry points so the script still works on older commits. Prefer a plain script over a test framework. Write comments and messages in the language the bug report is written in (an English report gets English messages), whatever language you usually reply in. In Node.js, set process.exitCode instead of calling process.exit(), so the output is not cut off.
102
-
103
- Put the script in a new file, never an existing one, at the repository root unless the language needs it somewhere else. The command runs from the repository root and must run only this script, not the whole test suite.
104
-
105
- ## Reply format
106
-
107
- Reply with these three tags, and nothing after them:
108
-
109
- <path>whobrokeit-repro.mjs</path>
110
- <command>node whobrokeit-repro.mjs</command>
111
- <code>
112
- ...the complete file...
113
- </code>
114
-
81
+ `You are helping whobrokeit find the commit that introduced a bug. Your only job: write one small script that shows the bug at the current commit. whobrokeit will then run that script on older commits with git bisect.
82
+
83
+ ## Bug report
84
+
85
+ <bug_report>
86
+ ${bug.trim()}
87
+ </bug_report>
88
+
89
+ The repository is checked out in the current directory at commit ${commit.short} ("${commit.subject}"). You can read and search the files, but you cannot change them or run anything. whobrokeit runs your script and tells you what happened.
90
+
91
+ ## The script
92
+
93
+ It will run unchanged on many older commits, so it must follow this contract exactly:
94
+
95
+ - The behavior is correct: exit 0.
96
+ - The bug is present: print one line that starts with \`${BUG_MARKER}\` and says what was expected and what actually happened, then exit 1.
97
+ - The check cannot run on this version of the code, because something it needs (a module, function or option) does not exist yet: exit 125. Never use 125 for the bug itself.
98
+ - If the bug shows up as an exception or a crash, catch it around the one call that triggers it and report it as the bug, but only when it is the error from the report: check its message or type, and rethrow anything else. Never put imports or setup inside that try, because on an older commit they can fail for unrelated reasons.
99
+ - Any other failure, like a crash in the script itself, counts as "can't tell" rather than as the bug.
100
+
101
+ Keep it short (usually under 40 lines) and deterministic: no network, no sleeping, no dependence on today's date or on random values, done in a few seconds. Use only the language runtime, the project's own code and the dependencies it already has (they are installed before the script runs). Load the code under test through stable entry points so the script still works on older commits. Prefer a plain script over a test framework. Write comments and messages in the language the bug report is written in (an English report gets English messages), whatever language you usually reply in. In Node.js, set process.exitCode instead of calling process.exit(), so the output is not cut off.
102
+
103
+ Put the script in a new file, never an existing one, at the repository root unless the language needs it somewhere else. The command runs from the repository root and must run only this script, not the whole test suite.
104
+
105
+ ## Reply format
106
+
107
+ Reply with these three tags, and nothing after them:
108
+
109
+ <path>whobrokeit-repro.mjs</path>
110
+ <command>node whobrokeit-repro.mjs</command>
111
+ <code>
112
+ ...the complete file...
113
+ </code>
114
+
115
115
  If the report does not describe something you can check, or the code shows that the described behavior is intended, reply with <cannot>one sentence saying why</cannot> instead.`,
116
116
  ];
117
117
  if (attempts.length > 0) {
package/dist/sandbox.js CHANGED
@@ -89,14 +89,14 @@ export class Sandbox {
89
89
  const setup = await this.pendingSetup();
90
90
  if (setup) {
91
91
  onSetup?.(setup.command);
92
- const result = await runCommand(setup.command, this.dir, this.timeoutMs);
92
+ const result = await runCommand(setup.command, this.dir, this.timeoutMs, { untrusted: true });
93
93
  if (result.code !== 0) {
94
94
  this.installedKey = null;
95
95
  return { verdict: 'skip', run: result, reason: 'dependencies failed to install' };
96
96
  }
97
97
  this.installedKey = setup.key;
98
98
  }
99
- const run = await runCommand(test, this.dir, this.timeoutMs);
99
+ const run = await runCommand(test, this.dir, this.timeoutMs, { untrusted: true });
100
100
  return { ...classify(run), run };
101
101
  }
102
102
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whobrokeit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Find the commit that broke it, and prove it.",
5
5
  "type": "module",
6
6
  "bin": {