Deliberately inject a small, known bug into a disposable git worktree, so learners can practice finding and fixing it.
troublemaker is a teaching tool. It makes trouble on purpose — but only inside a disposable git worktree it creates for you, never in your real code — and it grades the learner's fix against a probe-oracle pipeline.
Status: v0.1.0 — teaching prototype. Read Current limitations before relying on it for anything beyond practice.
- Zero runtime dependencies. Needs plain Node.js (>= 18) and
gitonPATH. - npm package:
dsh-troublemaker(CLI binary:troublemaker). Source: github.com/wonderfulcode1/troublemaker.
Debugging is a skill you learn by debugging. Real codebases are too big, too noisy, and too precious to practice on; synthetic bugs in throwaway exercises are too artificial to be motivating.
troublemaker sits in between: it takes your repository (or the bundled example), checks out a disposable worktree, and injects one small, known, describable bug — a single-character mutation like an off-by-one. The learner then:
- reads the bug description,
- finds the mutation in the worktree,
- fixes it by hand,
- re-runs the check until it reports
solved, - rolls the worktree back.
The tool exists to make this loop verifiable, not to trick anyone. Every injected scenario ships with:
- a probe — a log line inserted into the source that reports the buggy behavior at runtime,
- an oracle — the expected, correct value of that probe,
- a kill check at injection time proving the oracle actually detects the injected bug (a mutation the oracle cannot detect is rolled back immediately),
- a gate and a regression command the fixed code must keep passing.
This is the same pattern used by real bug-injection teaching systems: the difficulty lives in the scenario, and the machinery is honest — the oracle never lies about what correct behavior is. The injected bug is never hidden from you; the bug description names it, and the source of truth is the diff between the worktree and your committed code.
- It is not a sabotage tool. It cannot touch your working tree or any repository you did not point it at, and it never modifies a committed branch — see Safety guarantees.
- It is not a security tool, not a fuzzer, and not a benchmark against your ability to be fooled.
- It is not production software. It is an MVP for learning how bug-injection teaching works, published openly so others can study, reuse, and improve the idea.
If you use this tool to inject bugs into someone else's code without their consent, you are misusing it. The design deliberately makes that misuse hard (worktrees, clean-repo requirement, rollback), but no tool can stop a determined abuser. Please use it for what it is for: teaching and learning.
These are the technical guarantees the tool actually enforces, not promises:
- Only disposable worktrees are touched. Every scenario lives in
<repo>/.dsh-teaching-worktrees/<scenarioId>, created withgit worktree add --detach <path> HEAD. Your working tree, your branches, your stashes, and your committed history are never modified.rollbackremoves the worktree and prunes stale worktree records. - A dirty repo is refused.
injectfails unless bothgit diffandgit diff --cachedare clean, so uncommitted work can never be carried into or corrupted by a scenario. - The bug is always detectable. Injection runs a kill check: the driver must exercise the probe path, and the oracle must observe a mismatch with the mutated code. If the mutation is not detected, the scenario is rolled back and injection fails. You are never handed a bug the checker cannot see.
- The repo must contain the scenario's target file, and every anchor must appear exactly once. Injection fails loudly (and rolls back) if the expected pristine source is not present — the tool never guesses or partially mutates.
- Failure rolls everything back. Every failed step in
injectdeletes the worktree it created before the error is returned. - No network, no telemetry, no hidden behavior. The package has zero runtime dependencies; it only runs local
gitandnodecommands you can read in the source. checkis read-only. It runs the scenario's gate, driver, and regression commands inside the worktree and reads files; it writes nothing outside.dsh-teachingartifacts already produced byinject.- Commands are not executed through a shell. The tool spawns
gitandnodedirectly with argument vectors (and whitespace-split manifest commands); a scenario cannot smuggle shell metacharacters through quoting — see the limitation below.
The gate, driver, and regression strings in a scenario are commands that execute on your machine with your user privileges, inside the worktree. This is inherent to the design — a scenario must run code to be checked. Treat scenario manifests the way you would treat any executable: only inject scenarios you trust, and only into repositories you own. The built-in scenario runs nothing but node --check and a plain-node regression script.
Honest inventory of what v0.1.0 does not do:
- One built-in scenario. Only
v0-retain-items(an off-by-one in a tiny retainer class) ships. It targets the bundled example practice repo (examples/practice-repo); to use it elsewhere the repo must contain the same target file and anchors. - No scenario authoring format yet. Scenarios are code, not data: there is no documented manifest-only way to write a new scenario without editing the package. The on-disk worktree manifest (
dsh-teaching-scenarioformat v1) is produced and consumed, but authoring tooling is future work. - Zero-dependency scenarios only. The gate and regression run with plain
node; nonode_modulesinstall or toolchain is provisioned in the worktree (the harness version behind this tool linksnode_modules; this standalone CLI does not). - Whitespace-split commands only. Manifest
gate/regressionstrings cannot contain quoted arguments or shell operators. That is a deliberate safety trade-off (no shell), and it means complex gate commands need a wrapper script. - No auth or remote surface. Everything is local CLI. There is no server, no GUI, and no way to use this over a network.
- Windows-tested, expects
gitonPATH. The EOL handling normalizes injected files to LF; other platforms should work but are untested. fixworks only for built-in scenarios. The known-fix command needs the scenario definition; it cannot fix a worktree whose scenario the package does not ship.- No concurrency control. Injecting into the same repo from two processes at once is not coordinated.
- The injected probe line is plain
console.log. A learner who deletes or renames it trips thetamperedverdict — by design — but heavy logging can also slow the driver.
npm install -g dsh-troublemaker
troublemaker --helpOr run without installing:
npx dsh-troublemaker --helpThe bundled example is a tiny dependency-free repo. Initialize it, inject, break the loop by fixing the bug, and roll back:
git clone https://github.com/wonderfulcode1/troublemaker.git
cd troublemaker/examples/practice-repo
git init -b main
git add -A
git commit -m "pristine practice repo"
troublemaker inject .
# Injected scenario v0-retain-items into .../.dsh-teaching-worktrees/v0-retain-items
# gate: PASS
# kill check: mismatch (oracle detects the injected bug)
troublemaker check . v0-retain-items
# verdict: bug-alive
# Fix the bug by hand in
# .dsh-teaching-worktrees/v0-retain-items/src/retainer.js
# (`<=` back to `<`), then:
troublemaker check . v0-retain-items
# verdict: solved
troublemaker rollback . v0-retain-itemstroublemaker list <repo> list injected scenarios under a repo
troublemaker inject <repo> [--scenario ID] inject a built-in scenario (default: v0-retain-items)
troublemaker check <repo> <scenarioId> run the full check pipeline (verdict + probes)
troublemaker fix <repo> <scenarioId> apply the known fix (built-in scenarios only, demo convenience)
troublemaker rollback <repo> <scenarioId> remove the scenario worktree
troublemaker scenarios list built-in scenarios
troublemaker --version | --help
--json print the command result as JSON
inject— requires a clean repo. Creates a detached worktree, applies the mutation (each anchor must match exactly once), writes the scenario manifest, runs the gate, then runs the driver and proves the oracle detects the bug (kill check).check— runs the verdict pipeline and short-circuits at the first failure:tampered— a probe line no longer appears verbatim in its source file.type-fail— the gate command failed on the injected source.bug-alive— the driver ran but at least one probe differs from the oracle.incomplete— the driver never fired a required probe.regression-failed— all probes match but the regression suite failed.solved— all probes match and the regression suite passes.
fix— applies the known fix (built-in scenarios only; learners are expected to fix by hand).rollback— removes the worktree and prunes stale records.
Every answer is derived from the worktree's manifest.json and source at call time. The tool holds no per-scenario memory, so any number of shells observe the same truth, and a worktree created by one process can be checked or rolled back by another.
v0-retain-items injects an off-by-one into ItemRetainer.push in the bundled example repo: the comparison < is mutated to <=, so the retainer keeps maxItems + 1 items. The probe fires inside finish() and reports the retained items, seen count, and omitted count; the oracle pins the correct values. This is the same scenario used by the DeepSeek Harness teaching feature (bug-injection teaching), ported to run standalone with zero dependencies.
v0.1.0 is published as an honest MVP. It is deliberately small, the limitations above are written down rather than hidden, and the tool is intended as an educational artifact you can read end to end. Bug reports and ideas for the next scenario are welcome in the issue tracker.
MIT — see LICENSE.