Skip to content

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_progress state 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 lifecyclenot_startedin_progress → (passing | blocked). passing requires evidence. blocked requires 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 refuse

The 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 status

Now try to over-activate again:

sh
pnpm wip activate import-003   # should succeed; ask-002 is now passing
pnpm wip activate status-002   # MUST refuse

If 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.0

VCR 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-002

Step 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 OK

The 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.json and let WIP=1 hold the line.
  • Marking passing without a test or runnable evidence. Module 08 will encode evidence as a required field; for now, do not lie to yourself.
  • Letting vcr_target stay 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.mjs to 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.