timebomb-cli 0.7.0

Scan source code for deadline-tagged fuses and fail when they detonate
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# timebomb

[![CI](https://github.com/pbsladek/timebomb/actions/workflows/ci.yml/badge.svg)](https://github.com/pbsladek/timebomb/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/timebomb-cli.svg)](https://crates.io/crates/timebomb-cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Scan source code for deadline-tagged fuses and fail when they detonate.

The problem it solves: `// TODO: remove this after the migration` gets written with good intentions and stays forever. `timebomb` makes the deadline explicit and machine-enforceable. When the date passes, the build fails — forcing a fix or a conscious decision to extend the deadline.

---

## Fuse format

```
// TODO[2026-06-01]: remove this feature flag once the experiment ends
# FIXME[2026-03-15][alice]: workaround for upstream bug, revert after upgrade
-- HACK[2025-12-31]: temporary shim, drop this column after migration
```

**Syntax:** `TAG[YYYY-MM-DD]: message` or `TAG[YYYY-MM-DD][owner]: message`

The tag must be immediately followed by `[date]` with no space. Plain `// TODO: fix this` comments (no bracket-date) are ignored entirely, so you can adopt timebomb incrementally without touching existing annotations.

The scanner is language-agnostic — it matches the pattern anywhere on a line regardless of comment syntax (`//`, `#`, `--`, `;;`, `%`, `*`, anything). No language-specific parsers.

### Default triggers

`TODO`, `FIXME`, `HACK`, `TEMP`, `REMOVEME`, `DEBT`, `STOPSHIP`, `WORKAROUND`, `DEPRECATED`, `BUG`

Tags are matched case-insensitively. The full set is configurable via `.timebomb.toml`.

### Fuse status

Each fuse is classified relative to the current date, which is derived once at startup and threaded through the entire scan (so long runs across midnight are consistent):

| Status | Condition |
|--------|-----------|
| `detonated` | Date is in the past |
| `ticking` | Date is within the `fuse_days` warning window |
| `inert` | Date is beyond the warning window |

---

## Installation

### Pre-built binaries (fastest)

Download the latest release binary for your platform from [GitHub Releases](https://github.com/pbsladek/timebomb/releases/latest):

```bash
# Linux x86_64
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-linux-x86_64 \
  -o /usr/local/bin/timebomb && chmod +x /usr/local/bin/timebomb

# macOS Apple Silicon
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-macos-aarch64 \
  -o /usr/local/bin/timebomb && chmod +x /usr/local/bin/timebomb

# macOS Intel
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-macos-x86_64 \
  -o /usr/local/bin/timebomb && chmod +x /usr/local/bin/timebomb

# Windows x86_64 (PowerShell)
Invoke-WebRequest https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-windows-x86_64.exe `
  -OutFile timebomb.exe
```

### Via cargo

```bash
cargo install timebomb-cli --locked
```

### From source

```bash
git clone https://github.com/pbsladek/timebomb
cd timebomb
cargo install --path . --locked
```

---

## Commands

### `sweep` — scan and detonate in CI

```bash
timebomb sweep                          # scan current directory
timebomb sweep ./src                    # scan a specific path
timebomb sweep --fuse 30d               # also flag fuses ticking within 30 days
timebomb sweep --fuse 30d --fail-on-ticking  # exit 1 on ticking fuses too
timebomb sweep --since HEAD             # only check fuses on lines changed since HEAD
timebomb sweep --blame                  # enrich unowned fuses via git blame
timebomb sweep --format json            # machine-readable output
timebomb sweep --format github          # GitHub Actions workflow commands
timebomb sweep --tag FIXME              # only sweep fuses with this tag
timebomb sweep --owner alice            # only sweep fuses owned by alice
timebomb sweep --message oauth          # only sweep fuses whose message mentions oauth
timebomb sweep --no-inert               # hide inert fuses from output
timebomb sweep --quiet                  # suppress all output (exit code only)
timebomb sweep --summary                # print only the summary line
timebomb sweep --output report.json     # also write a JSON report to a file
timebomb sweep --max-detonated 0        # override ratchet ceiling for this run
timebomb sweep --max-ticking 5
```

`sweep` is the only command that exits non-zero. All other commands are informational and always exit 0.

### `armory` — prioritize active fuses

```bash
timebomb armory                         # top detonated and ticking fuses
timebomb armory --oldest                # show only the single most urgent fuse
timebomb armory --count                 # print only the active fuse count
timebomb armory --json                  # machine-readable prioritized list
timebomb armory --limit 5               # show the five most volatile fuses
timebomb armory --owner alice           # only Alice's active fuses
timebomb armory --tag FIXME             # only active FIXME fuses
timebomb armory --message oauth         # only active fuses whose message mentions oauth
timebomb armory --fuse 14d              # include fuses ticking within 14 days
```

`armory` ranks detonated fuses first, with the most overdue at the top, then ticking fuses by soonest deadline. It always exits 0.

### `manifest` — list all fuses

```bash
timebomb manifest                       # all fuses, sorted by date ascending
timebomb manifest --detonated           # only detonated
timebomb manifest --ticking 14d         # only ticking within 14 days
timebomb manifest --format json
timebomb manifest --format csv          # CSV output for spreadsheets / scripting
timebomb manifest --blame
timebomb manifest --owner alice         # filter by owner
timebomb manifest --tag TODO            # filter by tag
timebomb manifest --message oauth       # filter by message text
timebomb manifest --owner-missing       # only fuses with no owner and no blame result
timebomb manifest --path-only           # print unique files containing matching fuses
timebomb manifest --no-inert            # hide inert fuses
timebomb manifest --file src/auth.rs    # filter to a specific file (supports globs)
timebomb manifest --file "src/auth/**"  # glob filter
timebomb manifest --file src/auth.rs --file src/db.rs  # multiple files
timebomb manifest --between 2026-01-01 2026-06-30  # date range filter
timebomb manifest --sort date           # sort by expiry date (default)
timebomb manifest --sort file           # sort by file path then line
timebomb manifest --sort owner          # sort by owner then date
timebomb manifest --sort status         # sort detonated → ticking → inert
timebomb manifest --next 10             # show only the 10 soonest fuses
timebomb manifest --count               # print only the count as a plain integer
```

Terminal output includes a compact age column showing days until expiry or overdue:

```
DETONATED src/auth/login.rs:42              TODO[2025-01-15]      -433d  [alice]  remove legacy oauth flow
TICKING   src/db/schema.sql:108             FIXME[2026-04-08]     +15d          drop temp_users table
INERT     src/api/handler.rs:77             HACK[2099-01-01]      +26946d       revisit when platform ships
```

### `defuse` — interactively resolve detonated fuses

```bash
timebomb defuse                         # walk through each detonated fuse
timebomb defuse ./src
```

For each detonated fuse, `defuse` prompts:

```
DETONATED src/auth/login.rs:42  TODO[2025-01-15]: remove legacy oauth flow

  [e] Extend to new date
  [d] Delete line
  [s] Skip

Choice:
```

**Extend** prompts for a new date and rewrites the annotation in-place. **Delete** removes the line. Files are updated in a single bottom-up pass per file to avoid line-shift bugs.

### `plant` — insert a new fuse

```bash
timebomb plant src/auth/login.rs:42 "remove after migration" --date 2026-06-01
timebomb plant src/auth/login.rs:42 "remove after migration" --in-days 90
timebomb plant src/auth.rs "remove oauth" --search legacy_auth --tag FIXME --owner alice --yes
```

### `delay` — bump a deadline

```bash
timebomb delay src/auth/login.rs:42 --date 2026-09-01
timebomb delay src/auth/login.rs:42 --in-days 30 --reason "blocked on upstream fix"
```

### `disarm` — remove a fuse

```bash
timebomb disarm src/auth/login.rs:42
timebomb disarm --all-detonated         # remove every detonated fuse in the scan path
timebomb disarm --all-detonated --yes   # skip confirmation
```

### `intel` — breakdown by owner, tag, or month

```bash
timebomb intel                          # count fuses grouped by owner and tag
timebomb intel --by owner
timebomb intel --by tag
timebomb intel --by month               # timeline view grouped by expiry month
timebomb intel --by tag --format json
timebomb intel --message oauth          # only count fuses whose message mentions oauth
```

### `tripwire` — manage the git pre-commit hook

```bash
timebomb tripwire set --yes             # append timebomb block to .git/hooks/pre-commit
timebomb tripwire cut --yes             # remove only the timebomb block; leave other content intact
```

The hook block written by `tripwire set`:

```sh
# BEGIN timebomb
timebomb sweep --since HEAD .
# END timebomb
```

Installing twice is idempotent. Cutting removes only the marked block; if the file becomes empty it is deleted.

### `fallout` — compare two report snapshots

```bash
timebomb fallout report-jan.json report-feb.json
timebomb fallout --format json report-jan.json report-feb.json
```

Reads two JSON reports produced by `timebomb sweep --format json` and shows how fuse debt changed between them — newly detonated, resolved, and delayed (deadline bumped without fixing).

### `bunker` — ratchet enforcement

```bash
timebomb bunker save                    # snapshot current detonated/ticking counts
timebomb bunker show                    # compare live counts to the saved baseline
```

`bunker save` writes `.timebomb-baseline.json`:

```json
{
  "generated_at": "2026-03-22T10:00:00Z",
  "detonated": 3,
  "ticking": 5
}
```

When this file exists, `timebomb sweep` automatically loads it and exits 1 if the current detonated or ticking count exceeds the baseline — preventing debt from growing while not requiring everything to be fixed at once.

Hard ceilings can also be set in `.timebomb.toml` independently of the baseline file:

```toml
max_detonated = 0
max_ticking = 5
```

### `completions` — shell completion scripts

```bash
timebomb completions bash               # print bash completion script
timebomb completions zsh                # print zsh completion script
timebomb completions fish               # print fish completion script
```

Pipe to your completions directory to enable tab-completion for all subcommands and flags:

```bash
# zsh
timebomb completions zsh > ~/.zsh/completions/_timebomb

# bash (user-level, no sudo required)
mkdir -p ~/.local/share/bash-completion/completions
timebomb completions bash > ~/.local/share/bash-completion/completions/timebomb

# bash (system-wide, requires sudo)
timebomb completions bash | sudo tee /etc/bash_completion.d/timebomb

# fish
timebomb completions fish > ~/.config/fish/completions/timebomb.fish
```

---

## Output formats

### Terminal (default)

```
DETONATED  src/auth/login.rs:42       TODO[2026-01-15]      -433d  remove legacy oauth flow
TICKING    src/db/schema.sql:108      FIXME[2026-04-01]     +8d    drop temp_users table
INERT      src/api/handler.rs:77      HACK[2099-01-01]      +26946d  revisit when platform ships

Swept 142 file(s) · 17 fuse(s) · 1 detonated · 1 ticking · 15 inert
```

The age column (`-Xd` / `+Xd`) shows how many days overdue or until expiry. With `--blame`, unowned fuses show the git blame author as `[~name]`. Explicit `[owner]` brackets are shown as-is and are never overwritten.

Respects `NO_COLOR`.

### JSON (`--format json`)

```json
{
  "swept_files": 142,
  "total_fuses": 17,
  "detonated": [
    {
      "file": "src/auth/login.rs",
      "line": 42,
      "tag": "TODO",
      "date": "2026-01-15",
      "owner": null,
      "message": "remove legacy oauth flow",
      "status": "detonated"
    }
  ],
  "ticking": [...],
  "inert": [...]
}
```

### CSV (`--format csv`, `manifest` only)

```
file,line,tag,date,owner,status,message
src/auth/login.rs,42,TODO,2026-01-15,,detonated,remove legacy oauth flow
```

Fields containing commas or quotes are quoted per RFC 4180.

### GitHub Actions (`--format github`)

Emits [workflow commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions) that appear as inline PR annotations:

```
::error file=src/auth/login.rs,line=42::TODO detonated on 2026-01-15: remove legacy oauth flow
::warning file=src/db/schema.sql,line=108::FIXME ticking until 2026-04-01: drop temp_users table
```

Auto-detected when `GITHUB_ACTIONS=true` is set.

---

## Configuration

`.timebomb.toml` in the project root:

```toml
# Tags to scan for
triggers = ["TODO", "FIXME", "HACK", "TEMP", "REMOVEME", "DEBT", "STOPSHIP", "WORKAROUND", "DEPRECATED", "BUG"]

# Flag fuses expiring within this many days as ticking (0 = disabled)
fuse_days = 14

# Glob patterns to exclude from scanning
exclude = [
  "vendor/**",
  "node_modules/**",
  "*.min.js",
  ".git/**",
]

# File extensions to scan. If empty, all non-binary files are scanned.
extensions = ["rs", "go", "ts", "js", "py", "rb", "java", "sql", "tf", "yaml", "yml"]

# Ratchet ceilings: sweep fails if live count exceeds these values.
max_detonated = 0
max_ticking = 5
```

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `triggers` | `[string]` | see above | Tags to match (case-insensitive) |
| `fuse_days` | integer | `0` | Days before expiry to enter `ticking` status |
| `exclude` | `[string]` | `["vendor/**","node_modules/**","*.min.js",".git/**"]` | Glob exclusions |
| `extensions` | `[string]` | see defaults | Extensions to scan; empty means all non-binary |
| `max_detonated` | integer || Hard ceiling; `sweep` exits 1 if exceeded |
| `max_ticking` | integer || Hard ceiling; `sweep` exits 1 if exceeded |

CLI flags override config file values. If no config file is found, built-in defaults apply silently.

### Environment variables

| Variable | Description |
|----------|-------------|
| `TIMEBOMB_FUSE_DAYS` | Default fuse warning window (e.g. `14` or `14d`). Overridden by `--fuse`. |
| `NO_COLOR` | Disable terminal color output when set. |
| `GITHUB_ACTIONS` | When `true`, auto-selects GitHub Actions output format. |

---

## CI integration

### GitHub Actions

```yaml
name: timebomb
on:
  push:
  pull_request:
  schedule:
    - cron: '0 9 * * *'   # daily sweep even without a push

jobs:
  timebomb:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Install timebomb
        run: |
          curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-linux-x86_64 \
            -o /usr/local/bin/timebomb
          chmod +x /usr/local/bin/timebomb
      - run: timebomb sweep --fuse 14d --fail-on-ticking
```

`--format github` is inferred automatically from `GITHUB_ACTIONS=true`, so workflow command annotations appear in the PR diff without any extra flags.

### Pre-commit hook

```bash
timebomb tripwire set --yes
```

Or manually in `.git/hooks/pre-commit`:

```sh
#!/bin/sh
set -e
timebomb sweep --since HEAD .
```

---

## Releases

Releases are automated via [release-please](https://github.com/googleapis/release-please). Every merge to `main` is inspected for [Conventional Commits](https://www.conventionalcommits.org/):

| Commit type | Version bump |
|-------------|-------------|
| `fix:` | patch |
| `feat:` | minor |
| `feat!:` or `BREAKING CHANGE:` footer | major |

release-please opens a release PR that bumps `Cargo.toml` and drafts the changelog. Merging that PR creates the git tag and GitHub release automatically.

The crates.io package is `timebomb-cli`, but the release component stays `timebomb` so release tags remain `vX.Y.Z` and the installed executable remains `timebomb`.

---

## Scanner behavior

- **Walk:** Recursive directory walk via `walkdir`. Symlinks are not followed.
- **Exclusions:** Paths matching any `exclude` glob are skipped before opening files.
- **Extension filter:** Only files whose extension matches the `extensions` list are scanned. An empty list disables the filter.
- **Binary detection:** The first 8 KB of each candidate file is checked for null bytes (`\x00`). Files containing any are skipped silently.
- **Parallel scan:** After the serial walk phase collects candidates, files are scanned in parallel via `rayon`. The compiled regex is shared across all worker threads.
- **Invalid dates:** A fuse with an unparseable date (e.g. `TODO[2026-13-45]`) emits a warning to stderr and is skipped; the scan continues.
- **Sort:** Results are sorted by date ascending so the most urgent fuses appear first.

---

## Exit codes

| Code | Meaning |
|------|---------|
| `0` | Clean — no detonated fuses (or counts within baseline/ceilings) |
| `1` | Detonated fuses found, ticking threshold exceeded with `--fail-on-ticking`, or ratchet ceiling breached |
| `2` | Configuration or runtime error |

---

## Development

Requires Rust 1.80+.

```bash
cargo build
cargo test
cargo clippy -- -D warnings
cargo fmt --check
```

---

## License

MIT — see [LICENSE](LICENSE) for details.