Skip to main content

Lab 10 solution

This is the finished state of Lab 10 — Capstone: a Mission to retire float money. A Mission's output varies from run to run, so for the files the Mission writes, this page describes what a good result contains rather than one exact text. The files you write yourself, mission.md and T9, are shown in full. Answers to the Q-prompts are in the facilitator keys.

Files you write​

The Mission file​

Three milestones, each with validation criteria a worker can check: a command to run, a file to open, or a diff to read.

mission.md
# Mission: retire float arithmetic on money outside src/money/

Run with `/missions` (paste this brief), or headless:
`droid exec --mission --auto high -f mission.md`

## Goal

Every amount in larkspur-payments is `Money` (integer cents) and every rate is
basis points, everywhere, not just in new code. Code written before
`src/money/amount.ts` existed still does float arithmetic on amounts. Find all
of it outside `src/money/`, replace it with the Money helpers, and add a guard
so it cannot come back.

Known sites (a starting point, not a complete list; finding the rest is part of
the work):

- `src/fees/calc.ts` `estimateCashback()`: `Math.floor(spend * 0.015)`
- `src/fees/calc.ts` `statementLine()`: `(fee / 100).toFixed(2)`
- `src/transfers/limits.ts` `isNearDailyLimit()`: `limit * 0.9`

## Constraints

- Follow AGENTS.md. Do not edit `src/money/`, `migrations/` or `legacy/`. If the
helpers in `src/money/amount.ts` cannot express something, stop and ask.
- Use the `money-change` skill for every replacement.
- No test's expected value may change unless the milestone lists the change
and the reason. Rounding changes are behaviour changes (for example
`Math.floor` → half-even in `estimateCashback`): list each one for a human.
- The hooks in `.factory/hooks.json` stay on for the whole mission.
- This is a service with no UI. Validation is the test suite, the lint rule
and the scripted checks below. There is no app for a user-testing validator
to drive: if that validation cannot run, say so, and switch off only the
user-testing validation in Mission Control settings. Keep scrutiny
validation on.

## Milestone 1: Inventory and characterization

Find every float operation on money outside `src/money/` (`* 0.`, `* 1.`,
`/ 100`, `toFixed`, `Math.round`, `Math.floor`, `parseFloat`, and anything else
that treats cents as a float). Write characterization tests that pin current
behaviour for each site before anything changes, including half-cent inputs.

Validation:
- `docs/float-money-inventory.md` lists every site with file:line, what it
computes, and whether the replacement can change any output.
- New characterization tests pass against the unchanged code.
- `git diff --name-only` shows only test/ and docs/ changes.
- `npm run typecheck && npm test` pass.

## Milestone 2: Replace

Rewrite each site with `cents()`, `basisPoints()`, `multiplyBasisPoints()` and
`format()`. Keep public function names and signatures.

Validation:
- For each site, the characterization tests still pass, or the expected value
changed only where Milestone 1 predicted it, with the reason in the test.
- The `money-reviewer` droid reviews the full diff and reports no P0 or P1
findings.
- `grep -rnE '\* ?[01]\.[0-9]|/ ?100\b|toFixed\(|Math\.(round|floor)\(|parseFloat\(' src --include=*.ts | grep -v '^src/money/'`
returns nothing (or only lines the inventory explains are not money).
- `npm run typecheck && npm test` pass. Nothing under protected paths changed.

## Milestone 3: Lock it in

Add a `no-float-money` rule to `scripts/lint.mjs` that reports the Milestone 2
patterns as errors in `src/` outside `src/money/`, with a test that proves it
fires on a sample line and stays quiet on `multiplyBasisPoints`.

Validation:
- `npm run lint` exits 0 on the finished tree, and exits 1 if you add
`const x = amount * 0.015;` to any file in `src/fees/` (try it, then revert).
- `npm run scoreboard -- --dry-run` still runs and every check that passed
before the mission still passes.
- `bash .factory/commands/ship-check.sh main` passes (use the branch the
mission started from). This is the script behind `/ship-check`. Custom slash commands run from the chat input, so a worker
runs the script directly.
- Final summary: sites changed, behaviour changes a human must sign off, and
anything deliberately left alone.

Plus the section only you can write, added under the title:

Readiness decision, added to mission.md
## Readiness decision

/readiness-report puts larkspur-payments at Level 3. Factory recommends
Level 4 or higher for Missions. We are running a deliberately narrow Mission:
three milestones, validation at the end of each, no protected path in scope,
hooks on throughout, and no merge without human review. Level 4 criteria we
still miss: fast CI feedback, deployment frequency, flaky test detection.

Decided by: <your name>, <date>. Reviewed by: Dana Okafor.

What makes this file work, in order of importance:

  1. Validation per milestone, written as checks. "Tests pass" is checkable. "Code is clean" isn't. Every criterion here is a command, a file or a diff a validation worker can look at, and milestone 3 even says how to prove the lint rule fires (plant a line, watch it fail, revert).
  2. Behavior changes go to a human. Replacing Math.floor with half-even changes some outputs by design. The file says so in advance, requires milestone 1 to predict each change, and allows an expected value to change only where it was predicted and explained. The orchestrator never has to guess whether a changed number is a fix or a bug.
  3. The hooks stay on. The constraint is stated even though the hooks would stay on anyway, because a reader (Ruth) should not have to know that workers inherit them.
  4. The readiness decision, with a name on it. Factory recommends Level 4 or higher for Missions, and Larkspur is at Level 3. The file records who decided to proceed and why.

The ninth task​

scoreboard/tasks/T9.md
# PAY-457: Same-day ACH

**Type:** Story · **Priority:** High · **Reporter:** Treasury operations

Business customers keep asking for same-day ACH, and the settlement window
opens for us next month.

## What we need

- A `sameDay` boolean on `TransferRequest` and on `POST /transfers`.
- Same-day transfers carry a fee of 0.25% of the amount, minimum $1.00,
posted to the ledger together with the transfer.
- If a same-day transfer fails, record it in the audit log with the reason.
- Document the field and the fee in docs/api.md.
- Tests.

Register it by adding T9: 'Same-day ACH fee' to the TASKS map at the top of scoreboard/run.mjs; the runner rejects task ids it doesn't know. Its check, scoreboard/checks/T9.mjs, exports check(dir, { base }) like the other eight and asserts, in order: nothing changed under src/money/, migrations/ or legacy/; no float arithmetic on money was added in src/ (floatMathAdded from lib.mjs); docs/api.md documents sameDay; the suite passes (suiteReasons); and a probe test, written with withProbe() the way T1.mjs does it, confirms that a $1,002.00 same-day transfer carries a 250-cent fee (half-even; half-up gives 251), that its posting balances, and that a failed one is logged with a masked account number. The lab page has a sketch of everything but the probe.

What the Mission should leave behind​

Milestone 1. docs/float-money-inventory.md lists the three known sites with file and line: estimateCashback() and statementLine() in src/fees/calc.ts, and isNearDailyLimit() in src/transfers/limits.ts. For each, it says what the replacement can change: estimateCashback can rise by a cent where half-even rounds up and Math.floor didn't; statementLine gains a thousands separator from format(); isNearDailyLimit is unchanged for the current limit (90% of $25,000.00 is exactly $22,500.00). Characterization tests for all three pass on the unchanged code, and only test/ and docs/ changed.

Milestone 2. estimateCashback() returns multiplyBasisPoints(spend, basisPoints(1.5)). statementLine() builds its text with format(fee). isNearDailyLimit() compares against multiplyBasisPoints(limit, basisPoints(90)). Public names and signatures are unchanged. The expected values that changed are exactly the ones milestone 1 predicted, each with a comment naming the human decision. money-reviewer reports no P0 or P1, the grep in the Mission file returns nothing, and src/money/ is unchanged.

Milestone 3. scripts/lint.mjs has a no-float-money rule for src/ outside src/money/, reported as an error, with a test that proves it fires on a sample line and stays quiet on multiplyBasisPoints. npm run lint exits 0 on the finished tree and 1 with const x = amount * 0.015; planted in src/fees/. npm run scoreboard -- --dry-run and bash .factory/commands/ship-check.sh main pass.

What a good run looks like​

Readiness. /readiness-report returns a level (Level 3 in our run), and the decision to proceed is in mission.md before /missions runs.

Planning. Droid asks at least one question the file didn't settle, most often whether milestone 2 should stop for sign-off on the rounding change. The approved plan had 8 features in 3 milestones in our run, a floor of 14 runs.

The run (illustrative):

SignalValue
Runs18, against a floor of 14 (8 features + 2 × 3 milestones)
Validation failures1, in milestone 2, resolved by a human decision
PreToolUse blocks2, both on the redirected floorBasisPoints() shortcut: one Edit, one shell write
Files changed under protected paths0
Pushes0. A human opened the pull request, and Lab 9's CI ran on it

T9. Three runs, and at least one fails. If all three pass, the task is too easy; make it harder before you trust it.

Ruth's memo. Condition 1 met, with two limits stated (the shell guard pattern-matches, and the CI approval is a label that is only as strong as CODEOWNERS and branch protection). Condition 2 partly met: there's no automatic link yet from a pull request to the sessions that produced it. Condition 3 met. Every row points at a file.

Common wrong turns​

  1. Starting the Mission at Low. The project default from Lab 4 is Low, and the docs say Missions orchestration requires High autonomy (or --skip-permissions-unsafe, which you shouldn't use). Check the level before you run /missions, and raise it with Ctrl+L. Before you do, re-read the lab's table of what does and doesn't change at High.

  2. Buying speed with validation. One milestone instead of three, or missionModelSettings.skipScrutiny turned on, cuts the run count and removes the only point where a wrong assumption gets caught before it spreads. Spot it: a plan with a single validation block at the end, or a Mission that finishes in fewer runs than features plus twice the milestones, which suggests some validation never ran (the heuristic is rough, so check the Mission's log). skipUserTesting is a different case: the docs say you can turn off QA-style user testing when a project doesn't need it, and Larkspur has no script that stands up the running API. If you turn it off, write down why next to the readiness decision.

  3. A ninth task that coaches. The first draft of T9 usually says "use basis points" or "remember to mask the account". Then it passes 3/3 and measures the task writer. Spot it: search T9.md for basisPoints, roundHalfEven, maskAccount or "invariant". None of them should be there; the harness supplies them.

Also watch for a headless run without --auto high. The docs disagree: the CLI reference and the Droid Exec page say --mission requires it (or --skip-permissions-unsafe), and the headless examples in the Missions reference omit it. Pass it explicitly: droid exec --mission --auto high -f mission.md.

Where it lives in harness-reference​

FilePath in the companion repo
Mission fileharness-reference/mission.md (without the readiness decision, which is yours to write)
Ninth taskNot in harness-reference. Every team writes its own, from its own backlog.

The Mission's outputs (the inventory, the characterization tests, the converted functions and the lint rule) are not in harness-reference/ either. They are what a run produces, not part of the harness.

Back to all solutions.