Pairs with: Lecture 07 — Why Agents Overreach and Under-Finish. Time: ~60 min. Difficulty: Intermediate. Prerequisites: Module 06 checkpoint.
Module 07. Scope Boundaries (WIP=1)
Why this module
Agents are eager. Asked to add noted ask, they will also "improve" import, "fix" the citation rule, and "tidy" the test layout, all in one PR. Lecture 07 names this overreach the scope-creep failure: the broader an agent's surface area, the lower its verified completion rate. The empirical fix is the same one human teams use — limit work in progress. WIP=1: exactly one feature is active at a time, and no second feature activates until the first one's verification turns green.
Concepts
- WIP=1 — one and only one feature in
in_progressstate at any time. Refuse to start a second. - Verified completion rate (VCR) — verified-passing features divided by activated features. Target is 1.0. When VCR drops below 1.0, no new features activate until it returns.
- Scope surface — the set of files an active feature is allowed to touch. Anything outside that set is out of scope; if you must touch it, that is a separate feature.
- Active feature lifecycle —
not_started→in_progress→ (passing|blocked).passingrequires evidence.blockedrequires a documented reason and a recovery action.
→ Read Lecture 07 for the long-form treatment, including the empirical study showing VCR drops as scope widens.
Lab
Step 1 — Sketch a tiny feature_list.json
We are not yet building the full primitive — Module 08 does that. We just need a working list to enforce WIP=1 against. Create feature_list.json at the repo root:
json
{
"project": "noted-cli",
"rules": { "wip_limit": 1, "vcr_target": 1.0 },
"features": [
{
"id": "ask-002",
"title": "noted ask supports --json output",
"scope": ["src/commands/ask.ts", "src/cli.ts", "src/commands/ask.test.ts"],
"state": "not_started"
},
{
"id": "import-003",
"title": "noted import skips files larger than 1 MB",
"scope": ["src/commands/import.ts", "src/commands/import.test.ts"],
"state": "not_started"
},
{
"id": "status-002",
"title": "noted status reports active feature id",
"scope": ["src/commands/status.ts", "src/commands/status.test.ts"],
"state": "not_started"
}
]
}Step 2 — Build the WIP=1 enforcer
scripts/wip.mjs:
js
import { readFile, writeFile } from "node:fs/promises";
const path = "feature_list.json";
const fl = JSON.parse(await readFile(path, "utf8"));
const cmd = process.argv[2];
const id = process.argv[3];
const active = fl.features.filter((f) => f.state === "in_progress");
const failing = fl.features.filter((f) => f.state === "blocked");
const passed = fl.features.filter((f) => f.state === "passing").length;
const activated = passed + active.length + failing.length;
const vcr = activated === 0 ? 1 : passed / activated;
function save() { return writeFile(path, JSON.stringify(fl, null, 2) + "\n"); }
function fail(msg) { console.error(msg); process.exit(2); }
switch (cmd) {
case "status": {
console.log(`active: ${active.map((f) => f.id).join(", ") || "(none)"}`);
console.log(`blocked: ${failing.map((f) => f.id).join(", ") || "(none)"}`);
console.log(`vcr: ${vcr.toFixed(2)} (${passed}/${activated})`);
break;
}
case "activate": {
if (!id) fail("activate requires a feature id");
if (active.length >= fl.rules.wip_limit) fail(`WIP=${fl.rules.wip_limit}; ${active[0].id} is already active. Finish it first.`);
if (vcr < fl.rules.vcr_target) fail(`VCR=${vcr.toFixed(2)} below target ${fl.rules.vcr_target}. Resolve blocked features first.`);
const f = fl.features.find((x) => x.id === id);
if (!f) fail(`unknown feature id: ${id}`);
if (f.state !== "not_started") fail(`${id} is in state '${f.state}'; cannot activate.`);
f.state = "in_progress";
await save();
console.log(`activated ${id}`);
break;
}
case "pass": {
if (!id) fail("pass requires a feature id");
const f = fl.features.find((x) => x.id === id);
if (!f) fail(`unknown feature id: ${id}`);
if (f.state !== "in_progress") fail(`${id} is not in_progress`);
f.state = "passing";
await save();
console.log(`passed ${id}`);
break;
}
case "block": {
if (!id) fail("block requires a feature id");
const f = fl.features.find((x) => x.id === id);
if (!f) fail(`unknown feature id: ${id}`);
f.state = "blocked";
await save();
console.log(`blocked ${id}`);
break;
}
default:
fail("usage: wip status | activate <id> | pass <id> | block <id>");
}Add an npm script:
json
"scripts": {
"wip": "node scripts/wip.mjs"
}Step 3 — Use it
sh
pnpm wip status
pnpm wip activate ask-002
pnpm wip status
pnpm wip activate import-003 # MUST refuseThe second activate must exit 2 with the message WIP=1; ask-002 is already active. That refusal is the harness doing its job.
Step 4 — Wire the rule into AGENTS.md
Add to the hard-constraints block (top of AGENTS.md):
md
1. **WIP=1.** Run `pnpm wip status` before any work. If a feature is
`in_progress`, finish it (or move it to `blocked` with a documented
reason) before activating another.Step 5 — Practice the lifecycle
Run a real pass through the lifecycle on ask-002:
sh
pnpm wip activate ask-002
# implement --json in src/commands/ask.ts
# add a test in src/commands/ask.test.ts
pnpm test
pnpm wip pass ask-002
pnpm wip statusNow try to over-activate again:
sh
pnpm wip activate import-003 # should succeed; ask-002 is now passing
pnpm wip activate status-002 # MUST refuseIf both refusals happened, WIP=1 is enforced and you have one passing, one in-progress feature.
Step 6 — Block-and-unblock drill
Pretend import-003 hits an upstream issue. Mark it blocked:
sh
pnpm wip block import-003
pnpm wip status
pnpm wip activate status-002 # MUST refuse — VCR < 1.0VCR is now 1/2 = 0.5. Until you either resolve import-003 (mark it passing after a fix) or remove it from activated count, status-002 stays gated. That gate is the protection against an agent quietly stacking work on a broken base.
Resolve and continue:
sh
pnpm wip pass import-003 # only after the actual issue is fixed
pnpm wip activate status-002Step 7 — Update continuity artifacts and commit
PROGRESS.md adds:
md
## Current State
- Active feature: status-002
- VCR: 2/3 = 0.67 (status-002 in_progress)sh
git add .
git commit -q -m "module-07: WIP=1 enforcement and feature_list bootstrap"Verification
sh
pnpm wip status >/tmp/m07.log 2>&1 && \
grep -q "vcr:" /tmp/m07.log && \
node scripts/wip.mjs activate ask-002 2>&1 | grep -qiE "(already active|state 'passing'|state 'in_progress')" && \
echo "M07 OK"Expected:
M07 OKThe third line probes that re-activating ask-002 refuses for some valid reason (it's either already active, already passing, or in a non-startable state). What we are confirming is that the enforcer refuses, not which exact reason.
Common pitfalls
- Adding a "while you're in there" task. That is a second feature. Add it to
feature_list.jsonand let WIP=1 hold the line. - Marking
passingwithout a test or runnable evidence. Module 08 will encode evidence as a required field; for now, do not lie to yourself. - Letting
vcr_targetstay 1.0 if you have a real long-term blocker. Better to remove the blocked feature from the list than relax the target. Removing it makes the gap explicit. - Building the enforcer to read scope and check git diffs. Tempting, but Module 09 owns boundary checks via the verify pipeline. Keep
scripts/wip.mjsto state-transitions only.
Next
Module 08 — Feature Lists as Primitives. The list works as a state machine; it does not yet describe what verification turns a feature green. Module 08 turns each feature into a (behavior, verification, state) triple.
