A team opens a spike branch at 16:40 on a Thursday to answer a single question about retry behavior in a payment path. By 18:10 the branch holds a patch, the suite is green, and nobody in the room can say whether the original question was actually answered. The clock did its job and the spike still failed, because a time box without a written verdict is just a shorter afternoon. The scene is composite, but the failure mode is ordinary enough to recognize.
Most teams answer that failure with a checklist, and checklists rot quickly because they are read by the same optimism that produced the patch. A more durable fix is to let the spike produce an artifact that a machine writes: a verdict file recording the hypothesis, the falsifier, the replay result, and the size of the diff. The harness below fits inside a 90-minute budget and leaves behind something a reviewer can read without replaying the whole afternoon. The shell and Python snippets are a reference harness to adapt to your repository, not a packaged library.
Write the falsifier before the patch exists
A spike only earns its name when the hypothesis can be falsified by a command whose exit code you do not control. That means the falsifier is written first, committed to the spike branch, and left untouched while the assistant works. Otherwise the falsifier quietly mutates into whatever the patch happens to satisfy, which is how green suites become evidence of nothing at all. A short spike.md at the branch root is enough to hold the three fields that matter.
HYPOTHESIS: duplicate charge comes from a retry that reuses the idempotency key
FALSIFIER: npx jest --runTestsByPath test/charge.retry.spec.ts
BUDGET: 90 minutes
ONE HUNK: src/charge/retry.ts
The falsifier line has to name a test that fails now and would pass after the change, because a test that is already green proves nothing about the hypothesis. If you cannot name a single test path that flips, the task is exploration wearing a spike's clothes, and exploration deserves a different budget and a different branch naming scheme.
Drafting falsifier text is wordy work, and a language model is genuinely good at proposing the two or three commands that could discriminate between rival explanations. MonkeyCode's free model access is a reasonable place to do that drafting, and its free server option removes the argument about whose laptop runs the clock. The operator states a free allowance of ten million tokens alongside that server option, and those terms are worth re-checking on the product site before a team builds a weekly ritual around them.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
One boundary keeps the arrangement honest: the model may draft the hypothesis and the falsifier, but it must never write the verdict file. Evidence written by the party under test is not evidence, and the harness below enforces that separation by construction.
Put the budget and the gates in one script
The harness does three jobs in sequence, and it does them without asking anyone how confident they feel. It stamps the starting state, it runs the replay under a hard timeout, and it refuses to call the result clean when the dependency graph moved or the diff sprawled. The timeout is derived from the budget rather than typed twice, so a 90-minute spike cannot silently become a three-hour one. Save this as spike.sh inside the spike branch.
#!/usr/bin/env bash
# spike.sh — one hypothesis, one budget, one verdict file.
set -uo pipefail
: "${SPIKE_REPLAY_TEST:?set SPIKE_REPLAY_TEST to one test path}"
BUDGET_MIN="${SPIKE_BUDGET_MIN:-90}"
DEADLINE=$(( $(date +%s) + BUDGET_MIN * 60 ))
mkdir -p evidence
git rev-parse HEAD > evidence/base-sha.txt
sha256sum package-lock.json > evidence/lockfile-before.sha256
run_with_budget() {
local label="$1"; shift
local left=$(( DEADLINE - $(date +%s) ))
if [ "$left" -le 0 ]; then
echo "budget exhausted before: $label" | tee -a evidence/log.txt
return 124
fi
echo "--- $label, ${left}s left ---" | tee -a evidence/log.txt
timeout --signal=TERM "${left}s" "$@" 2>&1 | tee -a "evidence/${label}.txt"
}
run_with_budget replay npx jest --runTestsByPath "$SPIKE_REPLAY_TEST"
replay_status=$?
git diff -U0 | grep -c '^@@' > evidence/hunks.txt
sha256sum package-lock.json > evidence/lockfile-after.sha256
python3 verdict.py "$replay_status" "$(cat evidence/hunks.txt)"
Two details in that script carry most of the value. The pipefail setting makes the timeout's exit status survive the tee, so a replay killed at minute 89 is recorded as 124 rather than as a successful log line. The lockfile digest is taken twice, because a spike that upgrades a dependency has answered a different question than the one written down. Neither guard needs a reviewer to remember anything, and that is the point of putting them in code.
The verdict step is deliberately boring, and it belongs to the same small script.
import json, pathlib, sys
root = pathlib.Path("evidence")
replay_status, hunks = int(sys.argv[1]), int(sys.argv[2])
def digest(name):
path = root / name
return path.read_text().split()[0] if path.exists() else "missing"
lock_before = digest("lockfile-before.sha256")
lock_after = digest("lockfile-after.sha256")
reasons = []
if lock_before != lock_after:
reasons.append("dependency graph moved during the spike")
if hunks > 1:
reasons.append(f"{hunks} hunks touched; a spike may hold only one")
if replay_status == 124:
reasons.append("budget expired before the replay finished")
elif replay_status != 0:
reasons.append(f"replay still failing (exit {replay_status})")
verdict = "KILL" if reasons else "SHIP-TO-REVIEW"
detail = {
"verdict": verdict,
"hypothesis": pathlib.Path("spike.md").read_text().splitlines()[0],
"replay_status": replay_status,
"hunks": hunks,
"reasons": reasons or ["single hunk, lockfile stable, replay green"],
}
(root / "verdict.json").write_text(json.dumps(detail, indent=2))
print(verdict, detail["reasons"])
Notice what the script refuses to do. It does not claim the bug is fixed, and it does not score the elegance of the patch that the model produced. It only answers the narrow question the spike asked, and it answers that question the same way at 17:00 and at 02:00 the following morning.
Read the verdict, not the mood
Four outcomes cover most spikes, and each one maps to a next action that does not depend on how anyone feels at the end of the day.
Evidence in verdict.json
|
Verdict | Next action |
|---|---|---|
| replay exit 0, one hunk, lockfile stable | SHIP-TO-REVIEW | open a PR with verdict.json attached |
| replay still failing, budget remains | CONVERT | rewrite the falsifier or split the hypothesis |
| replay exit 124, or 4 hunks touched | KILL | the question was too large for 90 minutes |
| lockfile digest changed | KILL | revert the spike and re-run without dependency churn |
The CONVERT row is the one teams skip most often, and it is where the time box earns its keep. A failing replay inside the budget is not a failure of the spike; it is the spike telling you that the hypothesis was too coarse to be useful. Rewriting the falsifier costs five minutes and frequently saves the rest of the afternoon.
Run the clock on a machine that is not your laptop
A spike is supposed to be disposable, which makes it a poor candidate for the machine you depend on for the rest of the week. Running the harness on a remote development server keeps the branch, the toolchain, and the logs in one place that can be destroyed without touching local work. The free server option from MonkeyCode fits that shape of work, because the resource is meant for short-lived evaluation rather than for hosting anything durable.
Portability is the practical constraint to check before the spike starts. Capture the Node or Python version in evidence/base-sha.txt alongside the commit, since a replay that passes on a different runtime tells you about the runtime rather than about the bug. If the repository contains customer data or licensed dependencies, do not move the spike to a shared machine at all, and keep the harness local instead.
Where this stops working
The 90-minute budget assumes a suite that can replay a single file in under a minute, and the whole approach degrades badly on suites that boot a database fixture for twenty minutes. Failures spread across three services will not fit the box either, because the falsifier usually needs a multi-service environment that the timeout will kill mid-flight. Nothing here replaces an incident process, and a spike is the wrong tool once real customers are already affected.
The harness is also indifferent to a bad hypothesis that produced interesting side knowledge. A KILL verdict does not mean the afternoon was wasted, but it does mean the branch should not be merged under that name. Teams that cannot accept losing a patch they already like should keep using plain branches, because the verdict file will only make them argue with a JSON blob.
The right audience is small and specific: developers who already isolate a failing test before changing code, and who want the stop condition to be legible to someone who was not in the room.
If that shape fits your week, run one spike with the harness on the free server option and attach verdict.json to the pull request. A reviewer can then read the evidence in about a minute, which is a fair trade for the ninety minutes the spike consumed.
Top comments (0)