Your colleague does not need a clever patch. They need next week's report, booking page, calculation or search result to work when you are not sitting beside them. The code you change today becomes part of somebody else's routine. A successful demonstration is a beginning, not a handoff.
Open the project you have already chosen. Find one change that would make it more useful to its next user: a clearer failure message, a missing field, a correct calculation, an accessible control, or a result that can be regenerated without manual repair. This book helps you agree what that change means before a coding agent implements it, then decide whether the actual code keeps the agreement.
Keep your project. You do not need to replace it with a tracker, a CSV task or the examples below. The lab and company code are optional, synthetic practice material. Their purpose is to make the reasoning inspectable, not to prescribe what you build.
1. Nine files for a one-sentence request
You ask a coding agent to "validate the signal before calculating the summary." It returns 600 lines across nine files. It adds a validation framework, changes error messages, reformats unrelated functions, introduces a package, and rewrites tests. Somewhere inside that patch may be the right check. Nobody can tell in ten minutes whether existing behavior survived.
The company version looks equally reasonable. You ask for one reference field on a fictional internal ticket. The patch renames the ticket model, changes the stored schema, adds search, and updates every caller. The new field appears on screen, so the demonstration passes. Yet the patch also turns a display-only request into a migration and changes title formatting.
The failure began before code was generated. "Validate the signal" and "add a reference field" name directions, not reviewable outcomes. They do not say which inputs pass, which fail, what a failure changes, or what existing behavior is protected. The agent filled those gaps with plausible decisions. A large diff then hid those decisions from the reviewer.
The missing sentence is often about another person: "The analyst must not mistake a partial summary for a complete one," or "The reviewer needs the source reference, but existing ticket titles must remain unchanged." That sentence connects a technical boundary to the work it protects. Without it, you can optimize the patch while breaking the task.
2. After this you can
- Write a narrow specification for a useful project change, with ordinary, boundary, failure and preservation criteria.
- Protect files, interfaces, data effects, dependencies and existing behavior through explicit scope and non-goals.
- Direct an agent through a plan, bounded implementation and complete diff review.
- Accept, reject or revise by citing a reproducible result rather than the agent's confidence.
- Hand over a working change that a colleague can use again, with a clear response when it fails.
3. Open the right workbench
Continue from T08-L02: Code without being a developer: you can already trace input, validation, transformation, output and the entry point. T03-L02: Context engineering helps you select the current authoritative context. Here the new skill is agreeing and checking a change that someone else depends on. T08-L04: OpenCode Desktop: from first launch to a useful project provides the deeper application and project workflow; this book is not an installation guide.
Use your existing approved coding assistant and editor. You need to see the project files, read a proposed plan, inspect a full diff, and run the project's established check. Python 3 is required only for the optional Python fixtures; do not install it or change stacks to complete your own project. Git commands below apply only if the working folder is already a Git repository.
Start in a disposable working copy or the project's established isolated development branch, not a live deployment folder. In your editor's file explorer, open the project folder and the source file responsible for the behavior. Read its callers as well: a change that looks local can alter a result consumed elsewhere. In the editor or approved assistant's project view, confirm the displayed folder before you authorize any command.
There are four places to keep distinct:
| Place | Your action | What you should see |
|---|---|---|
| Editor, project files | Read current code; create or update the change specification | The existing behavior and the exact authorized change |
| Approved assistant conversation | Request a plan first, then authorize bounded edits | File-to-criterion mapping, not an unexplained implementation |
| Local terminal, project folder | Run the existing focused check | Actual command output, including failures |
| Editor's diff view | Compare before and after, including tests | Every changed line, not just the assistant's summary |
Use only public, synthetic or explicitly approved code and data. Before sharing context with an assistant, exclude credentials, .env files, customer records, participant or patient data, unpublished results and production configuration unless that exact use is authorized. A written specification does not technically enforce access: deny unnecessary network and shell permissions, review commands, and do not grant deployment, merge, database or production authority for this local change.
If the change affects safety, regulated data, permissions, money, employment or scientific validity, an accountable domain owner must define the governing rule. Stop when that rule is absent. A passing unit test cannot make an invented rule legitimate.
4. The idea in one page
Spec-driven development reverses the usual rush to code. Write observable requirements and acceptance criteria; state what may and must not change; review a plan mapped to those statements; permit a small implementation; run the named checks and inspect the diff; then accept or reject by citing the specification. An agent may propose code, tests or wording. The accountable human still owns product intent, unresolved assumptions and release.
The specification is not a second project. For a small change, it can fit in the project's existing issue or a short Markdown file. Its value comes from decisions it makes visible, not its length.
The narrow change contract
A useful specification is a small contract between requester, implementer and reviewer. It answers three questions:
What changes?
What must not change?
How will we verify both?
Use this compact structure in the project's existing change record. If there is none, create a new SPEC.md in the working copy; do not overwrite an unrelated specification.
Change ID and accountable owner
Colleague's recurring task and useful observable outcome
Current behavior
Required behavior, with numbered acceptance criteria
Allowed files and interfaces
Must not change / non-goals
Assumptions and unresolved decisions
Verification matrix: criterion -> check -> expected evidence
Stop conditions
How the colleague uses the result and recovers from failure
The problem explains why the change matters. The required behavior states what a caller or user can observe. The allowed scope bounds the proposed diff. The must not change list protects behavior that is easy to damage accidentally. The verification matrix says what evidence will decide acceptance. A stop condition turns uncertainty or scope expansion into a pause rather than an improvisation.
For your own project, begin with a sentence such as: "When [colleague] does [recurring task], this change lets them [useful result], without changing [existing behavior they rely on]." Replace the brackets with a real intended use, not a fictional team claim. If no colleague is available yet, name the intended user and mark the handoff untested.
Specify outcomes, not your first solution
Add a regex in analysis.py describes a mechanism. It may force the wrong design and gives no expected behavior. Prefer:
When any supplied signal is blank, non-numeric, non-finite, or negative, summary creation
stops with ValueError identifying the 1-based data-row number. No summary is returned.
That statement permits several implementations while giving tests and reviewers something observable. Implementation constraints belong in the specification only when genuinely binding. No new dependency can be legitimate for a small change or a deployment policy. Use a class called SignalValidator is normally a design guess for the agent's plan, not product behavior.
Avoid words such as improve, robust, clean, properly and user-friendly unless an observable condition follows. "Handle invalid references properly" invites invention. "After trimming outer whitespace, reject a reference longer than 20 characters with ValueError; return no payload" is testable.
For a user-facing change, specify the user's observation as carefully as the function result. "An error occurs" is weak. "The existing form retains the entered values, identifies the reference field in text, and does not submit the ticket" tells a reviewer what to try. If that is your project, include keyboard use and an understandable error message; a red outline alone is not enough. These are examples of criteria to agree, not behavior implemented by the Python functions below.
Acceptance criteria cover behavior and preservation
Write criteria before the agent writes code. Give each an identifier so plans, tests and review comments can point to the same rule. Cover at least:
| Kind | Question | Example evidence |
|---|---|---|
| Ordinary | What succeeds? | Known synthetic input produces exact output. |
| Boundary | Where does behavior change? | Length 20 passes; length 21 fails. |
| Failure | How does it fail, and what remains unchanged? | Exact exception type and no returned result. |
| Preservation | What existing behavior must survive? | Existing test and protected output values still pass. |
| Scope | What repository change is allowed? | Changed-file list contains only authorized paths. |
"It runs" proves only that one path did not crash. It does not prove the boundary, denied behavior, lack of side effects or preservation of existing behavior. A test name is also not proof by itself; inspect whether its input and assertion actually represent the criterion.
Pay particular attention to a recurring result. If a run fails, can yesterday's file still look like today's success? A function that raises an exception may be correct while its surrounding workflow is misleading. Specify freshness and failure behavior at the point where the colleague consumes the result. Do not quietly add file handling or a new interface to a function-only change; either use the existing mechanism or agree a separate change.
5. From contract to a small, inspectable implementation
Keep the plan and review bounded
Ten minutes is a scope test, not a universal review deadline. Name the allowed files, permit one behavior, and exclude drive-by formatting, unrelated renames and dependencies. Security-critical or unfamiliar code may need longer; if an informed reviewer cannot map the diff to the criteria quickly, split the change.
In the approved assistant's conversation for this working copy, attach or name the specification and the relevant source and test files. Use the application's plan or read-only control if available; a prompt alone is not a permission barrier. Request:
Read SPEC.md as the authorized change contract. Do not edit or run commands yet.
Read the affected function and its callers.
Return a table: proposed change, file, acceptance criterion, verification.
List assumptions. Omit work that cannot be mapped.
Stop on contradiction or scope expansion. Do not install, deploy or publish anything.
Replace SPEC.md with your existing record's name if you used one. A useful response proposes an edit in a named file and a check for a named criterion. "Improve validation across the application" is not a plan you can authorize.
Review that response before allowing edits. If it fits, explicitly authorize only the implementation and test paths in the contract. Documentation you created for the change belongs on the overall change's allowed-path list too; it is not permission for the agent to rewrite other documentation. Ask the assistant to stop after the bounded patch and report any check it could not run.
Inspect the actual diff
Open each changed source and test file in the editor's diff view. Read the removed lines as well as the added lines. A removed assertion can matter more than a new function. Map each block to a criterion or a necessary supporting edit; reject unexplained work rather than trusting "minor cleanup."
In a Git working copy, these commands run in the local terminal at the repository root and do not change files:
git status --short
git diff --name-only
git diff
git diff --cached
git diff --check
git diff --cached --check
git diff shows unstaged changes; git diff --cached shows staged changes. Neither displays the contents of an untracked new file. Find those entries in git status --short and open each new file in the editor. Do not stage unrelated work just to make it appear in a diff. The --check commands flag whitespace errors and conflict markers, not correct behavior. Without Git, use the editor's comparison against an untouched starting copy and inspect new files separately.
Run the focused check named in the contract, in the folder it expects. Keep its command and real result in the change record. An agent's "tests pass" summary is not a substitute for output you can inspect. A check you could not run stays unrun, even if the code looks convincing.
Reject with the criterion, observed mismatch, reproduction and smallest next step. If implementation exposes a wrong requirement, pause for the owner, version the changed criterion and rerun the verification matrix. Do not approve a wrong rule merely because the agent implemented it faithfully.
6. Optional worked examples: two narrow changes
Both examples use synthetic data and the Python standard library. Read either or both to see the complete loop: contract, bad attempt, evidence-based rejection, corrected implementation, tests and review. Use your own project's code for the actual delivery if it already supplies a useful change.
To run a fixture, create a new empty practice folder through your editor or operating system's file manager. Use signal-practice for the lab example or reference-practice for the company example, and open only that folder in the editor. Create the named source file with the starting implementation. Create SPEC.md with the matching contract. The test file shown later is new. Replace only the starting source implementation when you reach the corrected version; never paste a rejected attempt over your working project.
In a local terminal opened in that practice folder, python --version should identify Python 3. If your established Python 3 command is python3 or py -3, use it consistently in the commands below. If no approved interpreter is available, read the examples and mark their execution unrun; there is no installation requirement for this book.
Lab lane: reject invalid synthetic signals
The fictional analysis.py already summarizes signal strings:
from statistics import mean
def summarize(rows):
values = [float(row["signal"]) for row in rows]
return {"count": len(values), "mean_signal": round(mean(values), 2)}
Its existing known behavior is [{"signal": "10"}, {"signal": "14"}] returning {"count": 2, "mean_signal": 12.0}. In the practice folder's terminal, reproduce that baseline with:
python -c 'from analysis import summarize; print(summarize([{"signal": "10"}, {"signal": "14"}]))'
This quoting is for PowerShell 7 or a POSIX shell; in another terminal, use its supported quoting or run the same Python statements in the interpreter. The printed dictionary should contain count 2 and mean 12.0. Record what you actually observe, not the book's expectation.
Imagine a colleague preparing a recurring summary from approved measurements. Silently losing an invalid row would give them a plausible result with a different population. For this synthetic exercise only, the rule is to reject blank, non-numeric, missing, non-finite and negative signals, while allowing zero. Real calibrated measurements may legitimately be negative; this is not a scientific validation standard.
Lab change contract
ID: LAB-SIGNAL-VALIDATION-1
Owner: learner acting as owner of this synthetic exercise
Problem
Invalid signal text currently fails inconsistently; NaN and negative values can reach the mean.
Outcome
Reject an invalid signal before calculating; preserve the existing summary for valid rows.
The intended user must not receive a partial summary as if all supplied rows were accepted.
Acceptance criteria
AC-L1: Two valid rows "10" and "14" return exactly
{"count": 2, "mean_signal": 12.0}; a single "0" returns count 1 and mean_signal 0.0.
AC-L2: Blank, non-numeric, missing or None signal raises ValueError containing the
1-based data-row number.
AC-L3: Negative, NaN or Infinity signal raises ValueError containing that row number.
AC-L4: On any invalid row, summarize returns no partial result.
Allowed implementation change
analysis.py and test_analysis.py only. Python standard library only.
SPEC.md is the separate learner-maintained contract and review record.
Must not change / non-goals
NC-L1: Do not change the summarize(rows) interface, valid-result keys, rounding, row order,
file I/O, dependencies, other functions or repository configuration.
NC-L2: Do not skip or repair invalid rows.
Assumptions
rows is an existing iterable of mappings containing signal strings or the invalid cases above.
Empty-input behavior is outside this change; arbitrary object inputs are not supported here.
Verification
AC-L1 -> exact equality tests, including zero.
AC-L2 -> blank, "missing", absent key and None tests assert ValueError and row number.
AC-L3 -> -1, NaN and Infinity tests assert ValueError and row number.
AC-L4 -> all failure tests assert that the call raises instead of returning.
Scope -> only the two implementation/test paths and the learner's SPEC.md may differ.
Stop
Stop if negative values are scientifically valid, the interface must change, another source file
is needed, or current tests contradict this contract. Resolve the decision before editing.
Use and recovery
Practice user calls summarize with the supplied synthetic rows.
On ValueError, inspect the named input row; do not skip it or reuse an old result as current.
No reporting interface or real scientific workflow is implemented by this fixture.
The plan should map one validation block in analysis.py and focused cases in test_analysis.py to AC-L1 through AC-L4. It should not propose a framework or a new parser module.
Rejected lab attempt
Suppose the first attempt uses this comprehension:
values = [float(row["signal"]) for row in rows if float(row["signal"]) >= 0]
It runs for valid inputs. It also silently drops -1 and NaN, evaluates each accepted value twice, admits positive infinity, and does not provide the required row-specific failure. NaN >= 0 is false: the row disappears instead of causing rejection. These are different bugs from a calculation that simply crashes.
An example rejection is:
REJECT AC-L3 and NC-L2. For [{"signal": "10"}, {"signal": "-1"}], this candidate returns
{"count": 1, "mean_signal": 10.0}. AC-L3 requires ValueError naming row 2, and the
non-goal says not to skip invalid rows. Replace only the validation logic and focused tests.
This is not a style dispute about comprehensions. It is an observable contract violation. The quotation is a worked review example, not evidence that an agent made this attempt in your session.
Corrected lab implementation
In the practice editor, replace the contents of analysis.py with:
import math
from statistics import mean
def summarize(rows):
values = []
for row_number, row in enumerate(rows, start=1):
try:
value = float(row["signal"])
except (KeyError, TypeError, ValueError) as error:
raise ValueError(f"Data row {row_number}: invalid signal") from error
if not math.isfinite(value) or value < 0:
raise ValueError(f"Data row {row_number}: invalid signal")
values.append(value)
return {"count": len(values), "mean_signal": round(mean(values), 2)}
The try block converts a supplied value and gives missing or unconvertible values the same row-specific error. math.isfinite excludes both infinities and NaN; the separate comparison enforces the exercise's non-negative range. Only a row that passes both steps enters values. Calculation happens after validation completes, so a later invalid row cannot produce a returned partial summary.
Create test_analysis.py beside it:
import unittest
from analysis import summarize
class SummarizeTests(unittest.TestCase):
def test_ac_l1_preserves_valid_summary(self):
rows = [{"signal": "10"}, {"signal": "14"}]
self.assertEqual(summarize(rows), {"count": 2, "mean_signal": 12.0})
def test_ac_l1_accepts_zero_boundary(self):
self.assertEqual(summarize([{"signal": "0"}]), {"count": 1, "mean_signal": 0.0})
def test_ac_l2_rejects_bad_text_with_row_number(self):
for bad_signal in ("", "missing"):
with self.subTest(signal=bad_signal):
with self.assertRaisesRegex(ValueError, "Data row 2"):
summarize([{"signal": "10"}, {"signal": bad_signal}])
def test_ac_l2_rejects_missing_values_with_row_number(self):
for bad_row in ({}, {"signal": None}):
with self.subTest(row=bad_row):
with self.assertRaisesRegex(ValueError, "Data row 2"):
summarize([{"signal": "10"}, bad_row])
def test_ac_l3_rejects_out_of_range_values(self):
for bad_signal in ("-1", "NaN", "Infinity"):
with self.subTest(signal=bad_signal):
with self.assertRaisesRegex(ValueError, "Data row 2"):
summarize([{"signal": "10"}, {"signal": bad_signal}])
if __name__ == "__main__":
unittest.main()
In the practice folder's terminal, run:
python -m unittest -v test_analysis
The expected result is five passing test methods, with seven subtest cases exercised within them. Successful subtests are not necessarily printed as separate successes. AC-L4 is checked because each invalid call must raise; no assertion accepts a shortened summary. Inspect the source diff too: the interface, output keys and rounding line should remain unchanged. Empty input is deliberately outside this contract; do not imply it was checked.
Add your real results to SPEC.md, beneath the contract. This is a record template, not a pre-filled pass:
LAB-SIGNAL-VALIDATION-1 REVIEW
Implementation/test paths: [actual paths]
AC-L1 exact output and zero boundary: [result]
AC-L2 bad text and missing values with row number: [result]
AC-L3 negative/non-finite with row number: [result]
AC-L4 no partial return: [result]
Non-goals and full diff: [result]
Command: python -m unittest -v test_analysis
Actual output: [paste observed result; expected count is 5 methods]
Rejection: [actual failed attempt, or clearly labelled critique of the supplied candidate]
Limit: synthetic exercise rule; no claim about real instrument validity
Decision: [accept for practice / reject / unresolved]
Company lane: add one reference field
The fictional internal tool creates a display payload in tickets.py:
def ticket_payload(title, owner):
return {"title": title.strip(), "owner": owner}
The request is to add an optional external reference for display. Imagine a colleague repeatedly comparing tickets with their source requests: the reference makes that task possible without changing existing title text. There is no database, API, authentication or production service in this exercise.
The new result intentionally has an extra key. Keeping two-argument calls valid does not prove complete compatibility: a real consumer might compare exact dictionaries, unpack values or validate a schema. Inspect callers in your project before agreeing to a result-shape change. This fixture assumes its synthetic consumer accepts the added field.
Company change contract
ID: COMPANY-TICKET-REFERENCE-1
Owner: learner acting as owner of this synthetic exercise
Problem
Reviewers cannot display the fictional source reference beside a ticket.
Outcome
Callers may supply an optional reference; the returned payload includes its normalized value.
Acceptance criteria
AC-C1: ticket_payload(" Printer ", "Sam", " REF-204 ") returns exactly
{"title": "Printer", "owner": "Sam", "reference": "REF-204"}.
AC-C2: Omitting reference returns the same title and owner values plus "reference": "".
AC-C3: After trimming outer whitespace, a 20-character reference succeeds; a 21-character
reference raises ValueError.
AC-C4: A non-string reference raises TypeError; no payload is returned.
Allowed implementation change
tickets.py and test_tickets.py only. The optional third function parameter may be added.
SPEC.md is the separate learner-maintained contract and review record.
Must not change / non-goals
NC-C1: Do not change title or owner normalization or the existing parameter order.
NC-C2: Do not change authorization, persistence, API schemas, search, UI layout, dependencies,
logging or other implementation files. Do not invent a reference.
Assumptions
Existing callers use two positional arguments and accept the added result key.
The field is display-only synthetic text; length means Python string length after strip().
Verification
AC-C1 -> exact three-field payload test.
AC-C2 -> existing two-argument call with exact updated payload test.
AC-C3 -> padded inputs that trim to length 20 and 21 prove trim-before-length behavior;
the first returns the exact 20 characters and the second raises ValueError.
AC-C4 -> integer reference TypeError test.
Scope -> only the two implementation/test paths and the learner's SPEC.md may differ.
Stop
Stop if the field must be stored, searched, sent over an API, restricted by a real policy,
or added through changes to other callers. Those need separate decisions and specifications.
Use and recovery
Practice user calls ticket_payload with a synthetic reference, or omits it when unknown.
On rejection, correct the source input; do not truncate or fabricate the reference.
No actual ticket form or persistent service is implemented by this fixture.
Rejected company attempt
Imagine that the agent adds the field but also "standardizes" titles:
def ticket_payload(title, owner, reference=""):
return {
"title": title.strip().title(),
"owner": owner,
"reference": str(reference).strip()[:20],
}
The demonstration with " Printer " looks correct. The implementation still fails the contract: it changes existing title capitalization, converts an integer instead of rejecting it, and silently truncates 21 characters. A precise review would say:
REJECT AC-C3, AC-C4, and NC-C1. A 21-character reference is truncated rather than rejected;
reference=204 becomes "204" rather than raising TypeError; and title "API error" changes to
"Api Error" although title behavior is protected. Reproduce with those three inputs. Restore
the existing title expression and implement only the specified reference behavior.
The rejection does not argue whether title case is attractive. It identifies three contract violations. For the colleague, silent truncation is particularly dangerous: the displayed reference may look usable while no longer identifying the source request.
Corrected company implementation
Replace only the starting contents of tickets.py:
def ticket_payload(title, owner, reference=""):
if not isinstance(reference, str):
raise TypeError("reference must be a string")
normalized_reference = reference.strip()
if len(normalized_reference) > 20:
raise ValueError("reference must be at most 20 characters")
return {
"title": title.strip(),
"owner": owner,
"reference": normalized_reference,
}
The order matters. Check the type before calling string methods; trim before deciding length; construct the payload only after validation passes. The existing title.strip() expression remains intact. The code does not "helpfully" repair input by inventing characters or discarding them.
Create test_tickets.py in the same practice folder:
import unittest
from tickets import ticket_payload
class TicketPayloadTests(unittest.TestCase):
def test_ac_c1_adds_trimmed_reference(self):
actual = ticket_payload(" Printer ", "Sam", " REF-204 ")
expected = {"title": "Printer", "owner": "Sam", "reference": "REF-204"}
self.assertEqual(actual, expected)
def test_ac_c2_keeps_two_argument_calls(self):
actual = ticket_payload("API error", "Lee")
expected = {"title": "API error", "owner": "Lee", "reference": ""}
self.assertEqual(actual, expected)
def test_ac_c3_checks_trimmed_boundary(self):
twenty_characters = "R" * 20
self.assertEqual(
ticket_payload("T", "Lee", f" {twenty_characters} ")["reference"],
twenty_characters,
)
with self.assertRaises(ValueError):
ticket_payload("T", "Lee", f" {'R' * 21} ")
def test_ac_c4_rejects_non_string_reference(self):
with self.assertRaises(TypeError):
ticket_payload("T", "Lee", 204)
if __name__ == "__main__":
unittest.main()
In the company practice folder's terminal, run:
python -m unittest -v test_tickets
Expect four passing test methods. Compare each assertion with its criterion and inspect both implementation/test files. The two-argument test protects call syntax and title capitalization while explicitly accepting the new result key. The padded inputs prove that trimming precedes the length decision: a value that trims to 20 is returned exactly; one that trims to 21 raises ValueError. No test claims to persist, authorize, search or transmit the field.
Record the actual result in the same review format as the lab lane. Keep a rejected-attempt excerpt or labelled critique and its reason beside the accepted diff. That evidence shows you can disagree with a plausible implementation; it is not a request to preserve unsafe code in an executable file or manufacture an agent failure.
When the specification changes
Suppose review reveals that company references may contain 24 characters. Working code is not permission to keep the 20-character rule. Mark AC-C3 unresolved and ask the owner. If the owner confirms 24, revise the criterion and its boundary tests together, record 20 -> 24 and the reason, then request the implementation change. Rerun all checks in this small contract, including title preservation and non-string failure. Never edit only a failing assertion to make existing code green.
In the lab lane, discovering valid negative calibrated signals is even more important. The domain specialist must decide the valid range and units. Until then, neither the exercise requirement nor its implementation is acceptable for real analysis.
Changing a requirement is not failure. Hiding the change is. A useful record lets the next maintainer tell whether intent changed or implementation was wrong.
7. Make the change usable without you
A specification settles what to build. A handoff settles how someone else can rely on it. Return to your chosen project, not necessarily either fixture. Ask what your colleague will actually open: a page, a workbook, a script, a local application or a generated result. Give them that working entry point through the approved route, not just a diff they cannot use.
Use the existing project README or the same change record for a short handoff. Name the exact application or folder, the agreed version, the permitted input, the run command or interface action, and where success appears. Copy the command you actually checked; do not substitute an invented universal command. If your project is a web interface, say which page and control to use, what result should appear and how to recognize failure.
For a recurring task, add when to repeat it and how to distinguish a current result from an old one. A manually run local change can be useful; scheduling or deployment is not required just to demonstrate dependency. Keep existing output intact until the replacement has passed its checks. If the application cannot distinguish a failed run from stale success, record that limitation and do not present unattended use as ready.
Agree a recovery path before handing over the result. In a small local project, that may simply mean keeping the last known-good version in a separate named location, leaving original inputs untouched, and telling the colleague when to stop and contact the owner. If shared state or stored data is involved, use the project's existing authorized recovery procedure. Restoring code does not reverse data changes. Do not invent a rollback command or tell a colleague to delete data as a repair.
Try a short handoff with the intended colleague using approved sample input. Ask them to perform the recurring task from the instructions, without narrating each step for them. Then show one safe failure case in the practice copy. Can they identify what went wrong, avoid using an old or partial result, and find the recovery instruction? If they need a private explanation from you, put the missing information beside the tool or in the handoff, then check that point again.
Record what happened: who tried which version, the useful result, and any unresolved obstacle. These are observations to gather, not claims to copy from this book. If nobody is available, label the change locally checked; colleague handoff not yet observed. That is a useful prepared result, but not evidence of independent use.
8. What goes wrong
The specification describes the solution
Symptom: the contract says create SignalValidator with regex X but never states which signals pass, fail or remain unchanged.
Fix: move proposed classes and algorithms into the plan. Rewrite requirements as observable inputs, outputs, errors and preserved behavior. Keep a technical constraint only when it is genuinely binding.
There is no must-not-change clause
Symptom: the requested field appears, but title formatting, dependencies and storage also change.
Fix: list protected interfaces, outputs, paths, data effects and operations. Add a preservation check for the most likely regression. Say what must not change even when it feels obvious.
The change is too large to review
Symptom: one behavior arrives with refactors, renames, generated files, formatting and dependency updates.
Fix: reject unmapped work. Separate genuinely necessary prerequisite work into its own justified change, or choose a smaller implementation. Inspect changed paths before reading details; include staged and new files, not just the default diff.
Acceptance means only that it runs
Symptom: the happy-path demonstration succeeds, so failures, boundaries and side effects are assumed correct.
Fix: map each criterion to a check independent of the agent's reassurance. Test exact output, boundary values, denied input, preservation and scope. Inspect what each assertion actually proves.
Review becomes a taste argument
Symptom: comments say "too complicated" or "I would write this differently," and the agent responds with another unrelated design.
Fix: cite the criterion or non-goal, provide a mismatch and reproduction, then request the smallest correction. If the implementation satisfies the contract but a remaining concern matters, make that concern explicit rather than moving an invisible goalpost.
The specification is never updated
Symptom: code and tests quietly adopt a new limit while the contract retains the old one.
Fix: pause for the accountable decision. Version the criterion, record why it changed, update expected evidence and rerun the verification matrix. History should distinguish changed intent from failed implementation.
Passing tests are generated to match the bug
Symptom: the agent changes implementation and expected values together, so every test passes while protected behavior disappears.
Fix: write expected outcomes before implementation, review test diffs separately and retain a known-behavior test. A green suite is useful only when its assertions still represent authorized intent.
The contract is mistaken for enforcement
Symptom: broad filesystem, network or deployment access is granted because the prompt says not to use it.
Fix: combine written boundaries with an isolated working set, least-privilege tool permissions, command approval and human review. A non-goal documents a violation; technical controls reduce the opportunity for one.
The patch works only with its author beside it
Symptom: the colleague cannot find the right version, repeat the run or distinguish failure from yesterday's success.
Fix: repair the entry point or handoff, not the colleague. Provide the checked action, success signal, failure response and safe fallback. Do not call the recurring workflow ready until those limits are clear.
9. Do it yourself: one useful accepted change
Choose one behavior in your existing project that another person needs. A report correction, a better search result, a form behavior or a bounded script change can all qualify. Use an optional fixture if suitable project code or permissions are unavailable; keep that result labelled practice. You are delivering useful working behavior supported by a change record, not completing a mandatory ticket or validation exercise.
The following 90-minute outline is a work session, not a reading-time estimate or a deadline for consequential review. If a step needs longer, leave it unresolved rather than fabricating a pass.
Minutes 0-10: establish the starting point. Open the right working copy. Name the intended colleague, their recurring task and one useful improvement. Reproduce current behavior with a permitted example, recording the exact action and result. Keep original inputs and the known-good version recoverable.
Minutes 10-25: agree the contract. In the existing change record or new SPEC.md, write the outcome, numbered criteria, allowed files, protected behavior, assumptions, checks and stop conditions. Resolve domain or policy ambiguity with its accountable owner. Include how the colleague sees success and failure; do not assume a unit-test exception is an adequate user experience.
Minutes 25-35: review a plan before edits. In the approved assistant, request file-to-criterion mapping. Reject invented product rules, unexplained paths, unnecessary dependencies and cleanup. Save the accepted plan in the same record.
Minutes 35-50: implement one bounded change. Authorize only the agreed implementation and test files. Do not approve installation, network, merge, deployment, database or production commands for this local exercise. If scope expands, pause rather than rewriting the contract merely to excuse the patch.
Minutes 50-65: inspect and challenge. Read the complete diff and new files. Run the boundary or failure check most likely to expose a mismatch. Review expected values separately from implementation. Record a concrete rejection if something fails. If the first attempt passes, critique one supplied or prose-only nonconforming candidate and label it as a hypothetical; do not damage good code to create a rejection story.
Minutes 65-78: correct and verify. Request only the smallest necessary correction. Run the contract's checks and retain actual output. Inspect ordinary behavior, boundaries, failure, preservation and changed paths. If a requirement changed, record the decision and rerun its matrix. Mark unavailable checks explicitly.
Minutes 78-90: hand over the useful result. Provide the working entry point, checked action, version, permitted input, expected result and recovery instructions. Ask the colleague to try the recurring task and a safe failure case. Record observed use or mark the handoff pending. Keep the specification, final diff, evidence and review decision together in the existing project record.
Stop without accepting if you cannot identify the owner, expected result, permitted data, exact changed paths or evidence for a consequential rule. An honest unresolved record is safer than a fabricated pass.
10. Exit check
Deliver one useful implemented project change, with its specification, final implementation or diff, actual check evidence, review decision, and colleague use and recovery instructions attached in one place. Include one concrete rejection or a clearly labelled critique of a nonconforming candidate; do not claim an observed failed attempt when none occurred.
It passes the local review when another reader can trace changed blocks and tests to numbered criteria; reproduce the ordinary, boundary, failure, preservation and scope checks; see that only authorized paths changed; and understand deliberate exclusions. If intent changed, include the old rule, accountable decision, reason, new rule and rerun evidence.
It is ready for the intended colleague only when they can reach the agreed version, perform the useful task, recognize failure and follow the safe recovery path within their permissions. Record the actual handoff outcome. A locally checked patch with an unobserved handoff remains prepared, not proven independently usable.
Do not submit sensitive data, credentials, production access or an unreviewed agent transcript. A local working result is enough for this book; deployment and shared-repository release remain separate authorized decisions.
11. Rule to remember
Say what must not change, then give the next person a result they can use without guessing.
12. Further reading & tools
- Taught: T08-L02: Code without being a developer establishes the bounded script, code-reading landmarks, known-input check and repair loop this book turns into a team-reviewable change.
- Taught: T03-L02: Context engineering supplies the current specification and relevant code context while excluding stale or unauthorized material.
- Inherited foundation: The published manuscript draws on "Product requirements & spec engineering" and "Write a narrow change contract." Their observable requirements, scope boundaries, failure rules and verification matrix are retained here; archived chapter routes are not required to follow this book.
- Catalogued, primary: NIST SP 800-218, Secure Software Development Framework (opens in a new tab) provides guidance for defining security requirements and retaining evidence that software satisfies them. This inherited reference is not a claim that the small exercise implements the full framework.
- Catalogued, primary: OWASP Code Review Guide (opens in a new tab) offers systematic code-review guidance; apply it proportionately to the risk and scope of a change.
- Taught, primary: Python
unittestdocumentation (opens in a new tab) documents the standard-library test structures and module-specific commands used in the optional fixtures. - Taught, primary: Python
math.isfinite(opens in a new tab) explains the finite-number check in the lab implementation. It does not decide which scientific values are valid for your work. - Taught, primary: Git diff documentation (opens in a new tab) explains unstaged and staged comparisons and
--check. Review new files separately as described above. - Catalogued: Claude Code (opens in a new tab) and Codex (opens in a new tab) are coding-assistant options, not requirements for this workflow. Product controls vary; use the approved interface and permissions you actually have.
- Next: T08-L04: OpenCode Desktop: from first launch to a useful project develops the application and project workflow beyond this focused change contract.
- Catalogued: Tools index is a place to compare tools after the project outcome, data policy, permissions and acceptance evidence are clear.