You will run a repo-wide mechanical change — migrating 40 modules off three legacy logging patterns onto structured logging — the way Stripe-style agent fleets do it: decompose into verifiable subtasks, run agents on batches in parallel, and build an aggregator that machine-verifies every batch before a human ever reads a diff.
The skill is not the logging swap; it is designing decomposition boundaries and verification so that agent output is trustworthy at a scale where you cannot review line-by-line.
~2 hours hands-on · Advanced.
Deterministic inline generator — no AI needed to build the scaffold. In a fresh git repo, save and run python make_scaffold.py:
# make_scaffold.py
import os
os.makedirs("app", exist_ok=True)
open("app/__init__.py", "w").close()
with open("app/legacy_log.py", "w") as f:
f.write("def log(msg):\n print('[LEGACY] ' + msg)\n")
FORMS = [
['print("[{n}] processed %d records" % count)'],
['import sys',
'sys.stderr.write("{n}: retry attempt " + str(count) + "\\n")'],
['from app.legacy_log import log',
'log("{n} finished with " + str(count))'],
['print(f"[{n}] skipped {{count}} stale rows")'],
]
for i in range(40):
n = f"task{i:02d}"
body = "\n".join(" " + ln.format(n=n) for ln in FORMS[i % 4])
with open(f"app/{n}.py", "w") as f:
f.write(f'def run(count: int) -> int:\n'
f' """Process {i} widgets."""\n'
f' result = count * {i + 1}\n'
f'{body}\n'
f' return result\n')
print("wrote 40 modules")
And the deterministic checker, check.py:
import importlib, pathlib, re, sys
BANNED = [r"\bprint\(", r"sys\.stderr\.write", r"legacy_log"]
fails = []
for p in sorted(pathlib.Path("app").glob("task*.py")):
src = p.read_text()
for pat in BANNED:
if re.search(pat, src):
fails.append(f"{p}: banned pattern {pat}")
if "logging.getLogger(__name__)" not in src:
fails.append(f"{p}: no module logger")
if re.search(r'(%\s*\(|%\s*count|"\s*\+|f"\[)', src):
fails.append(f"{p}: eager string formatting in log call")
try:
m = importlib.import_module(f"app.{p.stem}")
expected = int(p.stem[4:])
if m.run(3) != 3 * (expected + 1):
fails.append(f"{p}: behaviour changed")
except Exception as e:
fails.append(f"{p}: import/run failed: {e}")
print("\n".join(fails) or "ALL CLEAN: 40/40")
sys.exit(1 if fails else 0)
Target state per module: logger = logging.getLogger(__name__) at module level, the legacy call replaced by a logger.info(...) with lazy args (logger.info("[%s] processed %d records", name, count)), return values untouched, app/legacy_log.py deleted last. Scaffold verification: ls app/task*.py | wc -l = 40; python check.py exits 1 listing ~80+ violations; git add -A && git commit -m baseline.
harness/plan.md) before touching an agent. Decide: batch size and boundary (by file range? by legacy-pattern form? — decision point: pattern-based batches give each agent one transformation to learn, file-range batches parallelize more evenly; justify your pick in two sentences), the exact per-module transformation contract (input pattern → output pattern, with one worked example per legacy form), and the definition of done per batch: python check.py scoped to the batch passes.sys.argv support so python check.py app/task00.py app/task09.py ... (or a --range 0 9 flag) checks only that batch. This 5-minute edit is what makes parallel verification possible — agents get fast, scoped feedback.harness/task-prompt.md): the transformation contract, the worked examples, the banned patterns, "change nothing else — no docstring edits, no reformatting", and the command the agent must run to self-verify. One prompt, parameterized by file list — never hand-write per-batch prompts.git worktree add ../batch-b main) so edits can't collide. Sequential batches through one tool is an acceptable fallback — the harness, not the concurrency, is what's graded. Keep per-batch transcripts.harness/aggregate.sh): for each batch, apply/merge its diff, run the scoped check, run the full python check.py, and append a line to harness/verification.log — batch B: files=N scoped=PASS full=PASS/FAIL sha=COMMIT_SHA. A batch that fails scoped verification gets bounced back to its agent with the checker output, not hand-fixed; log the bounce. Decision point: merge order — batches touching legacy_log importers must land before deleting legacy_log.py itself.python check.py prints ALL CLEAN: 40/40, delete app/legacy_log.py, re-run, commit. Produce the human-review artifact: git diff baseline..HEAD --stat plus a 10-line sampled review (you eyeball 4 random modules, one per legacy form — record which).plan.md: which batch bounced and why, what you'd change in the task prompt for 400 files instead of 40.At fleet scale you review the harness, the sampled diffs, and the verification log — not 40 diffs. If your checker can't catch a bad batch, your decomposition isn't done, no matter how good the prompts are.
python check.py exits 0 with ALL CLEAN: 40/40; grep -rn "print(\|legacy_log\|stderr" app/ returns nothing.run(3) assertions all pass (this is your characterization layer — it ran on every batch).harness/verification.log shows one line per batch, at least one scoped check per batch, and any bounces with their reason.app/task*.py and deletes legacy_log.py — git diff baseline..HEAD --stat shows no collateral files.Keep the harness/ directory (plan.md, task-prompt.md, aggregate.sh, verification.log) plus the final aggregated diff. Self-grading rubric:
Pass = 7/10.
logger.info("... %s", x) with no + or f-string inside the call passes; if a legitimate f-string elsewhere in a module trips it, scope the regex to lines containing logger..The harness pattern is the most client-transferable skill in this track because verification is fully local and deterministic: check.py-style gates ship no code to any model. On restricted engagements, run the transformation itself with the sanctioned agent (Copilot Business) or a local model — mechanical pattern swaps are exactly where local coder models are production-usable — or with a pure codemod (libcst/jscodeshift) generated once from a redacted example, keeping the LLM out of the loop entirely. The verification.log becomes your client deliverable: evidence that every machine change passed a machine gate plus sampled human review.
No hidden seeds — the scaffold's traps are structural, and here they are for post-lab comparison: (1) four distinct legacy forms means single-example prompts under-generalize (agents nail print and miss the sys.stderr.write inline-import form, leaving a dead import sys behind — did your checker or your sampling catch dead imports? The provided checker does not: that gap is deliberate); (2) form 3 creates the dependency trap — modules importing legacy_log must migrate before the helper is deleted, or intermediate states break imports for every batch after; (3) form 4's f-string is the eager-formatting bait — a naive migration to logger.info(f"...") preserves the anti-pattern the migration exists to remove; (4) the run(3) return-value check is the behaviour pin — agents that "clean up" result = count * k get caught by the checker, which is the whole argument for verification-first decomposition.