DEV Community

Harper Zhu
Harper Zhu

Posted on

One Hypothesis File for a Ninety-Minute Spike

A backend team once asked an agent whether a retry worker dropped poison messages after five failures. The session ran on a spare cloud box because nobody wanted the laptop to become the source of truth. Ninety minutes later the worker was untouched, yet the repository held a new dashboard and a rewritten Dockerfile. The original question still had no evidence, only a larger diff and a colder trail of assumptions.

Time-boxed agent spikes fail in a familiar pattern when extra curiosity is treated as extra scope. One hypothesis becomes three, and the clock expires on a tour instead of a verdict. A useful analogy is a wet-lab notebook that allows only one experiment per page. A second hypothesis on the same page is contamination, not extra insight, and the trial should stop.

The method below keeps a ninety-minute spike honest by forcing a single hypothesis file to exist before any model call. That file names the claim, the command that would falsify it, and the evidence path that must appear. The agent may edit application code, but it may not create a second hypothesis document or widen the falsifying command. When the clock hits ninety minutes, the harness records pass, fail, or kill, then refuses further writes.

A free evaluation bench helps because the hypothesis should not share a wallet or a hostname with production traffic. MonkeyCode offers free model access and a free server option that can host this kind of disposable trial. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow still works if those offerings are ignored, provided some other isolated host and model budget exist.

The spike begins with a hypothesis file that a human writes by hand, never by the agent. The filename is fixed so the harness can refuse siblings without parsing natural language. The claim must be falsifiable in one command, which keeps the agent from substituting a slide deck for a test. The allowed command is copied into the harness environment as an immutable string.

# HYPOTHESIS.md — written by a human before any model call
id: spike-2026-09-07-poison-retry
claim: After five failed deliveries, the worker writes status=dead and stops retrying.
falsify_cmd: python -m pytest tests/test_poison_retry.py -q
evidence_path: evidence/result.json
max_minutes: 90
allowed_paths:
  - worker/retry.py
  - tests/test_poison_retry.py
forbidden_paths:
  - Dockerfile
  - HYPOTHESIS-*.md
Enter fullscreen mode Exit fullscreen mode

That document is deliberately boring, because boring claims are easier to kill than cinematic ones. The test file should already fail before the agent starts, which proves the harness is measuring the claim rather than a leftover green suite. If the test cannot be written quickly, the spike is too large and should be split before a model is invoked. Shipping a failing characterization test is part of setup, not part of the agent's score.

The listings below are a proposed harness for an isolated host, not a recorded benchmark from a production fleet. A characterization test states the claim in executable form so a summary paragraph cannot replace evidence. The worker module is expected to exist already; the agent is allowed to change only the paths named in the hypothesis file. Reviewers should reject a spike that adds files outside that list even when the test later turns green.

# tests/test_poison_retry.py — proposed failing characterization test
from worker.retry import RetryWorker


def test_poison_message_marked_dead_after_five_failures():
    worker = RetryWorker(max_attempts=5)
    message = {"id": "m-1", "attempts": 0, "status": "pending"}
    for _ in range(5):
        message = worker.deliver(message, error=True)
    assert message["status"] == "dead"
    assert message["attempts"] == 5
    assert worker.next_attempt_at(message) is None
Enter fullscreen mode Exit fullscreen mode

Copying the tree onto the free server should happen before the clock starts, not as an agent side quest. A short scp and a pretest ssh command keep the laptop from becoming an undeclared participant. If the pretest is already green, the operator stops and rewrites the hypothesis instead of celebrating. If the pretest is still red, the ninety-minute window may open and the agent may begin.

# Proposed transfer onto an isolated evaluation host. Replace USER@HOST.
scp -r poison-spike USER@HOST:~/spike-poison-retry
ssh USER@HOST 'cd ~/spike-poison-retry && chmod +x spike.sh guard_hypothesis.py
python3 -m pytest tests/test_poison_retry.py -q; echo pretest_exit:$?'
Enter fullscreen mode Exit fullscreen mode

A tiny Python module can lock the hypothesis surface while the ninety-minute clock runs on the evaluation host. It refuses extra markdown files that look like competing claims, and it freezes the hypothesis digest after first read. The script is meant to run from a loop on the evaluation host, not as an honor system inside the agent's prompt. Prompt-only gates fail as soon as the model decides that the written gate is merely optional advice.

#!/usr/bin/env python3
"""guard_hypothesis.py — proposed freeze for a single hypothesis file."""
from __future__ import annotations

import hashlib
import json
import os
import sys
import time
from pathlib import Path

ROOT = Path(os.environ.get("SPIKE_ROOT", ".")).resolve()
HYPO = ROOT / "HYPOTHESIS.md"
LOCK = ROOT / "evidence" / "hypothesis.lock"
EVIDENCE = ROOT / "evidence" / "guard.json"
MAX_SECONDS = 90 * 60


def digest(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def extra_hypothesis_files() -> list[str]:
    names: list[str] = []
    for path in ROOT.rglob("*"):
        if not path.is_file():
            continue
        rel = str(path.relative_to(ROOT))
        lowered = path.name.lower()
        is_sibling = path.resolve() != HYPO.resolve() and (
            path.name.upper().startswith("HYPOTHESIS")
            or ("hypothesis" in lowered and path.suffix.lower() in {".md", ".yml", ".yaml"})
        )
        if is_sibling:
            names.append(rel)
    return sorted(set(names))


def main() -> int:
    EVIDENCE.parent.mkdir(parents=True, exist_ok=True)
    if not HYPO.is_file():
        EVIDENCE.write_text(json.dumps({"status": "kill", "reason": "missing_hypothesis"}) + "\n")
        return 2
    if not LOCK.exists():
        LOCK.write_text(json.dumps({"sha256": digest(HYPO), "started_at": time.time()}) + "\n")
    lock = json.loads(LOCK.read_text())
    extras = extra_hypothesis_files()
    elapsed = time.time() - float(lock["started_at"])
    status = "ok"
    reason = "single_hypothesis"
    if digest(HYPO) != lock["sha256"]:
        status, reason = "kill", "hypothesis_mutated"
    elif extras:
        status, reason = "kill", "second_hypothesis:" + ",".join(extras)
    elif elapsed > MAX_SECONDS:
        status, reason = "kill", "timebox_exceeded"
    payload = {
        "status": status,
        "reason": reason,
        "elapsed_seconds": int(elapsed),
        "extras": extras,
    }
    EVIDENCE.write_text(json.dumps(payload, indent=2) + "\n")
    return 0 if status == "ok" else 3


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

The shell harness is the only process allowed to start the model session and the falsifying command. It records the hypothesis digest, runs the guard, and treats a mutated file as a kill even if tests pass. Passing tests after a mutated claim belong to a different experiment rather than a late success. That distinction is the entire point of running a ship-or-kill spike instead of a tour.

#!/usr/bin/env bash
# spike.sh — proposed driver: one hypothesis, ninety minutes, one evidence file.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")" && pwd)"
export SPIKE_ROOT="$ROOT"
EVIDENCE_DIR="$ROOT/evidence"
mkdir -p "$EVIDENCE_DIR"

if [[ ! -f "$ROOT/HYPOTHESIS.md" ]]; then
  printf '%s\n' '{"status":"kill","reason":"missing_hypothesis"}' > "$EVIDENCE_DIR/result.json"
  exit 2
fi

FALSIFY_CMD="$(python3 - <<'PY'
from pathlib import Path
for line in Path("HYPOTHESIS.md").read_text().splitlines():
    if line.startswith("falsify_cmd:"):
        print(line.split(":", 1)[1].strip())
        break
PY
)"

if [[ -z "$FALSIFY_CMD" ]]; then
  printf '%s\n' '{"status":"kill","reason":"missing_falsify_cmd"}' > "$EVIDENCE_DIR/result.json"
  exit 2
fi

python3 "$ROOT/guard_hypothesis.py"
START_TS="$(date +%s)"
STATUS="fail"
REASON="tests_red"

# Placeholder only: start the isolated agent on the evaluation host here.
# Example: ssh USER@HOST 'cd ~/spike-poison-retry && timeout 90m agent-run --root .'
# Do not treat an agent transcript as evidence if guard_hypothesis.py later kills.

if python3 "$ROOT/guard_hypothesis.py"; then
  if bash -lc "$FALSIFY_CMD"; then
    STATUS="pass"
    REASON="claim_held"
  else
    STATUS="fail"
    REASON="claim_falsified_or_unproven"
  fi
else
  STATUS="kill"
  REASON="$(python3 -c 'import json,pathlib; print(json.loads(pathlib.Path("evidence/guard.json").read_text())["reason"])')"
fi

END_TS="$(date +%s)"
python3 - "$STATUS" "$REASON" "$((END_TS - START_TS))" "$FALSIFY_CMD" <<'PY'
import json, pathlib, sys
status, reason, elapsed, cmd = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
pathlib.Path("evidence/result.json").write_text(
    json.dumps(
        {
            "status": status,
            "reason": reason,
            "elapsed_seconds": elapsed,
            "falsify_cmd": cmd,
        },
        indent=2,
    )
    + "\n"
)
PY

python3 "$ROOT/guard_hypothesis.py" || true
echo "spike ${STATUS} (${REASON})"
[[ "$STATUS" == "pass" ]]
Enter fullscreen mode Exit fullscreen mode

Operators should run the characterization test once before the agent is allowed to touch the tree. A green test at minute zero means the hypothesis was already true, so the spike cannot produce learning. A missing test means the team is scoring prose rather than measuring a falsifiable claim. Either case is a setup failure, and the ninety-minute clock should remain stopped until it is fixed.

The ship-or-kill rule stays numerical so a fluent summary paragraph cannot override the recorded status. Pass means the frozen command exited zero while the hypothesis digest stayed constant and no sibling claim file appeared. Fail means the command stayed red while the paperwork stayed honest, which is a clean negative result. Kill means the experiment was contaminated by time, a second claim, or a rewritten hypothesis, which is more useful than a flattering diff.

Reviewers can treat result.json as a boarding pass that either admits the diff to human review or sends it back to the archive. A pass status with a mutated hypothesis digest is still a kill, so the guard runs after tests as well as before them. Status fail with a stable digest is a clean negative result and should be saved with the same care as a green run. A kill status is a process bug in the spike rather than a defect in the worker under test.

Free model access changes the economics of retries without changing the scientific rule of one hypothesis. Teams can afford a second ninety-minute trial on a clean server snapshot after a kill, which is healthier than widening the first trial. A free server also keeps filesystem residue out of laptops, where leftover agent files quietly become tomorrow's undocumented features. None of that requires a particular model name, quota story, or hardware claim; it only requires isolation and a clock.

The approach has limits that should remain visible whenever a team is tempted to stretch the clock. It does not replace incident response, because production outages rarely wait for a hypothesis file and a ninety-minute window. It does not suit open-ended design exploration, where multiple hypotheses are the work rather than contamination. It also assumes the falsifying command truly tests the claim, which a vague pytest file can miss.

People chasing architectural vision, performance tuning across noisy hosts, or multi-service migrations should pick a longer, human-facilitated study instead. Students who need a tour of a codebase should ask for a guided reading session rather than a spike with a fake claim. Vendors who want a feature montage will hate the kill status, and they are not the audience. The method is for teams that need a yes, a no, or a stop, not a highlight reel.

Clock drift and ephemeral disks are practical hazards when a shared free server becomes the bench. The harness stores the start timestamp on disk, so a restarted process can still kill the spike instead of granting a bonus hour. If the server wipes the workspace, a missing lock file is a kill rather than a silent restart under the same identifier. The evidence directory should be copied off-box at minute ninety, including failures that prevent a bad merge.

A second analogy helps when a team wants to negotiate extra scope after the clock has started. Adding a hypothesis mid-flight is like moving the finish line after the sprinter has already left the blocks. The honest move is to stop, archive the evidence, and open a new file with a new clock. That sounds slower than letting the agent keep typing, yet it is usually faster than reviewing an unanswered tour.

Readers who already time-box agent work can copy this harness onto an isolated free server without changing editors. The valuable part is the kill rule, not the brand printed on the evaluation bench. If a spike cannot state one falsifiable claim in a short file, it is not ready for a model, free or otherwise.

Top comments (0)