Skip to main content

Lab 2 solution

This is the reference end state for Lab 2 — AGENTS.md: the contract Droid reads first.

Illustrative figures

Scoreboard figures and session details on this page are illustrative until the workshop is piloted. The file itself is the real reference file.

Files at the end of the lab​

larkspur-payments
AGENTS.mdnew# committed at the repository root
AGENTS.md
# Larkspur Payments

The service that moves customer money: ACH transfers, fees and ledger
postings. Regulated: any change here can end up in front of an examiner.

## Commands

- Install: `npm ci`
- Test everything: `npm test`
- Test one file: `npm test -- test/fees/overdraft.test.ts`
- Types: `npm run typecheck`
- Lint: `npm run lint` (never run `--fix` over src/money/)
- Scoreboard: `npm run scoreboard` (costs tokens; ask first)

## Project layout

- `src/money/` Money type, arithmetic, rounding. **Protected.**
- `src/fees/` fee schedule and calculation
- `src/transfers/` ACH transfers, daily limits, idempotency
- `src/ledger/` double-entry postings
- `src/audit/` audit log and PII masking
- `migrations/` SQL migrations. **Protected: needs a CHG ticket.**
- `legacy/` vendor file parser. **Frozen: never edit. Wrap it.**
- `test/` Vitest. `test/helpers/clock.ts` controls time in tests.

## Invariants (never traded for a passing test)

1. Money is integer minor units: use `Money` (cents). No floating point
on amounts. Percentages are basis points: use `basisPoints()`.
2. Fees round half-even with `roundHalfEven()`. The published fee
disclosure requires it. Never `Math.round`, `toFixed` or
`Math.floor` on money.
3. Every ledger posting balances: total debits equal total credits.
4. Account numbers never appear unmasked in logs, errors, fixtures or
commits. Use `maskAccount()`.
5. Every transfer carries an idempotency key.

## Never

- Never edit src/money/, migrations/ or legacy/ without a human decision.
If the task seems to need it, stop and say so.
- Never change a test's expected value to make it pass. Find out why the
value differs.
- Never add retries or skips to a flaky test. Fix the cause; it is usually
time, so use `test/helpers/clock.ts`.
- Never connect to a database or call a production host.

## When you are unsure

Stop and explain what you found and what decision you need. At Larkspur a
clear question is a good outcome, not a failure.

## Verification (before you say done)

1. `npm run typecheck && npm test`
2. `git diff --name-only` shows nothing under src/money/, migrations/ or
legacy/ unless a human approved it in this session.
3. Summarize what changed and why in two or three sentences for the
reviewer, naming any invariant you relied on.

Where this lives in harness-reference/: harness-reference/AGENTS.md. It's the same file, character for character. To compare yours:

larkspur-payments
diff AGENTS.md harness-reference/AGENTS.md

Differences in wording are fine, as long as every section still names something Droid can act on. The table below says what each section must keep.

SectionWhat it must containThe task it moves
Opening paragraphWhat the service does, and that it's regulatedAll: it sets how trade-offs are weighed
CommandsExact install, test, single-file test, typecheck and lint commandsAll: they're what verification runs
Project layoutEvery protected and frozen path, marked; the test clock helperT2, T4, T8
InvariantsThe five rules, each naming its helper (Money, basisPoints(), roundHalfEven(), maskAccount())T1, T5, T6
NeverProtected paths, test expectations, retries or skips on flaky tests, databases and productionT2
When you are unsurePermission to stop, and that stopping is a good outcomeT4
VerificationTypecheck and test, a protected-path diff check, a reviewer summary naming invariantsAll

At about 2,300 characters, the file is a small fraction of the 80,000-character cap on what Droid loads at session start. That's deliberate.

There is exactly one guidance file. No CLAUDE.md, no .factory/AGENTS.md, and no nested files, unless you did Extension 1:

larkspur-payments
git ls-files | grep -iE "(agents|claude)\.md$"
AGENTS.md

If you did Extension 1, src/money/AGENTS.md appears too. It isn't part of the reference harness. Keep it on a branch until you've measured whether it changes anything.

What a good run looks like​

Dana's draft​

With Dana's draft committed, T1 and T2 both stay at 0/3. The sessions show the draft was loaded, and Droid does follow it: it adds tests and input validation, because "write tests" and "be careful" are the only concrete things it says. It still uses a float and Math.round on the fee. A good answer to Q1 rewrites one line of the draft into something that names basisPoints() and roundHalfEven().

The recitation check​

Started from src/fees/, a new session lists the five invariants and the never-list without calling a single tool. If it can only answer after reading AGENTS.md with a tool, check that the file is at the repository root and committed, and that you started a new session after adding it.

The prompt that outranks the file​

In Step 6, a good session names the protected path, the disclosure and the customer impact before asking to edit rounding.ts, and treats your request as the human decision. You reject the edit at the approval prompt. That's the expected result, not a failure of the file. The file produced a warning; only the prompt at Autonomy Off stopped the change.

The scoreboard​

Scoreboard after this labIllustrative
TaskBaselineAGENTS.md
T1 Add a 1.5% foreign transaction fee0/33/3+3
T2 Fix the flaky overdraft test0/32/3+2
T3 Enforce the daily ACH limit for business accounts3/33/3
T4 Add a memo column to transfers0/31/3+1
T5 Audit-log failed transfers1/33/3+2
T6 Extract validation from ledger/post.ts2/33/3+1
T7 Document the /transfers route3/33/3
T8 Accept the new vendor file format1/33/3+2
Total1021 / 24

Conventions and the never-list land. T2 still edits rounding on one run in three: the rule is written down, not enforced.

Expect a large move, with T2 still short of 3/3. In the reference run, T1, T5 and T8 go to 3/3, T6 to 3/3, T4 to 1/3 and T2 to 2/3, and attempted edits to protected paths fall from nine to three. Your exact numbers will vary. The two results to confirm:

  • T4 moves because of the stop condition. Open a passing T4 session. It should end without a migration file and with a sentence saying it needs an approved CHG ticket.
  • T2 still fails at least one run in three, and the failing run read the never-list. Open it. Droid quotes the rule, decides this case is an exception, and changes src/money/rounding.ts. Lab 5 opens with this session.

Three common wrong turns​

1. The scoreboard didn't move at all​

Almost always, AGENTS.md wasn't committed. The scoreboard gives each run a git worktree, and worktrees are made from commits, so an uncommitted file is invisible to every run.

How to spot it: git status --short shows ?? AGENTS.md or M AGENTS.md, or the T1 sessions show Droid using Math.round with no sign of the invariants. Fix: commit, then re-run.

2. Two guidance files that disagree​

Teams that already use other agents often have a CLAUDE.md, and some learners also put a copy in .factory/. Droid reads AGENTS.md, CLAUDE.md and the context directories .factory/, .agents/ and .agent/ at each level from the working directory up to the git root. Two files with slightly different commands give the model two answers, and you don't control which one it uses.

How to spot it: the git ls-files check above lists more than one file, or sessions alternate between two test commands. Fix: merge into one root AGENTS.md and delete the others. Factory's docs say it plainly: don't create every file.

3. Writing it like Dana's draft, only longer​

The most common rewrite adds more words of the same kind: "Always be extremely careful with rounding. Money is very important at Larkspur. Please follow best practices for financial code." It reads as more serious. It changes nothing, because none of it names a function, a path or a command. The scorer playground penalizes it, and so does the scoreboard.

How to spot it: run the file through the AGENTS.md scorer. If "Names real code" or "A never-list" fails, or the adjectives penalty is non-zero, a run has nothing to match against. Fix: for each sentence, ask "what would Droid do differently after reading this?" If you can't name the tool call, rewrite the sentence or delete it.

Also watch for​

  • Writing a task's answer into the file. A line that spells out the exact fix for one scoreboard task will move that task, and it measures nothing, because the answer is now in the file the scoreboard reads. The file should say how Larkspur works (use the clock helper for time-dependent tests), not what the fix for one task is.
  • Running lint --fix to tidy the new file's neighbors. The linter's --fix rewrites import order under src/, including src/money/. That's why the Commands section warns against it. Lab 5 catches it with a Stop hook.

Where to go next​

Lab 3 puts a human review in front of the decision the third T2 run makes, by asking for the plan before the diff.