# Letter to Friday-Me: Budget, Isolate, Then Stamp the Receipt

> Source: <https://dev.to/codejs_8314/letter-to-friday-me-budget-isolate-then-stamp-the-receipt-28ie>
> Published: 2026-09-13 20:05:49+00:00

The following letter is a reconstructed failure log.

It is not a claim about any employer.

Friday at 16:40, the coding agent was still looping.

No receipt named the runner, files, or stop condition.

Friday-me,

You treated a free remote loop as unbounded compute.

That single assumption consumed an entire working day.

Three process mistakes did the real damage here.

None of them were about model quality.

A refactor request looked small at noon.

You pointed an agent at a dirty working tree.

You let it retry on a remote box with no cap.

By late afternoon the tree was noisy.

Two patches conflicted, and one secret file entered context.

You could not reconstruct the successful step.

This letter is the workflow you lacked.

It is a local control plane for free remote loops.

Free model access does not mean an infinite loop.

Free server time does not mean an infinite retry budget.

Without a cap, failure becomes a quiet background process.

You watched tokens burn through duplicate tool calls.

The same failing test command ran without a recorded cap.

No file recorded the step limit or the continue reason.

A loop without a stop rule is not an agent.

It is a cron job you forgot to kill.

The remote workspace inherited your entire laptop state.

`node_modules`, `.env`, and a half-applied patch came along.

The model then fixed symptoms that were only local dirt.

Context windows then filled with unrelated local diffs.

The runner executed scripts against stale local dependencies.

You debugged the model instead of the dirty tree.

Isolation is not a style choice here.

It is the difference between a session and a mess.

When the loop died, evidence died with it.

You had chat scrollback and a dirty git status.

You did not have a hash, a file list, or a budget remainder.

Monday-you cannot replay what Friday-you actually ran.

Support cannot tell which runner produced the patch.

You spend the next day reconstructing a session that never closed.

A session receipt is not extra process theater.

It is the smallest artifact that makes the day replayable.

Keep exploratory loops off the laptop when the tree is messy.

A free remote server plus free model access can host that drill.

MonkeyCode currently offers free model access and a free server option.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The workflow below does not depend on that product.

Any SSH box and any model endpoint can run the same receipt.

Follow these steps before the first prompt.

Do not reorder them under time pressure.

`max_steps` or `max_seconds` hits zero.
If any step fails, delete the workspace.

Do not just retry inside the dirty tree.

The script below is an example control plane.

Treat it as unexecuted sample code, not a benchmark.

It enforces three gates: budget, isolation, and receipt.

It never sends secrets and it never applies a patch.

``` bash
#!/usr/bin/env node
// Example only. Review before use. Not a hosted service client.

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import {
  existsSync,
  readFileSync,
  writeFileSync,
} from "node:fs";
import path from "node:path";

const ROOT = process.cwd();
const RECEIPT = path.join(ROOT, ".session-receipt.json");
const FORBIDDEN = new Set([".env", ".env.local", "id_rsa", "id_ed25519"]);

function git(args) {
  return execFileSync("git", args, { encoding: "utf8" }).trim();
}

function sha256(text) {
  return createHash("sha256").update(text).digest("hex");
}

function load() {
  if (!existsSync(RECEIPT)) {
    throw new Error("no open receipt; run init first");
  }
  return JSON.parse(readFileSync(RECEIPT, "utf8"));
}

function save(doc) {
  writeFileSync(RECEIPT, JSON.stringify(doc, null, 2) + "\n");
}

function assertCleanAllowlist(allow) {
  const dirty = git(["status", "--porcelain"]);
  if (dirty) {
    throw new Error("worktree is dirty; isolate first");
  }
  for (const rel of allow) {
    const base = path.basename(rel);
    if (FORBIDDEN.has(base)) {
      throw new Error(`forbidden path in allowlist: ${rel}`);
    }
  }
}

function init() {
  const maxSteps = Number(process.env.MAX_STEPS || 8);
  const maxSeconds = Number(process.env.MAX_SECONDS || 900);
  const allow = (process.env.ALLOW || "src,tests")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);

  if (maxSteps < 1 || maxSteps > 32) {
    throw new Error("MAX_STEPS must be between 1 and 32");
  }

  assertCleanAllowlist(allow);

  const doc = {
    status: "open",
    opened_at: new Date().toISOString(),
    git_sha: git(["rev-parse", "HEAD"]),
    branch: git(["rev-parse", "--abbrev-ref", "HEAD"]),
    allowlist: allow,
    budget: {
      max_steps: maxSteps,
      max_seconds: maxSeconds,
      steps_used: 0,
    },
    runner: {
      host: process.env.RUNNER_HOST || "unspecified",
      kind: process.env.RUNNER_KIND || "free-remote",
    },
    model: {
      endpoint: process.env.MODEL_ENDPOINT || "unspecified",
    },
    artifact: null,
  };

  if (existsSync(RECEIPT)) {
    throw new Error("receipt already exists; close or delete it");
  }
  save(doc);
  console.log("receipt opened:", RECEIPT);
}

function tick() {
  const doc = load();
  if (doc.status !== "open") {
    throw new Error("receipt is not open");
  }
  const started = Date.parse(doc.opened_at);
  const elapsed = (Date.now() - started) / 1000;
  doc.budget.steps_used += 1;

  if (doc.budget.steps_used > doc.budget.max_steps) {
    throw new Error("step budget exhausted");
  }
  if (elapsed > doc.budget.max_seconds) {
    throw new Error("time budget exhausted");
  }
  save(doc);
  console.log(
    `step ${doc.budget.steps_used}/${doc.budget.max_steps}; ${Math.floor(elapsed)}s elapsed`,
  );
}

function close() {
  const doc = load();
  const diffPath = process.env.DIFF_PATH || "agent.patch";
  if (!existsSync(diffPath)) {
    throw new Error(`missing diff: ${diffPath}`);
  }
  const diff = readFileSync(diffPath, "utf8");
  if (!diff.startsWith("diff --git ") && !diff.startsWith("--- ")) {
    throw new Error("artifact is not a unified diff");
  }
  if (diff.length > 200_000) {
    throw new Error("diff exceeds 200k characters; split the task");
  }

  doc.status = "closed";
  doc.closed_at = new Date().toISOString();
  doc.artifact = {
    path: diffPath,
    sha256: sha256(diff),
    bytes: Buffer.byteLength(diff),
  };
  save(doc);
  console.log("receipt closed:", doc.artifact.sha256);
}

const cmd = process.argv[2];
 if (cmd === "init") init();
else if (cmd === "tick") tick();
else if (cmd === "close") close();
else {
  console.error("usage: receipt.mjs <init|tick|close>");
  process.exit(2);
}
```

Use a detached worktree for every agent session.

Do not run this script in a dirty repo.

```
git fetch --quiet
SHA=$(git rev-parse HEAD)
git worktree add --detach ../agent-scratch "$SHA"
cd ../agent-scratch

export MAX_STEPS=8
export MAX_SECONDS=900
export ALLOW=src,tests
export RUNNER_HOST=free-server.example
export RUNNER_KIND=free-remote
export MODEL_ENDPOINT=unspecified

node ../main/receipt.mjs init

# Each agent tool-call wrapper should run:
node ../main/receipt.mjs tick

# After the loop writes agent.patch:
export DIFF_PATH=agent.patch
node ../main/receipt.mjs close
```

Wrap every remote tool invocation with a tick call.

If `tick` exits non-zero, kill the loop.

`MAX_STEPS=8` is a local policy, not a vendor quota.

It forces a split when the task is still vague.

Raise it only after the allowlist shrinks.

`MAX_SECONDS=900` bounds retries you will not watch.

Network blips should not become an overnight process.

Pair it with a shell `timeout` around SSH.

The 200k diff cap blocks silent tree dumps.

A useful patch names files and stays reviewable.

If the artifact is larger, the task was too wide.

| Condition | Local laptop | Isolated free server | 
|---|---|---|
| Secrets in tree | Stop. Redact first. | Do not copy. | 
| Dirty git status | Isolate or revert. | Fresh worktree only. | 
| Exploratory refactor | Optional. | Preferred with a budget. | 
| Prod deploy | Human-owned. | Never. | 
| Unknown model endpoint | Do not send code. | Do not send code. | 
| Need replay on Monday | Receipt required. | Receipt required. | 

Read the decision table before you prompt.

If two rows conflict, choose the stricter one.

Keep the receipt document small, complete, and boring.

These fields are the minimum useful set.

`git_sha` of the isolated tree.`allowlist` of paths copied to the runner.`budget.max_steps` and `budget.max_seconds`.` runner.host` and `runner.kind`.` model.endpoint` without secrets.`artifact.sha256` of the unified diff.
Do not store prompts that still contain credentials.

Do not store raw env values for later debug.

Name the endpoint as a hostname only.

If you cannot name it, do not open the receipt.

This receipt is a local file, not a signed audit log.

Anyone with disk access can edit it after the fact.

A free server is not a tenancy boundary.

It is not a SOC2 control or an availability SLA.

The example script does not apply any patches.

It does not sandbox syscalls or prove runner honesty.

The budget counters remain local to the process.

A crashed wrapper can skip the tick call.

Pair this with a wall-clock timeout on the SSH session.

```
timeout 15m ssh runner 'cd /work && ./run-agent.sh'
```

If you need cryptographic provenance, add detached signatures.

This article does not implement that layer.

Skip this workflow in the following cases.

These constraints are hard limits, not style notes.

Do not send regulated data to a free remote model.

Do not copy customer exports onto a free server.

Do not run this as your production CI.

Security teams that need attestation want a different stack.

Students on a shared laptop should still isolate paths.

If you cannot name the endpoint, do not start the loop.

Friday-me, the model was not the outage.

The missing stop rule was the outage.

The dirty tree and missing receipt were the outage.

Bound the loop, isolate the tree, and stamp the receipt.

Then you can spend Monday on the patch, not the archaeology.

MonkeyCode is one place to try a free server for this drill.
