TAI BUI
Back to curriculum
BuildPython

Scope Contracts and Task Boundaries

The model doesn't know where the work ends. A scope contract is a per-task file that says where the work begins, where it ends, and how to roll back if it spills. It turns "stay in scope" from a wish into a check.

TIME: ~50 minutesPREREQUISITES: Phase 14 · 32 (Minimal Workbench), Phase 14 · 33 (Rules as Constraints)

Learning Objectives

  • Write a scope contract that an agent reads at task start and a verifier reads at task end.
  • Specify allowed files, forbidden files, acceptance criteria, rollback plan, and approval boundaries.
  • Implement a scope checker that compares a diff against the contract and flags violations.
  • Make scope creep visible, automatic, and reviewable.

The Problem

Agents creep. The task is "fix the login bug." The diff touches the login route, the email helper, the database driver, the README, and the release script. Each touch had a plausible reason in the moment, but together they make a different change than the one that was reviewed.

Scope creep is easy to miss in agent work because the agent narrates each step in good faith. A stricter prompt doesn't fix that. A contract on disk that records what was promised, plus a check that compares the result against the promise, does.

Warm-up0/3
1. After an agent finishes a task in a git repo, which command lists the files it changed?Warm-up
2. Which glob pattern is meant to match Python files inside app/ at any folder depth?Warm-up
3. Under the principle of least privilege, what should a process or agent be granted?Warm-up
Check: The Problem0/3
1. What is scope creep in agent work?Define
2. An agent asked to "fix the login bug" also edits the README and a release script. Why is a stricter prompt alone a weak fix?Explain
3. Why is scope creep easy to miss in agent work?Explain

The Concept

A scope contract is a per-task file that records where the work begins, where it ends, and how to roll back if it spills. The agent reads it at task start. A verifier reads it at task end.

Mermaid diagram

What goes in a scope contract

FieldPurpose
task_idLinks to the task on the board
goalOne sentence the reviewer can verify
allowed_filesGlobs the agent may write
forbidden_filesGlobs the agent must not touch even by accident
acceptance_criteriaTest commands or assertion lines that prove done
rollback_planOne paragraph the operator can execute if a halt is required
approvals_requiredActions outside scope that need explicit human sign-off

A contract without forbidden_files is incomplete. The negative space is half the contract.

Globs, not raw paths

Repos move files. Pin the contract to globs (app/**/*.py, tests/test_signup*.py) and a refactor between sessions won't invalidate it.

Rollback is part of scope

Writing down the rollback forces the author to think about what could go wrong. If you can't roll back from a contract, don't approve it.

Scope check is a diff check

The agent writes a diff. The checker takes that diff, the allowed globs, the forbidden globs, and the list of acceptance commands that ran. Every violation becomes a tagged finding that the verification gate can refuse.

Two altitudes of scope: the feature list and the task contract

The scope contract bounds one task, not the project. An agent can stay perfectly inside the contract for the login fix and still decide, on the next turn, that the project also needs a settings page, a dark mode toggle, and a rewrite of the router. The contract answers which files are in scope for the task. It says nothing about which work is in scope for the project.

That second altitude needs its own primitive: a feature_list.json the agent reads at session start. It's the project backlog as an ordered, machine-readable file. The agent picks exactly one feature whose status is todo, writes its id into the active scope contract, and may not start a second feature in the same session. "One feature at a time" stops being a line in the prompt that the agent can rationalize past. It becomes a value read off disk and a check the gate enforces.

json
{
  "project": "knowledge-base",
  "active": "import-pdf",
  "features": [
    { "id": "import-pdf",   "status": "in_progress", "goal": "import a PDF into the library",        "done_when": "pytest tests/test_import.py && a sample PDF appears in the library view" },
    { "id": "full-text-search", "status": "todo",     "goal": "search document text and rank hits",   "done_when": "query returns ranked results with snippets" },
    { "id": "cite-answers", "status": "todo",         "goal": "answers carry source citations",        "done_when": "every answer renders at least one clickable citation" }
  ]
}
FieldPurpose
activeThe single feature the current session may touch; empty means pick one and set it
features[].idStable slug the scope contract's task_id points at
features[].statustodo, in_progress, done, blocked; only one in_progress at a time
features[].goalOne sentence the reviewer can verify
features[].done_whenThe acceptance line that flips in_progress to done

Two rules keep the list load-bearing instead of decorative. First, "at most one in_progress" is itself a startup check (Phase 14 · 33). If the list shows two, the session refuses to start until a human resolves it. Second, the feature list is a file, not a chat message. Chat scrolls out of context, while the file persists across sessions and across agents. The handoff (Phase 14 · 40) writes the finished feature's status back to done, so the next session opens to an accurate board instead of re-deriving what's left.

The contract and the list compose by least privilege, using the same least-privilege merge that the production-patterns section spells out later: the task contract's allowed_files must sit inside whatever the active feature touches, never outside it.

Check: The Concept0/15
1. What is a scope contract?Define
2. What is a rollback plan in a scope contract?Define
3. What is an approval boundary?Define
4. What is a diff check?Define
5. Which fields does the lesson's scope contract table list besides forbidden_files?Define
6. Why does the lesson call a contract without forbidden_files incomplete?Explain
7. Why does the agent read the contract at task start and a verifier read it again at task end?Explain
8. Why pin allowed and forbidden entries to globs rather than raw paths?Explain
9. What does the scope check take as input, and what does it produce?Explain
10. Why does the lesson add a feature_list.json on top of the per-task contract?Explain
11. What invariant does feature_list.json enforce at session startup?Explain
12. Why does the lesson say a rollback plan matters beyond recording a revert command?Explain
13. A session starts and feature_list.json shows import-pdf and full-text-search both marked in_progress. What should happen?Apply
14. An agent finishes the login-fix task entirely inside its allowed globs, then on the next turn starts building a settings page and a dark mode toggle. Which missing piece should have stopped it?Analyze
15. Agents on your team may edit app code freely, but a human must say yes before any database migration is run. Which contract design fits?Design

Build It

code/main.py implements four pieces:

  • A ScopeContract dataclass (glob lists for allowed, forbidden and soft docs paths, plus acceptance commands, rollback plan, approvals and the time, network and violation budgets) and a merge_contracts function for the least-privilege merge.
  • A RunSummary dataclass holding the touched files, the commands run, the elapsed minutes and the network hosts. There is no diff parser; the demo builds both summaries by hand.
  • A scope_check that compares a RunSummary against the contract and returns a ScopeReport: writes sorted into in-scope, off-scope, soft off-scope and forbidden, plus severity-tagged findings.
  • Two demo runs: one stays in scope, one creeps. The checker flags the creep with the exact file and reason.

Run it:

python3 code/main.py

The output shows the contract, the two runs, the per-run verdicts, and the reports archived to closed/T-001.json (both runs share a task id, so the creep run overwrites the clean one).

Check: Build It0/8
1. In the demo, the project contract allows app.py, test_app.py and lib/**/*.py, and the task contract allows app.py and test_app.py. What is the merged allowed_files?Apply
2. In the demo's creep run, which touched files end up in forbidden_writes under the merged contract?Apply
3. In the creep run, README.md is touched and is not in allowed_files. What finding does scope_check give it with the demo's default settings?Apply
4. The merged time budget is 30 minutes (the project allows 60, the task 30). The creep run took 42.1 minutes. What does scope_check report for time?Apply
5. The merged network_egress is the intersection of the project's two hosts and the task's api.anthropic.com. The creep run contacted api.anthropic.com and evil.example. What does the checker report?Apply
6. In the demo, the creep run raises block findings but no off_scope warn findings, and the merged violation_budget is 0. What do passed() and over_budget print?Apply
7. A touched path matches both a forbidden glob and an allowed glob in scope_check. How is it classified?Apply
8. A run touches only files inside allowed_files, yet scope_check returns passed=False with an acceptance.missing finding. What is the most likely cause?Analyze

Production patterns in the wild

One write-up (a single dev.to post) reports that a practitioner running "specsmaxxing" (scope contracts in YAML before invoking the agent) saw the rabbit-hole rate drop from 52% to 21% in three weeks without changing the agent. The post credits the contract, not the model. Four patterns make the gain stick.

Violation budgets, not binary failures. agent-guardrails (the OSS merge gate used by Claude Code, Cursor, Windsurf, Codex via MCP) ships a violationBudget per task. Minor scope slips within budget surface as warnings, and the merge gate refuses only once the budget is exceeded. Pair it with violationSeverity: "error" | "warning". The budget is the difference between a gate that ships and a gate that gets disabled by the team that hated it.

Severity asymmetry by path family. Off-scope writes to docs/** are usually warn. The demo goes one step softer: paths matching docs_paths_soft (docs/**, README.md, **/*.md) produce an info finding (scope.soft_off_scope), and any other off-scope write is a warn that counts against the budget. Writes to scripts/**, migrations/**, config/prod/** are always block, which in the demo means listing them in forbidden_files. That asymmetry belongs in the contract, not the runtime, because it's project-specific and changes per task.

Time and network budgets next to file budgets. A time_budget_minutes field bounds the wall clock, and the runtime refuses to continue past it without re-approval. A network_egress allowlist on hostnames stops the agent from quietly hitting an external API that wasn't part of the task. These are scope dimensions too. File globs are necessary but not sufficient.

Multi-contract merge semantics (least privilege). When two scope contracts apply (say, a project-wide one plus a task-specific one), the merge works like this:

  • Intersect allowed_files: both contracts must permit the path.
  • Union forbidden_files: either contract can prohibit it.
  • time_budget_minutes takes the most restrictive value (min).
  • approvals_required accumulates.
  • network_egress is None for no enforcement, [] for deny-all, and [...] as an allowlist. Under merge, None defers to the other side, two lists intersect, and deny-all stays deny-all.

State this in the contract schema so the merge is mechanical and reviewable.

Check: Production patterns in the wild0/10
1. What does a violation budget do in a scope gate?Define
2. What are the lesson's merge semantics when a project contract and a task contract both apply?Define
3. Why prefer a violation budget with severity tiers over a hard stop on every slip?Explain
4. The specsmaxxing write-up reports the rabbit-hole rate falling from 52% to 21% in three weeks. What does the post credit for the change?Explain
5. How should severity differ by path family, and where should that rule live?Explain
6. Why do time and network budgets belong in the contract too?Explain
7. A project contract allows src/** and a task contract allows src/auth/** and forbids src/auth/legacy/**. The agent writes src/billing/invoice.py. What does the merged check do?Apply
8. A team's gate blocked every run that touched a README or a docs page, and after two weeks they switched it off. Which lesson idea would have kept it alive?Analyze
9. An engineer sets network_egress to [] on a task contract, believing it means no restriction. What actually happens?Analyze
10. Your agent already has tight file globs, but it must also be unable to quietly call an external API that was never part of the task. What should you add to the contract?Design

Use It

Three places this shows up in production:

  • Claude Code slash commands. A /scope command writes the contract and pins it as session context. Subagents read the contract before acting.
  • GitHub PRs. Push the contract as a JSON file in the PR body or as a checked-in artifact, and let CI run the scope checker against the merge diff.
  • LangGraph interrupts. A scope violation triggers an interrupt. The handler asks the human whether the contract needs to grow or the agent needs to back off.

The contract travels with the task. When the task closes, archive it under outputs/scope/closed/.

Check: Use It0/2
1. Which production integration points does the lesson name for scope contracts?Define
2. You want scope checks to run on every agent change in an existing GitHub workflow, with a human deciding when the agent crosses the line. Which setup fits?Design

Ship It

outputs/skill-scope-contract.md generates a scope contract from a task description, along with a glob-aware checker that runs in CI on every agent diff.

Author's Note

I'd start a contract with forbidden_files and the rollback plan, and fill in the goal after. The lesson says the negative space is half the contract, and I take that at face value: an allowed list alone leaves half of it unwritten. I also treat the rollback paragraph as an approval test. If I can't write down how to undo the task, I don't approve the contract.

My bet on enforcement is a violation budget with severity tiers rather than a hard stop on every slip. A gate that gets disabled by the team that hated it isn't checking anything, so I'd let docs/** slip with at most a warning and keep migrations/** blocking.

Exercises

  1. Add a network_egress field listing allowed external hosts. Refuse runs that touch other hosts.
  2. Extend the checker to fail soft on docs/** and hard on scripts/**. Justify the asymmetry.
  3. Make the contract derive allowed_files from a goal field using a static rule set (no LLM). What goes wrong on the first edge case?
  4. Add a time_budget_minutes and refuse to continue once the wall clock exceeds it.
  5. Run two contracts against the same diff. What is the right merge semantics when both apply?

Key Terms

TermWhat people sayWhat it actually means
Scope contract"The task brief"Per-task file (a dataclass in this lesson's build; YAML in the specsmaxxing example) listing allowed/forbidden files, acceptance, rollback
Scope creep"It also touched..."Files outside the contract changed in the same task
Rollback plan"We can revert"The one-paragraph operator runbook for halting
Approval boundary"Needs sign-off"An action listed in the contract as requiring explicit human approval
Diff check"Path audit"Comparing touched files against the contract globs

Further Reading

Code

Ships With