DEV Community

Dakota Wu
Dakota Wu

Posted on

A Slice Gate That Stops Weekend MVPs From Growing Phantom Features

A solo founder does not need a complete architecture to ship this weekend. The founder needs one slice file, an unknowns log, and a local gate that rejects any AI patch adding routes, tables, or paid services outside that slice. Free coding models can draft the code. They cannot be trusted to define the product.

Phantom features are the usual failure mode. An agent asked for a waitlist page returns OAuth, a billing portal, a queue, and three cloud SDKs. The launch date slips. The card comes out. A slice gate treats extra product as a test failure, not a pleasant surprise.

This workflow is for one person shipping a single user story on a free host. It is not a platform playbook.

The core rule

Every coding session is bound to a slice.json that lists allowed files, allowed environment variables, forbidden vendor strings, and the one command that must start the app. The model may write code only inside that envelope. Anything else is a failed gate, even if the generated code looks polished.

Unknowns go in unknowns.md. The model records guesses instead of silently turning them into features. A missing log is also a failure.

Artifact: three files in the repo root

The examples below are meant to be copied into an empty directory and run with Python 3 and git. They do not call a network API.

slice.json:

{
  "name": "waitlist-mvp",
  "stories": [
    "A visitor can submit an email on one page.",
    "The app appends that email to a local CSV."
  ],
  "allowed_paths": [
    "app.py",
    "templates/index.html",
    "static/app.css",
    "tests/test_waitlist.py",
    "requirements.txt",
    "README.md",
    "slice.json",
    "unknowns.md",
    "gate.py"
  ],
  "allowed_env": ["PORT"],
  "forbidden": [
    "stripe",
    "openai",
    "anthropic",
    "aws_",
    "mongodb+srv",
    "postgres://",
    "redis://",
    "sentry",
    "segment",
    "auth0"
  ],
  "max_files": 12,
  "start_command": "python app.py"
}
Enter fullscreen mode Exit fullscreen mode

unknowns.md starts as a checklist, not a design doc:

# Unknowns for waitlist-mvp

- [ ] Email uniqueness: reject duplicates or allow them?
- [ ] Rate limit: needed for a private beta URL or not?
- [ ] Export: founder runs `cat data/emails.csv` for now.
- [x] Persistence: local CSV is enough for this slice.
- [x] Auth: none. The form is public.
Enter fullscreen mode Exit fullscreen mode

gate.py is the reproducible check. It reads the slice, inspects the working tree, and exits non-zero on a violation.

#!/usr/bin/env python3
"""Local slice gate. Stdlib only. Run from the repo root."""
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
SLICE = json.loads((ROOT / "slice.json").read_text(encoding="utf-8"))


def tracked_and_dirty() -> list[str]:
    names = set()
    for args in (
        ["git", "ls-files"],
        ["git", "diff", "--name-only"],
        ["git", "ls-files", "--others", "--exclude-standard"],
    ):
        out = subprocess.check_output(args, cwd=ROOT, text=True)
        names.update(line.strip() for line in out.splitlines() if line.strip())
    return sorted(names)


def fail(msg: str) -> None:
    print(f"GATE FAIL: {msg}", file=sys.stderr)
    raise SystemExit(1)


def main() -> None:
    if not (ROOT / "unknowns.md").exists():
        fail("unknowns.md is missing; the model must log guesses, not invent them")
    text = (ROOT / "unknowns.md").read_text(encoding="utf-8").strip()
    if len(text.splitlines()) < 3:
        fail("unknowns.md is too thin to count as a real log")

    files = tracked_and_dirty()
    allowed = set(SLICE["allowed_paths"])
    extra = [p for p in files if p not in allowed]
    if extra:
        fail(f"paths outside the slice: {extra}")
    if len(files) > int(SLICE["max_files"]):
        fail(f"file count {len(files)} exceeds max_files")

    forbidden = tuple(s.lower() for s in SLICE["forbidden"])
    allowed_env = {name.upper() for name in SLICE["allowed_env"]}
    for path in files:
        body = (ROOT / path).read_text(encoding="utf-8", errors="ignore")
        lower = body.lower()
        for token in forbidden:
            if token in lower and path not in {"slice.json", "gate.py"}:
                fail(f"{path} mentions forbidden token {token!r}")
        for line in body.splitlines():
            stripped = line.strip()
            if stripped.startswith("os.environ") or "getenv(" in stripped:
                for part in stripped.replace(",", " ").replace(")", " ").split():
                    key = part.strip("\"'")
                    if key.isupper() and key not in allowed_env and path == "app.py":
                        if key.startswith(("AWS_", "STRIPE", "OPENAI")):
                            fail(f"app.py reads undeclared env {key}")

    readme = (ROOT / "README.md").read_text(encoding="utf-8")
    if SLICE["start_command"] not in readme:
        fail("README.md does not document the one start command")
    print("GATE OK: slice holds")


if __name__ == "__main__":
    os.chdir(ROOT)
    main()
Enter fullscreen mode Exit fullscreen mode

A founder who is not using git yet can replace tracked_and_dirty() with a Path.rglob walk and skip .git and __pycache__. The rest of the contract stays the same.

Numbered session

  1. Write the slice before any prompt. Two stories is enough. A third story waits until the first two run locally.
  2. Fill unknowns.md with every product guess. Unchecked items stay out of the code. Checked items become constraints in the next prompt.
  3. Prompt the model with the slice JSON pasted verbatim. Tell it to edit only allowed_paths and to append to unknowns.md when it wants a new service.
  4. Run python gate.py. A failure is the expected outcome on the first pass. Delete the extra files. Do not negotiate with the patch.
  5. Run the start command from README.md. If the page does not accept an email and write a CSV, the slice is unfinished. New architecture is still forbidden.
  6. Freeze the slice. The next weekend gets a new slice.json name, not a silent expansion of this one.

A short prompt skeleton that stays inside the envelope:

Implement only the stories in slice.json.
Do not add files outside allowed_paths.
Do not add env vars outside allowed_env.
If a dependency or vendor is required, append a question to unknowns.md and stop.
Document start_command in README.md.
After edits, the command `python gate.py` must print GATE OK.
Enter fullscreen mode Exit fullscreen mode

Keep that block in the repo as PROMPT.txt. Paste it every session. Do not rely on chat memory.

Where a free coding server fits

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

MonkeyCode is an open-source coding assistant with a free-model path and a free-server option. For a solo founder the useful part is operational: the slice gate stays in the repo, and the model run can sit on hosted free access instead of a paid cloud agent. The gate does not care which UI produced the patch. It cares that python gate.py exits zero.

Use the hosted option when a laptop is busy, when the founder wants a browser workspace, or when installing a local stack would delay the same-day ship. Keep secrets off that workspace. The waitlist CSV is local product data. It does not belong in a prompt dump.

Do not treat free access as unlimited, dedicated, or permanent. Quotas, model lists, and hardware change. The slice file is the durable artifact. The host is a convenience.

Decision table

Situation Use the slice gate Skip this workflow
One founder, one story, ship today Yes No
Bill must stay at zero Yes No
Agent keeps adding vendors Yes No
Multi-service production with an on-call rotation No Yes
Regulated data, SSO, or a paid SLA No Yes
Architecture is the deliverable No Yes

The middle row is the indie case. Accept the limits. A CSV waitlist with an ugly form beats an invented platform that never launches.

Minimal app the gate will accept

The following app.py is a complete slice, not a sketch. Pair it with a one-line templates/index.html form and a test that posts an email.

#!/usr/bin/env python3
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
import csv
import os
import urllib.parse

DATA = Path(__file__).resolve().parent / "data"
CSV_PATH = DATA / "emails.csv"
FORM = b"""<!doctype html><title>Waitlist</title>
<form method="post"><input name="email" type="email" required>
<button>Join</button></form>"""

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(FORM)

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length).decode("utf-8")
        email = urllib.parse.parse_qs(raw).get("email", [""])[0].strip()
        DATA.mkdir(exist_ok=True)
        new_file = not CSV_PATH.exists()
        with CSV_PATH.open("a", newline="", encoding="utf-8") as handle:
            writer = csv.writer(handle)
            if new_file:
                writer.writerow(["email"])
            writer.writerow([email])
        self.send_response(303)
        self.send_header("Location", "/")
        self.end_headers()

if __name__ == "__main__":
    port = int(os.environ.get("PORT", "8080"))
    HTTPServer(("127.0.0.1", port), Handler).serve_forever()
Enter fullscreen mode Exit fullscreen mode

data/emails.csv is created at runtime, so it does not need a seat in allowed_paths until the founder decides to commit it. Most founders should gitignore it.

Add data/ to allowed_paths only after the gate has passed once. Expanding the slice is a conscious edit to JSON, not a side effect of a chat reply.

Limitations

The gate is a string fence. It will miss a paid SDK imported under an alias. It will miss infrastructure created in a dashboard the repo never sees. It does not prove security, deliverability, or GDPR handling. A forbidden-token list lags the vendor market.

Free models hallucinate APIs that look cheap. The unknowns log is the counter, not a guarantee. If the founder skips the gate and merges from the chat window, the workflow does nothing.

This approach is a poor fit for teams that need shared environments, for founders already on a committed cloud bill, and for products whose first story truly requires a third-party identity provider. Those projects need a real architecture review. They do not need a weekend slice.

What to freeze after a successful ship

Commit slice.json, unknowns.md, gate.py, and the passing app. Tag the commit slice-waitlist-1. The next feature starts as a new JSON file, not as a comment in the old one. That habit is the whole method. Models draft. The founder defines done.

Indie founders who want the same loop on hosted free models can keep this gate local and point a MonkeyCode workspace at the repo when a browser session is more convenient than a laptop install. The product decision still lives in slice.json.

A free server option is enough to reproduce the setup.

Top comments (0)