{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"pairing-on-a-real-task","title":"Pairing with a model on real work","url":"https://addaly.com/learn/how-software-changed/pairing-on-a-real-task","position":40,"of":87,"minutes":10,"answer":"Work in steps you can run and throw away; speed only helps when reversing it is cheap.","keyIdea":"Work in steps you can run and throw away; speed only helps when reversing it is cheap."},"body":{"format":"markdown","text":"## A real task, not a demo\n\nDemos use empty directories. Here is something closer to your week.\n\nYou have a billing service, four years old, about 40,000 lines. Invoice totals are computed in one place and assume a single currency. Finance now wants line items in the currency they were charged in, with the invoice total in the account's base currency, using the rate at the time of each charge.\n\nThis is normal work: an existing system, real constraints, and consequences if you get it wrong.\n\n## Start by making it read, not write\n\nThe first request should produce no code.\n\n> Read `billing/invoice.py`, `billing/line_item.py`, `billing/fx.py`, and the migration in `migrations/0142_*.sql`. Describe how an invoice total is computed today, where currency is assumed, and every place that assumption is baked in. Do not write code.\n\nRead the answer carefully. You are not checking whether it is impressive, you are checking whether it matches what you know. If it has invented a helper that does not exist, or missed the second code path, you have found that out for the cost of one prompt instead of one diff.\n\nThis step catches the most expensive failure (a confident change built on a wrong mental model) before it exists.\n\n## Work in steps you can run\n\nThe unit of work is: one small change, run something, commit or throw away.\n\nNot \"implement multi-currency invoicing.\" Instead:\n\n> Figure: Multi-currency invoicing, in steps you can run. Steps: 1. Read, and write nothing (Describe how the total is computed today and every place a single currency is assumed. Check the answer against what you already know.); 2. Add the column and backfill it (Run the migration on a copy. Commit.); 3. Write a pure convert function, with your own tests (The assertions come from the requirement, never from the code. Commit.); 4. Change one call site (Run the suite. Commit.); 5. Change the rest (Every step ends in something you can execute, which is what keeps the cost of being wrong at fifteen minutes.). Not implement multi-currency invoicing. Speed only helps where reversing it is cheap, and small commits are the whole of what keeps it cheap.\n\n1. Add a `currency` column and backfill it with the existing default. Run the migration on a copy. Commit.\n2. Add a pure `convert(amount, from_ccy, to_ccy, at)` function with your own tests. Commit.\n3. Change one call site. Run the test suite. Commit.\n4. Change the rest.\n\nEach step has a checkpoint you can actually execute. The reason is not tidiness. It is that you want the cost of being wrong to stay at fifteen minutes, and small commits are what keeps it there.\n\n## Say what not to do\n\nConstraints are more useful than instructions, and they are usually the part people leave out:\n\n> Do not add a dependency. Do not change the public signature of `Invoice.total()`. Do not touch the migrations directory. If you think the schema needs to change, say so and stop.\n\n\"Say so and stop\" is worth having in your vocabulary. Without it, uncertainty gets resolved by guessing, and the guess arrives dressed as a decision.\n\n## Give it the real text\n\nPaste the actual stack trace, the actual failing test output, the actual file. Not your summary of them.\n\n```\nFAILED tests/test_invoice.py::test_total_with_mixed_currency\nE  decimal.InvalidOperation: [<class 'decimal.DivisionUndefined'>]\nE  billing/fx.py:88: in convert\nE      return (amount / rate).quantize(TWO_PLACES)\n```\n\nYour paraphrase drops the line number, the exception subclass, and the expression, which is where the answer usually is. This sounds obvious and it is one of the most common avoidable mistakes.\n\n## Know when to restart instead of arguing\n\nIf it is wrong twice in the same way, stop correcting it. A conversation that has gone off is difficult to steer back, because everything already said is still shaping what comes next. Start again with better framing: the constraint you discovered, the file you forgot to include, the thing you now know it misunderstood.\n\nDebugging the conversation almost always costs more than restarting it. People spend twenty minutes on the third correction because the first two felt so close.\n\n## The refusals worth practising\n\n**Never accept a fix that changes the test to match the code.** When a test fails and the proposed fix edits the assertion, that is the failure mode to watch for. Sometimes the test was wrong. Decide that yourself, out loud, with a reason.\n\n**Do not hand over architecture on a system you will maintain for four years.** Which of two designs to adopt is a bet on what changes next, and that is a judgement about your business.\n\n**Do not accept a change you do not understand, even when it works.** This is the discipline that holds the whole thing up. If you would not have written it and you cannot say why it is right, you are not finished. You have just moved the work to whoever gets paged.\n\n## About the agentic modes\n\nThe tools that run commands and edit many files unattended are where both the leverage and the risk sit. They can also drop tables, force-push, and install things. Treat them the way you would treat a script with your credentials: sandboxed, permissioned, and with the diff read before it lands.\n\nThe specific tools here change every few months, and anything written about them in 2026 will look quaint in 2028. The discipline underneath does not change: small reversible steps, a checkpoint you can run, and a human who can explain the result."},"check":{"question":"A test has failed twice. Both times you explained the failure and got back a change that did not fix it. You are now composing a third, more detailed correction. What is usually the better move?","options":[{"text":"Start a fresh session with the framing you have now: the constraint you discovered and the file you forgot to include.","correct":true},{"text":"Continue, but paste the entire file rather than the snippet, since the missing context is what is causing the repeated failure.","correct":false,"tempting":"More context sometimes helps, but it adds to a conversation whose earlier wrong turns keep shaping every subsequent answer."},{"text":"Continue, and tell it explicitly that the last two attempts were wrong so it avoids repeating them.","correct":false,"tempting":"Naming the errors keeps them in view, which tends to anchor the next attempt near the same mistaken framing."},{"text":"Accept that the task is unsuited to this way of working and write the fix entirely by hand.","correct":false,"tempting":"Sometimes correct, but it discards a working method after two failures, when the usual cause is a stale conversation rather than an unsuitable task."}]},"prev":{"slug":"when-not-to-specify","title":"When writing it down is the wrong move","url":"https://addaly.com/learn/how-software-changed/when-not-to-specify"},"next":{"slug":"git-as-a-safety-net","title":"Git is what makes speed safe","url":"https://addaly.com/learn/how-software-changed/git-as-a-safety-net"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Pairing with a model on real work', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/pairing-on-a-real-task (Accessed: 31 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Pairing with a model on real work","courseTitle":"How Software Development Changed","module":{"index":5,"slug":"the-working-loop","title":"The working loop"},"answer":null,"keyIdea":"Work in steps you can run and throw away; speed only helps when reversing it is cheap.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure 5.1","figure":{"type":"flow","steps":[{"label":"Read, and write nothing","detail":"Describe how the total is computed today and every place a single currency is assumed. Check the answer against what you already know."},{"label":"Add the column and backfill it","detail":"Run the migration on a copy. Commit."},{"label":"Write a pure convert function, with your own tests","detail":"The assertions come from the requirement, never from the code. Commit."},{"label":"Change one call site","detail":"Run the suite. Commit."},{"label":"Change the rest","detail":"Every step ends in something you can execute, which is what keeps the cost of being wrong at fifteen minutes."}],"title":"Multi-currency invoicing, in steps you can run","caption":"Not implement multi-currency invoicing. Speed only helps where reversing it is cheap, and small commits are the whole of what keeps it cheap."},"alt":"Figure: Multi-currency invoicing, in steps you can run. Steps: 1. Read, and write nothing (Describe how the total is computed today and every place a single currency is assumed. Check the answer against what you already know.); 2. Add the column and backfill it (Run the migration on a copy. Commit.); 3. Write a pure convert function, with your own tests (The assertions come from the requirement, never from the code. Commit.); 4. Change one call site (Run the suite. Commit.); 5. Change the rest (Every step ends in something you can execute, which is what keeps the cost of being wrong at fifteen minutes.). Not implement multi-currency invoicing. Speed only helps where reversing it is cheap, and small commits are the whole of what keeps it cheap."}},"blocks":[{"t":"h2","id":"a-real-task-not-a-demo","runs":[{"text":"A real task, not a demo"}]},{"t":"p","runs":[{"text":"Demos use empty directories. Here is something closer to your week."}]},{"t":"p","runs":[{"text":"You have a billing service, four years old, about 40,000 lines. Invoice totals are computed in one place and assume a single currency. Finance now wants line items in the currency they were charged in, with the invoice total in the account’s base currency, using the rate at the time of each charge."}]},{"t":"p","runs":[{"text":"This is normal work: an existing system, real constraints, and consequences if you get it wrong."}]},{"t":"h2","id":"start-by-making-it-read-not-write","runs":[{"text":"Start by making it read, not write"}]},{"t":"p","runs":[{"text":"The first request should produce no code."}]},{"t":"quote","runs":[{"text":"Read "},{"text":"billing/invoice.py","code":true},{"text":", "},{"text":"billing/line_item.py","code":true},{"text":", "},{"text":"billing/fx.py","code":true},{"text":", and the migration in "},{"text":"migrations/0142_*.sql","code":true},{"text":". Describe how an invoice total is computed today, where currency is assumed, and every place that assumption is baked in. Do not write code."}]},{"t":"p","runs":[{"text":"Read the answer carefully. You are not checking whether it is impressive, you are checking whether it matches what you know. If it has invented a helper that does not exist, or missed the second code path, you have found that out for the cost of one prompt instead of one diff."}]},{"t":"p","runs":[{"text":"This step catches the most expensive failure (a confident change built on a wrong mental model) before it exists."}]},{"t":"h2","id":"work-in-steps-you-can-run","runs":[{"text":"Work in steps you can run"}]},{"t":"p","runs":[{"text":"The unit of work is: one small change, run something, commit or throw away."}]},{"t":"p","runs":[{"text":"Not “implement multi-currency invoicing.” Instead:"}]},{"t":"ol","start":1,"items":[[{"text":"Add a "},{"text":"currency","code":true},{"text":" column and backfill it with the existing default. Run the migration on a copy. Commit."}],[{"text":"Add a pure "},{"text":"convert(amount, from_ccy, to_ccy, at)","code":true},{"text":" function with your own tests. Commit."}],[{"text":"Change one call site. Run the test suite. Commit."}],[{"text":"Change the rest."}]]},{"t":"p","runs":[{"text":"Each step has a checkpoint you can actually execute. The reason is not tidiness. It is that you want the cost of being wrong to stay at fifteen minutes, and small commits are what keeps it there."}]},{"t":"h2","id":"say-what-not-to-do","runs":[{"text":"Say what not to do"}]},{"t":"p","runs":[{"text":"Constraints are more useful than instructions, and they are usually the part people leave out:"}]},{"t":"quote","runs":[{"text":"Do not add a dependency. Do not change the public signature of "},{"text":"Invoice.total()","code":true},{"text":". Do not touch the migrations directory. If you think the schema needs to change, say so and stop."}]},{"t":"p","runs":[{"text":"“Say so and stop” is worth having in your vocabulary. Without it, uncertainty gets resolved by guessing, and the guess arrives dressed as a decision."}]},{"t":"h2","id":"give-it-the-real-text","runs":[{"text":"Give it the real text"}]},{"t":"p","runs":[{"text":"Paste the actual stack trace, the actual failing test output, the actual file. Not your summary of them."}]},{"t":"code","lang":"","label":"Text","text":"FAILED tests/test_invoice.py::test_total_with_mixed_currency\nE  decimal.InvalidOperation: [<class 'decimal.DivisionUndefined'>]\nE  billing/fx.py:88: in convert\nE      return (amount / rate).quantize(TWO_PLACES)"},{"t":"p","runs":[{"text":"Your paraphrase drops the line number, the exception subclass, and the expression, which is where the answer usually is. This sounds obvious and it is one of the most common avoidable mistakes."}]},{"t":"h2","id":"know-when-to-restart-instead-of-arguing","runs":[{"text":"Know when to restart instead of arguing"}]},{"t":"p","runs":[{"text":"If it is wrong twice in the same way, stop correcting it. A conversation that has gone off is difficult to steer back, because everything already said is still shaping what comes next. Start again with better framing: the constraint you discovered, the file you forgot to include, the thing you now know it misunderstood."}]},{"t":"p","runs":[{"text":"Debugging the conversation almost always costs more than restarting it. People spend twenty minutes on the third correction because the first two felt so close."}]},{"t":"h2","id":"the-refusals-worth-practising","runs":[{"text":"The refusals worth practising"}]},{"t":"p","runs":[{"text":"Never accept a fix that changes the test to match the code.","b":true},{"text":" When a test fails and the proposed fix edits the assertion, that is the failure mode to watch for. Sometimes the test was wrong. Decide that yourself, out loud, with a reason."}]},{"t":"p","runs":[{"text":"Do not hand over architecture on a system you will maintain for four years.","b":true},{"text":" Which of two designs to adopt is a bet on what changes next, and that is a judgement about your business."}]},{"t":"p","runs":[{"text":"Do not accept a change you do not understand, even when it works.","b":true},{"text":" This is the discipline that holds the whole thing up. If you would not have written it and you cannot say why it is right, you are not finished. You have just moved the work to whoever gets paged."}]},{"t":"h2","id":"about-the-agentic-modes","runs":[{"text":"About the agentic modes"}]},{"t":"p","runs":[{"text":"The tools that run commands and edit many files unattended are where both the leverage and the risk sit. They can also drop tables, force-push, and install things. Treat them the way you would treat a script with your credentials: sandboxed, permissioned, and with the diff read before it lands."}]},{"t":"p","runs":[{"text":"The specific tools here change every few months, and anything written about them in 2026 will look quaint in 2028. The discipline underneath does not change: small reversible steps, a checkpoint you can run, and a human who can explain the result."}]}],"check":{"question":[{"text":"A test has failed twice. Both times you explained the failure and got back a change that did not fix it. You are now composing a third, more detailed correction. What is usually the better move?"}],"options":[{"text":[{"text":"Start a fresh session with the framing you have now: the constraint you discovered and the file you forgot to include."}],"correct":true,"tempting":null},{"text":[{"text":"Continue, but paste the entire file rather than the snippet, since the missing context is what is causing the repeated failure."}],"correct":false,"tempting":[{"text":"More context sometimes helps, but it adds to a conversation whose earlier wrong turns keep shaping every subsequent answer."}]},{"text":[{"text":"Continue, and tell it explicitly that the last two attempts were wrong so it avoids repeating them."}],"correct":false,"tempting":[{"text":"Naming the errors keeps them in view, which tends to anchor the next attempt near the same mistaken framing."}]},{"text":[{"text":"Accept that the task is unsuited to this way of working and write the fix entirely by hand."}],"correct":false,"tempting":[{"text":"Sometimes correct, but it discards a working method after two failures, when the usual cause is a stale conversation rather than an unsuitable task."}]}],"order":[3,2,1,0],"why":null},"prev":{"slug":"when-not-to-specify","title":"When writing it down is the wrong move","minutes":8},"next":{"slug":"git-as-a-safety-net","title":"Git is what makes speed safe","minutes":9}}}