DEV Community

Charlie Hu
Charlie Hu

Posted on

A Scope Card Beats a Bigger Prompt: Cutting a Weekend Agent Demo to One Path

A weekend agent demo is finished when one named path returns a checked result. A larger prompt does not finish it. Free model access and a free server option lower the cost of a trial, and they leave the failure mode unchanged: the side project dies in unfinished scope.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. The only product claims used here are operator-supplied: free model access and a free server option. Model names, token quotas, hardware, duration, and permanence are omitted because they were not verified for this draft. Current docs should be checked before either option is treated as a fixed budget.

The public posts on this account already cover small controls around agent side projects: merge gates, tool allowlists, call envelopes, circuit breakers, dry-run staging, and worker heartbeats. This note does not rebuild those controls. It covers the cut that has to happen before any of them earn a place in a two-day repo. The audience is a developer shipping a narrow demo, not a team designing a platform.

Cut first, then host

The working demo is a local command. It reads a scope card, accepts one intent, and prints a fixture result plus an explicit skip ledger. A live call stays behind a flag that defaults to off.

A free server can host that same process later. It is not required to prove the slice. Saturday defines the card. Sunday runs the fixture path and writes down what was refused.

Anything that needs a model token is a follow-up slice, and only if the fixture cannot show the response shape honestly. That order is the method. Hosting is a later convenience, not the proof.

Scope card

The card is JSON so the draft runner has no parser dependency. Field names stay blunt on purpose. A reader should see the cut without a diagram.

{
  "slice_id": "invoice-status-v0",
  "allowed_intents": ["lookup_invoice_status"],
  "fixture": {
    "tool": "billing.lookup",
    "input": {"invoice_id": "INV-1042"},
    "output": {"status": "open", "currency": "USD", "amount_cents": 4200}
  },
  "skipped": [
    "multi-invoice search",
    "refunds and other writes",
    "a browser UI",
    "retries, workers, and cron",
    "a second model call to explain the status"
  ],
  "live": {
    "enabled_by_default": false,
    "env_base_url": "AGENT_SLICE_BASE_URL",
    "path": "/v0/slice",
    "timeout_seconds": 10
  }
}
Enter fullscreen mode Exit fullscreen mode

slice_id names the demo. allowed_intents is a closed set. fixture is the result a reviewer can rerun without an account. skipped is the scope cut, stored as data so it shows up in the command output.

live records how a later call would be addressed. The path /v0/slice is a local contract for this side project. It is not a documented vendor endpoint, and it should not be copied into a client as if it were one.

Runner

The module below is a proposal. It has not been timed, load-tested, or pointed at a hosted offer. It refuses unknown intents before any network call. It replays the fixture unless --live is set. It prints the skip list on every exit path so a green demo cannot hide the cut.

#!/usr/bin/env python3
"""Unexecuted proposal: one-intent scope runner for a weekend demo."""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path


def load_card(path: Path) -> dict:
    try:
        card = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise SystemExit(f"card_not_json: {exc}") from exc
    for key in ("slice_id", "allowed_intents", "fixture", "skipped", "live"):
        if key not in card:
            raise SystemExit(f"card_missing_field: {key}")
    if not isinstance(card["allowed_intents"], list) or not card["allowed_intents"]:
        raise SystemExit("card_needs_one_intent")
    return card


def decide(card: dict, intent: str, live: bool) -> dict:
    skipped = list(card.get("skipped") or [])
    allowed = set(card["allowed_intents"])
    if intent not in allowed:
        return {
            "ok": False,
            "slice_id": card["slice_id"],
            "reason": "intent_outside_slice",
            "skipped": skipped,
        }
    if not live:
        return {
            "ok": True,
            "slice_id": card["slice_id"],
            "mode": "fixture",
            "result": card["fixture"]["output"],
            "skipped": skipped,
        }
    env_name = card["live"]["env_base_url"]
    base = os.environ.get(env_name, "").rstrip("/")
    if not base:
        return {
            "ok": False,
            "slice_id": card["slice_id"],
            "reason": "live_requested_but_base_url_missing",
            "skipped": skipped,
        }
    payload = json.dumps(
        {
            "slice_id": card["slice_id"],
            "intent": intent,
            "input": card["fixture"]["input"],
        }
    ).encode("utf-8")
    timeout = int(card["live"].get("timeout_seconds", 10))
    req = urllib.request.Request(
        base + card["live"]["path"],
        data=payload,
        headers={"content-type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            body = json.loads(resp.read().decode("utf-8"))
    except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
        return {
            "ok": False,
            "slice_id": card["slice_id"],
            "reason": "live_call_failed",
            "detail": type(exc).__name__,
            "skipped": skipped,
        }
    return {
        "ok": True,
        "slice_id": card["slice_id"],
        "mode": "live",
        "result": body,
        "skipped": skipped,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Run one scoped demo path")
    parser.add_argument("--card", required=True)
    parser.add_argument("--intent", required=True)
    parser.add_argument("--live", action="store_true")
    args = parser.parse_args()
    report = decide(load_card(Path(args.card)), args.intent, args.live)
    json.dump(report, sys.stdout, indent=2)
    sys.stdout.write("\n")
    return 0 if report.get("ok") else 2


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Two behaviors are deliberate. The live failure returns an error type, not the exception text, so a URL or upstream body is less likely to land in a pasted demo log. The fixture path never reads AGENT_SLICE_BASE_URL.

A configured host cannot change the default demo. That split is what makes the weekend rerunnable after a laptop reboot, with or without a network.

Commands and expected reports

Save the card as scope.invoice.json and the module as slice_runner.py. Then run the dry path:

python3 slice_runner.py \
  --card scope.invoice.json \
  --intent lookup_invoice_status
Enter fullscreen mode Exit fullscreen mode

The expected report shape is:

{
  "ok": true,
  "slice_id": "invoice-status-v0",
  "mode": "fixture",
  "result": {"status": "open", "currency": "USD", "amount_cents": 4200},
  "skipped": ["multi-invoice search", "refunds and other writes"]
}
Enter fullscreen mode Exit fullscreen mode

An intent outside the card must fail closed, with no socket:

python3 slice_runner.py \
  --card scope.invoice.json \
  --intent refund_invoice
echo "exit=$?"
Enter fullscreen mode Exit fullscreen mode

reason should be intent_outside_slice, and the exit code should be 2. The skip list should still print. A reviewer who asks for refunds gets a recorded refusal, not a silent success.

Requesting a live call without the env var is the third check:

python3 slice_runner.py \
  --card scope.invoice.json \
  --intent lookup_invoice_status \
  --live
Enter fullscreen mode Exit fullscreen mode

Expected reason: live_requested_but_base_url_missing. The runner must not guess a host, and it must not fall back to the fixture while claiming a live result. Those are different failures. Mixing them makes the demo look healthier than the setup is.

Decision table

Weekend pressure Keep in v0 Replace with the fixture Write into skipped
Add one more intent Only if the sentence still names one path — The extra intent
Show a model answer Only for a field the fixture cannot shape Status, amount, currency Explanations and chat
Put it on a server One process, one route, after the local command works A checked-in JSON file Queues and background jobs
Make it look finished The skip list in the output — A UI that hides skips
Handle failure Non-zero exit and a reason code — Retries and backoff trees

Use the table as a review checklist, not as a slogan. If a row cannot be filled without inventing a metric, the row stays blank and the work stays skipped.

Where a free route actually helps

Free model access is relevant on the step a fixture would fake. In this invoice slice, that step does not exist yet. Status, currency, and amount are canned. A later card might allow one intent, classify_invoice_note, whose input is a short ambiguous string and whose output is a label plus a reason code.

That is the first honest use of a model. It is still one request, still checked against allowed_intents, and still off unless --live is passed. A free server option is relevant after that local command is boring.

Copy the same three files to the host, set the base URL only for the live flag, and keep the fixture command as the regression check. Region, size, and how long the offer lasts are not known here. If the host is unavailable on demo day, the fixture path still runs on a laptop. That fallback is the reason the card exists.

Do not treat either option as a capacity plan. This draft measured nothing: no latency, no token count, no concurrency. A number copied from an older post would be worse than a blank. Confirm the current terms in the product docs before spending a weekend on a live flag.

What the weekend skips

The skip list is the build log. It is not a backlog disguised as humility.

  1. No planner loop. One intent in, one result out. Turn caps belong to a different note.
  2. No tool bus. The fixture names one tool string. It does not route, stage, or allowlist a catalog.
  3. No receipt book and no heartbeat. Those mechanisms answer other questions. Pulling them in would erase the cut.
  4. No write path. Refunds stay skipped because a demo mutation is how a side project stops being a demo.
  5. No second model call to explain the status. Explanation text is how scope leaks back in.
  6. No claim about a token pool, a machine size, or an end date. Unverified numbers stay out of the README as well as this page.

Failure notes worth keeping

A few failures are more instructive than the happy path.

  • A card with an empty allowed_intents list should exit on load. A slice that allows everything is not a slice.
  • A live timeout should surface as live_call_failed with detail set to the exception type. The demo script can then say the host did not answer, which is a true statement.
  • A fixture output that omits a field the reviewer expects should be fixed in the card, not patched in the runner. Hidden defaults recreate the unfinished product.
  • Editing skipped without editing the spoken demo is a process bug. The command output is the script.

Clear field names do more work here than a comment. If skipped needs a paragraph to explain itself, the cut is still fuzzy and the card is not ready to demo.

Test plan before calling it a demo

These checks are local and manual until they are pasted into a real file and run. Until then they are a plan, not a result.

  1. Parse failure: truncate the JSON and confirm a non-zero exit with card_not_json.
  2. Missing field: delete skipped and confirm card_missing_field.
  3. Fixture path: allowed intent, no --live, result equals fixture.output.
  4. Reject path: unknown intent, no socket, exit code 2.
  5. Live guard: --live and an empty base URL, no guessed host, no fixture fallback.
  6. Skip visibility: every string in skipped appears in the report for both success and refusal.
  7. Timeout lock: timeout_seconds stays at 10. A hung process must not become the demo.

A single assertion can lock check 3 once the function is imported:

def test_fixture_path():
    card = {
        "slice_id": "invoice-status-v0",
        "allowed_intents": ["lookup_invoice_status"],
        "fixture": {"output": {"status": "open"}},
        "skipped": ["refunds and other writes"],
        "live": {"env_base_url": "AGENT_SLICE_BASE_URL", "path": "/v0/slice"},
    }
    report = decide(card, "lookup_invoice_status", live=False)
    assert report["mode"] == "fixture"
    assert report["result"]["status"] == "open"
    assert report["skipped"] == ["refunds and other writes"]
Enter fullscreen mode Exit fullscreen mode

Run it only after the file exists in the repo. A pasted assertion is not a passing suite. Record the command and the exit code in the README next to the skip list, so the next session does not have to reconstruct the demo from memory.

Who should not use this

This runner is a scope fence for a side project. It is not authentication, not authorization, and not a sandbox. A person who can edit the card can widen the slice. A person who can set the base URL can send the payload to an unintended host.

Skip the approach when any of these are true:

  • The demo must mutate real billing data or other customer state.
  • A reviewer has already been promised several intents. Cut the promise before writing code.
  • The audience needs a latency, cost, or uptime figure. None were measured.
  • The host must stay up unattended. This draft does not establish that.
  • Secrets need more care than one env var that is never printed. The example does not redact arbitrary logs.
  • The work is a multi-tenant product. A JSON card in a repo root is the wrong boundary.

Clear fields beat a cleaner abstraction that hides the skip list. If a comment is required to explain skipped, the field name is wrong, not the reader.

After the card is boring

Leave the card in the repo root. Add a second intent only by copying the card to a new slice_id and deleting the old promises from the spoken demo. If a free model route and a free server are still available for that next slice, spend them on the one step the fixture cannot show, and confirm the current terms before relying on either. The weekend result is the cut. A longer catalog can wait.

Top comments (0)