Public: anyone can read this; the crew can change it.

The agent can keep a private note on a deal that never reaches a client #308

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Each deal now has a "Private note" box on its page (up to 500 characters). The note shows only there and never reaches a draft, timeline or handout.

shipped production is serving it deploy d2eada7

opened this

Each deal now has a "Private note" box on its page (up to 500 characters). The note shows only there and never reaches a draft, timeline or handout.

The full bun test run had one failure when I ran it: a speed test ("the clock with closed deals answers within budget…") failed in the full run but passed when I reran speed.test.ts alone. A hand-built deal in partycontact.test.ts lacked the new note field, so I gave it one. I did not rerun the whole suite after that.

What changed

  • Storage: a new last migration adds deal.note (text, at most 500 characters), and migrations.pin.json pins it. migrations.test.ts now excludes it when it rebuilds an older database.
  • Saving: setDealNote in src/deals.ts asks permit(update) on deal and is audited as deal.note. Line breaks are kept. Any other control character, or more than 500 characters, is refused. Empty clears the note.
  • Doors:

- Page: a plain form posting to POST /deals/:id/note. It goes through the same same-origin door as every other form, and the output is escaped. - API: PATCH /api/deals/:id takes note, alone or together with other fields. A bad note is a 400 with field note before anything else is applied. - OpenAPI: the deal schema and the PATCH body gain note, and contract/openapi.json is re-emitted.

  • Export and import: the note travels as private_note. Import validates it and refuses a bad one with a 422 naming the field.
  • Activity page: it words deal.note.
  • Docs: README has one line and docs/TECHNICAL.md has the details.
  • Tests: src/dealnote.test.ts covers the PATCH rules and audit row, the form, escaping and the export and import round trip. A note containing the word ZEBRAWORD is checked for absence from the client timeline, broker file, calendar file, sheets, This week, the clock, the spreadsheet export, the reminder drafts and the status update.

Policy: no contract cell changed. The existing deal update cell is user, so any signed-in user who can reach a deal can overwrite its note, as they can its address.

Not done

  • No MCP tool or terminal command for the deal note; the issue didn't ask for either.
  • A refused form save comes back as a notice rather than keeping the typed text.

Checks not run

  • scripts/gate.ts and design-lint.ts: neither exists in this repo. I ran bun run lint:design instead and it was clean.
  • tsc: there is no tsconfig.json, so bun x tsc --noEmit has nothing to check.

To try it: open any deal, type in "Private note", save, then open the client timeline and the broker file; the note isn't there.

🤖 Generated with Claude Code

Closes #306

commented

Each deal now has a "Private note" box on its page (up to 500 characters). The note shows only there and never reaches a draft, timeline or handout.

The full bun test run had one failure when I ran it: a speed test ("the clock with closed deals answers within budget…") failed in the full run but passed when I reran speed.test.ts alone. A hand-built deal in partycontact.test.ts lacked the new note field, so I gave it one. I did not rerun the whole suite after that.

What changed

  • Storage: a new last migration adds deal.note (text, at most 500 characters), and migrations.pin.json pins it. migrations.test.ts now excludes it when it rebuilds an older database.
  • Saving: setDealNote in src/deals.ts asks permit(update) on deal and is audited as deal.note. Line breaks are kept. Any other control character, or more than 500 characters, is refused. Empty clears the note.
  • Doors:

- Page: a plain form posting to POST /deals/:id/note. It goes through the same same-origin door as every other form, and the output is escaped. - API: PATCH /api/deals/:id takes note, alone or together with other fields. A bad note is a 400 with field note before anything else is applied. - OpenAPI: the deal schema and the PATCH body gain note, and contract/openapi.json is re-emitted.

  • Export and import: the note travels as private_note. Import validates it and refuses a bad one with a 422 naming the field.
  • Activity page: it words deal.note.
  • Docs: README has one line and docs/TECHNICAL.md has the details.
  • Tests: src/dealnote.test.ts covers the PATCH rules and audit row, the form, escaping and the export and import round trip. A note containing the word ZEBRAWORD is checked for absence from the client timeline, broker file, calendar file, sheets, This week, the clock, the spreadsheet export, the reminder drafts and the status update.

Policy: no contract cell changed. The existing deal update cell is user, so any signed-in user who can reach a deal can overwrite its note, as they can its address.

Not done

  • No MCP tool or terminal command for the deal note; the issue didn't ask for either.
  • A refused form save comes back as a notice rather than keeping the typed text.

Checks not run

  • scripts/gate.ts and design-lint.ts: neither exists in this repo. I ran bun run lint:design instead and it was clean.
  • tsc: there is no tsconfig.json, so bun x tsc --noEmit has nothing to check.

To try it: open any deal, type in "Private note", save, then open the client timeline and the broker file; the note isn't there.

🤖 Generated with Claude Code

Changes requested — before the latest push

Solid work on the page, API, export/import, audit and the leak test. One change needed.

Fix first: the terminal door is missing. The review bar says whatever a person can do on the page they can do in src/tty.ts, and the README must say how. Today the private note can be read and written only on the web page and in the API; src/tty.ts has no way to see or save it. Add a key on the deal view in src/tty.ts that shows the note and prompts for a new one (empty clears it). It must call setDealNote, which asks permit(update). Reads must be gated the same way the page gates them. A write made there should reach web viewers live, as other tty writes do. Add a test for it, add a line to the README next to the note sentence saying which key to press, and update docs/TECHNICAL.md. If the repo has an MCP tool surface, add a set_deal_note tool the same way. The builder's note that the issue did not ask for it does not override this bar.

Smaller points to fix in the same round:

  • README: the added sentence makes one very long line in the middle of a paragraph. Rewrap it.
  • The form route (POST /deals/:id/note) has no test for a signed-in non-owner or guest. The test only checks that an unsigned post leaves the note unchanged. Assert the status as well.
  • Run the full bun test once after the last edit. The builder reported not doing so.
commented

The private note can now be read and written in the terminal and over MCP, and the smaller review points are fixed too. The full bun test run is green (813 pass). The first full run had two timing-budget failures that did not reproduce when I reran those files alone, or on a second full run.

  • Terminal: open a deal and press K. The prompt shows the current note and takes a new one; an empty line clears it. The deal header now shows Private note: …. The key calls setDealNote, which asks permit(update). Reading goes through openDeal, which reads the deal through permit like the page does. A write emits deal.changed, so web viewers update live. A test in terminal.test.ts covers setting, showing, clearing, the deal.note audit row, and a guest being refused.
  • MCP: new set_deal_note tool, with the same 500-character rule. src/dealnote.test.ts tests it, and the tool list in server.test.ts includes it.
  • Docs: the README line is rewrapped and now names K. docs/TECHNICAL.md lists the terminal key and the MCP tool.
  • Form test: a guest's post is asserted as a 303 that leaves the note unchanged.
  • Where I disagree: you asked for a signed-in non-owner case to be asserted as refused. contract.json sets deal update to user, so another signed-in user is allowed, exactly as they can already edit the address. The test asserts that 303 and documents it. Making it author-only would change a contract cell, and I won't do that without the issue asking.
commented

I merged main into the branch, resolved the four conflicts, and pushed. The full bun test run passes (820 pass, 0 fail).

  • src/db.ts: the new main migration (deadline_move.undo) stays where main put it. My deal.note migration comes after it, still last.
  • src/migrations.pin.json: pins run in the same order — main's new hash, then mine.
  • src/migrations.test.ts: the older-database filter now excludes both new migrations.
  • src/terminal.ts: I took main's key help, which adds U undo move, and kept K private note on the deal-view line.

No contract cell changed.

Approved

Earlier asks are met: the terminal reads and writes the note (K, gated by permit, shown in the deal header, with a test including the guest), the MCP tool set_deal_note exists and is tested, and the README names K. The non-owner case stays allowed because it uses the existing deal update cell for a user, and no contract cell is loosened. Input is validated at the boundary, output is escaped, and the absence test uses ZEBRAWORD across the timeline, file, ics, sheets, week, drafts, status and export. The preview answers 200. I could not diff since 4a2ec06 because the clone lacks that commit, so I read the whole change. Non-blocking: PATCH with a note plus other fields applies them in two steps, which is not atomic, though the note is validated first.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs