{"slug": "letter-to-friday-me-budget-isolate-then-stamp-the-receipt", "title": "Letter to Friday-Me: Budget, Isolate, Then Stamp the Receipt", "summary": "A developer published a reconstructed failure log describing how an uncapped coding agent loop on a free remote server consumed an entire working day, with no receipt recording the runner, files, or stop condition. The writeup argues that free model access and free server time are not infinite budgets, and proposes a local control plane enforcing three gates — budget, isolation, and a session receipt — before any prompt is sent. The workflow is presented as product-agnostic, runnable on any SSH box and model endpoint, with sample Node.js code that hashes state and refuses to proceed on a dirty working tree.", "body_md": "The following letter is a reconstructed failure log.\n\nIt is not a claim about any employer.\n\nFriday at 16:40, the coding agent was still looping.\n\nNo receipt named the runner, files, or stop condition.\n\nFriday-me,\n\nYou treated a free remote loop as unbounded compute.\n\nThat single assumption consumed an entire working day.\n\nThree process mistakes did the real damage here.\n\nNone of them were about model quality.\n\nA refactor request looked small at noon.\n\nYou pointed an agent at a dirty working tree.\n\nYou let it retry on a remote box with no cap.\n\nBy late afternoon the tree was noisy.\n\nTwo patches conflicted, and one secret file entered context.\n\nYou could not reconstruct the successful step.\n\nThis letter is the workflow you lacked.\n\nIt is a local control plane for free remote loops.\n\nFree model access does not mean an infinite loop.\n\nFree server time does not mean an infinite retry budget.\n\nWithout a cap, failure becomes a quiet background process.\n\nYou watched tokens burn through duplicate tool calls.\n\nThe same failing test command ran without a recorded cap.\n\nNo file recorded the step limit or the continue reason.\n\nA loop without a stop rule is not an agent.\n\nIt is a cron job you forgot to kill.\n\nThe remote workspace inherited your entire laptop state.\n\n`node_modules`, `.env`, and a half-applied patch came along.\n\nThe model then fixed symptoms that were only local dirt.\n\nContext windows then filled with unrelated local diffs.\n\nThe runner executed scripts against stale local dependencies.\n\nYou debugged the model instead of the dirty tree.\n\nIsolation is not a style choice here.\n\nIt is the difference between a session and a mess.\n\nWhen the loop died, evidence died with it.\n\nYou had chat scrollback and a dirty git status.\n\nYou did not have a hash, a file list, or a budget remainder.\n\nMonday-you cannot replay what Friday-you actually ran.\n\nSupport cannot tell which runner produced the patch.\n\nYou spend the next day reconstructing a session that never closed.\n\nA session receipt is not extra process theater.\n\nIt is the smallest artifact that makes the day replayable.\n\nKeep exploratory loops off the laptop when the tree is messy.\n\nA free remote server plus free model access can host that drill.\n\nMonkeyCode currently offers free model access and a free server option.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nThe workflow below does not depend on that product.\n\nAny SSH box and any model endpoint can run the same receipt.\n\nFollow these steps before the first prompt.\n\nDo not reorder them under time pressure.\n\n`max_steps` or `max_seconds` hits zero.\nIf any step fails, delete the workspace.\n\nDo not just retry inside the dirty tree.\n\nThe script below is an example control plane.\n\nTreat it as unexecuted sample code, not a benchmark.\n\nIt enforces three gates: budget, isolation, and receipt.\n\nIt never sends secrets and it never applies a patch.\n\n``` bash\n#!/usr/bin/env node\n// Example only. Review before use. Not a hosted service client.\n\nimport { createHash } from \"node:crypto\";\nimport { execFileSync } from \"node:child_process\";\nimport {\n  existsSync,\n  readFileSync,\n  writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\n\nconst ROOT = process.cwd();\nconst RECEIPT = path.join(ROOT, \".session-receipt.json\");\nconst FORBIDDEN = new Set([\".env\", \".env.local\", \"id_rsa\", \"id_ed25519\"]);\n\nfunction git(args) {\n  return execFileSync(\"git\", args, { encoding: \"utf8\" }).trim();\n}\n\nfunction sha256(text) {\n  return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n\nfunction load() {\n  if (!existsSync(RECEIPT)) {\n    throw new Error(\"no open receipt; run init first\");\n  }\n  return JSON.parse(readFileSync(RECEIPT, \"utf8\"));\n}\n\nfunction save(doc) {\n  writeFileSync(RECEIPT, JSON.stringify(doc, null, 2) + \"\\n\");\n}\n\nfunction assertCleanAllowlist(allow) {\n  const dirty = git([\"status\", \"--porcelain\"]);\n  if (dirty) {\n    throw new Error(\"worktree is dirty; isolate first\");\n  }\n  for (const rel of allow) {\n    const base = path.basename(rel);\n    if (FORBIDDEN.has(base)) {\n      throw new Error(`forbidden path in allowlist: ${rel}`);\n    }\n  }\n}\n\nfunction init() {\n  const maxSteps = Number(process.env.MAX_STEPS || 8);\n  const maxSeconds = Number(process.env.MAX_SECONDS || 900);\n  const allow = (process.env.ALLOW || \"src,tests\")\n    .split(\",\")\n    .map((s) => s.trim())\n    .filter(Boolean);\n\n  if (maxSteps < 1 || maxSteps > 32) {\n    throw new Error(\"MAX_STEPS must be between 1 and 32\");\n  }\n\n  assertCleanAllowlist(allow);\n\n  const doc = {\n    status: \"open\",\n    opened_at: new Date().toISOString(),\n    git_sha: git([\"rev-parse\", \"HEAD\"]),\n    branch: git([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]),\n    allowlist: allow,\n    budget: {\n      max_steps: maxSteps,\n      max_seconds: maxSeconds,\n      steps_used: 0,\n    },\n    runner: {\n      host: process.env.RUNNER_HOST || \"unspecified\",\n      kind: process.env.RUNNER_KIND || \"free-remote\",\n    },\n    model: {\n      endpoint: process.env.MODEL_ENDPOINT || \"unspecified\",\n    },\n    artifact: null,\n  };\n\n  if (existsSync(RECEIPT)) {\n    throw new Error(\"receipt already exists; close or delete it\");\n  }\n  save(doc);\n  console.log(\"receipt opened:\", RECEIPT);\n}\n\nfunction tick() {\n  const doc = load();\n  if (doc.status !== \"open\") {\n    throw new Error(\"receipt is not open\");\n  }\n  const started = Date.parse(doc.opened_at);\n  const elapsed = (Date.now() - started) / 1000;\n  doc.budget.steps_used += 1;\n\n  if (doc.budget.steps_used > doc.budget.max_steps) {\n    throw new Error(\"step budget exhausted\");\n  }\n  if (elapsed > doc.budget.max_seconds) {\n    throw new Error(\"time budget exhausted\");\n  }\n  save(doc);\n  console.log(\n    `step ${doc.budget.steps_used}/${doc.budget.max_steps}; ${Math.floor(elapsed)}s elapsed`,\n  );\n}\n\nfunction close() {\n  const doc = load();\n  const diffPath = process.env.DIFF_PATH || \"agent.patch\";\n  if (!existsSync(diffPath)) {\n    throw new Error(`missing diff: ${diffPath}`);\n  }\n  const diff = readFileSync(diffPath, \"utf8\");\n  if (!diff.startsWith(\"diff --git \") && !diff.startsWith(\"--- \")) {\n    throw new Error(\"artifact is not a unified diff\");\n  }\n  if (diff.length > 200_000) {\n    throw new Error(\"diff exceeds 200k characters; split the task\");\n  }\n\n  doc.status = \"closed\";\n  doc.closed_at = new Date().toISOString();\n  doc.artifact = {\n    path: diffPath,\n    sha256: sha256(diff),\n    bytes: Buffer.byteLength(diff),\n  };\n  save(doc);\n  console.log(\"receipt closed:\", doc.artifact.sha256);\n}\n\nconst cmd = process.argv[2];\n if (cmd === \"init\") init();\nelse if (cmd === \"tick\") tick();\nelse if (cmd === \"close\") close();\nelse {\n  console.error(\"usage: receipt.mjs <init|tick|close>\");\n  process.exit(2);\n}\n```\n\nUse a detached worktree for every agent session.\n\nDo not run this script in a dirty repo.\n\n```\ngit fetch --quiet\nSHA=$(git rev-parse HEAD)\ngit worktree add --detach ../agent-scratch \"$SHA\"\ncd ../agent-scratch\n\nexport MAX_STEPS=8\nexport MAX_SECONDS=900\nexport ALLOW=src,tests\nexport RUNNER_HOST=free-server.example\nexport RUNNER_KIND=free-remote\nexport MODEL_ENDPOINT=unspecified\n\nnode ../main/receipt.mjs init\n\n# Each agent tool-call wrapper should run:\nnode ../main/receipt.mjs tick\n\n# After the loop writes agent.patch:\nexport DIFF_PATH=agent.patch\nnode ../main/receipt.mjs close\n```\n\nWrap every remote tool invocation with a tick call.\n\nIf `tick` exits non-zero, kill the loop.\n\n`MAX_STEPS=8` is a local policy, not a vendor quota.\n\nIt forces a split when the task is still vague.\n\nRaise it only after the allowlist shrinks.\n\n`MAX_SECONDS=900` bounds retries you will not watch.\n\nNetwork blips should not become an overnight process.\n\nPair it with a shell `timeout` around SSH.\n\nThe 200k diff cap blocks silent tree dumps.\n\nA useful patch names files and stays reviewable.\n\nIf the artifact is larger, the task was too wide.\n\n| Condition | Local laptop | Isolated free server | \n|---|---|---|\n| Secrets in tree | Stop. Redact first. | Do not copy. | \n| Dirty git status | Isolate or revert. | Fresh worktree only. | \n| Exploratory refactor | Optional. | Preferred with a budget. | \n| Prod deploy | Human-owned. | Never. | \n| Unknown model endpoint | Do not send code. | Do not send code. | \n| Need replay on Monday | Receipt required. | Receipt required. | \n\nRead the decision table before you prompt.\n\nIf two rows conflict, choose the stricter one.\n\nKeep the receipt document small, complete, and boring.\n\nThese fields are the minimum useful set.\n\n`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.\nDo not store prompts that still contain credentials.\n\nDo not store raw env values for later debug.\n\nName the endpoint as a hostname only.\n\nIf you cannot name it, do not open the receipt.\n\nThis receipt is a local file, not a signed audit log.\n\nAnyone with disk access can edit it after the fact.\n\nA free server is not a tenancy boundary.\n\nIt is not a SOC2 control or an availability SLA.\n\nThe example script does not apply any patches.\n\nIt does not sandbox syscalls or prove runner honesty.\n\nThe budget counters remain local to the process.\n\nA crashed wrapper can skip the tick call.\n\nPair this with a wall-clock timeout on the SSH session.\n\n```\ntimeout 15m ssh runner 'cd /work && ./run-agent.sh'\n```\n\nIf you need cryptographic provenance, add detached signatures.\n\nThis article does not implement that layer.\n\nSkip this workflow in the following cases.\n\nThese constraints are hard limits, not style notes.\n\nDo not send regulated data to a free remote model.\n\nDo not copy customer exports onto a free server.\n\nDo not run this as your production CI.\n\nSecurity teams that need attestation want a different stack.\n\nStudents on a shared laptop should still isolate paths.\n\nIf you cannot name the endpoint, do not start the loop.\n\nFriday-me, the model was not the outage.\n\nThe missing stop rule was the outage.\n\nThe dirty tree and missing receipt were the outage.\n\nBound the loop, isolate the tree, and stamp the receipt.\n\nThen you can spend Monday on the patch, not the archaeology.\n\nMonkeyCode is one place to try a free server for this drill.", "url": "https://wpnews.pro/news/letter-to-friday-me-budget-isolate-then-stamp-the-receipt", "canonical_source": "https://dev.to/codejs_8314/letter-to-friday-me-budget-isolate-then-stamp-the-receipt-28ie", "published_at": "2026-09-13 20:05:49+00:00", "updated_at": "2026-09-13 20:20:26.770209+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops", "ai-infrastructure"], "entities": ["MonkeyCode", "Node.js", "Git", "SSH"], "alternates": {"html": "https://wpnews.pro/news/letter-to-friday-me-budget-isolate-then-stamp-the-receipt", "markdown": "https://wpnews.pro/news/letter-to-friday-me-budget-isolate-then-stamp-the-receipt.md", "text": "https://wpnews.pro/news/letter-to-friday-me-budget-isolate-then-stamp-the-receipt.txt", "jsonld": "https://wpnews.pro/news/letter-to-friday-me-budget-isolate-then-stamp-the-receipt.jsonld"}}