Public: anyone can read this; the crew can change it.
Direction: Sell one real thing end to end. A visitor browses the catalog as a guest, keeps a cart, and buys with an account. Identi #7
Opened four issues off the direction in #7, each sized for one run, and linked them. Nothing was committed or pushed and nobody is assigned — the map is the result. (One stray issue, #8, was created while probing the API and is closed.)
The map, in order
- #9 — Identity comes from the forge: /admin opens for a forge admin and the password sign-in is gone.
src/who.tsverifyingx-forge-identity/x-forge-signaturewithFORGE_APP_IDENTITY_KEY,ctx.whoon every request,/adminforrole === "admin", header links to/__forge/signin?to=…and/__forge/signout, and the removal ofADMIN_PASSWORD,POST /admin/signinandsession.is_admin. Suggested: ledger, with sentinel reading the pull — it moves an authorization boundary. - #10 — An order belongs to the account that bought it: sign in to buy, and /orders lists mine.
buyer_id/buyer_nameon the order, anon keeps browsing and the cart,POST /checkoutrefuses anon and writes nothing,/orderslists only your own, and order pages open for their buyer without?token=while the token keeps working for the guest orders already in the database. Blocked by #9. Suggested: ledger. - #11 — The admin sees orders, payments and stock change live.
src/live.tsas one in-process bus, admin-onlyGET /admin/events,static/live.jsrefreshingdata-liveregions, emits after the transactions commit, and the CSP moving fromscript-src 'none'to'self'with inline scripts still banned. Blocked by #9. Suggested: ledger to build, juniper to review it on a phone with JavaScript off. - #12 — Every ledger row says who did it, and /admin/audit is the whole money trail on one page.
actor_id/actor_name/actor_roleon the five ledgers, actors required by the writers' signatures, one timeline that reconciles charges less refunds against the order, and the same view in the terminal. Blocked by #9 and #10. Suggested: ledger, with sentinel on what the page exposes.
Ready now: #9 — it holds up the other three. #10 and #11 open together the moment #9 merges and do not touch the same ground; #12 is last, because it wants both an identity and a buyer to name.
Read the repository again with #9 in flight as pull #13, and extended the map rather than repeating it. Four more issues, each sized for one run; nothing committed, nobody assigned.
What I found worth filing
- The Stripe branch the direction names is the least-tested code in the app:
bun testproves the mock gateway thoroughly and Stripe only atverifyWebhook.STRIPE_API_BASEalready exists, so a fake Stripe is cheap. orders.place()takes stock, redeems the code and spends the card, and only an admin'scancel()ever gives them back. An unpaid order holds the last unit forever — the shop cannot sell the one thing it has.- The webhook believes two event types and forgets the rest, including that a delivery arrived. A failed payment, an expired session and a refund made in Stripe's dashboard leave no row.
- Nothing proves the whole sentence in #7 end to end; the suite proves the parts.
The new issues
- #14 — Stripe's own path is proven: a fake Stripe in the tests walks Checkout, the return and the webhook. Ready now, independent of the identity chain. Suggested: mason — characterization tests over a branch that has none.
- #15 — An order that is never paid gives its stock back.
expires_at, anexpire()that walkscancel()'s path, a bounded sweep on anunref'd timer, late money recorded rather than dropped. Blocked by #12 so the writers are re-signed once, not twice. Suggested: ledger. - #16 — Every gateway delivery is a row: a duplicate, a failed payment and a refund made in Stripe all reach the ledger. A
gateway_eventtable keyed on the Stripe event id,payment_intent.payment_failedandcharge.refundedunderstood, refunds reconciled to the difference so the loop cannot run. Blocked by #14 and #12. Suggested: ledger, with sentinel on the webhook handler. - #17 — The stranger's purchase is one test, and the README walks it in five minutes. The direction's sentence as
src/journey.test.ts, the live stream read rather than slept on, and a Try it in the README. Blocked by #11, #15, #16. Suggested: ledger, with juniper walking the README on a phone.
Ready now: #9 (in flight as #13) and #14 — they share no files, so they can run side by side. #10 and #11 open when #9 merges; then #12; then #15 and #16 together; #17 last, as the thing that says the map is finished.
The map is extended off #7, with #9 in flight as pull #13. Four new issues, linked; nothing committed, the clone is as I found it, and nobody is assigned.
Opened, in order
- #14 — Stripe's own path is proven: a fake Stripe in the tests walks Checkout, the return and the webhook. No blockers. The Stripe branch the direction names is the least-tested code here —
bun testcovers the mock gateway thoroughly and Stripe only atverifyWebhook, whilesrc/gateway.tsalready readsSTRIPE_API_BASE, so a fake is oneBun.serveaway. Suggested: mason. - #15 — An order that is never paid gives its stock back. Blocked by #12.
orders.place()takes stock, redeems the code and spends the card, and only an admin'scancel()gives them back: a shopper who closes the tab on the payment page keeps the last unit off the shelf forever.expires_at, anexpire()that walkscancel()'s path, a bounded sweep on anunref'd timer, and late money recorded rather than dropped. Suggested: ledger. - #16 — Every gateway delivery is a row: a duplicate, a failed payment and a refund made in Stripe all reach the ledger. Blocked by #14 and #12. The webhook believes two event types and forgets the rest, including that a delivery arrived; this adds a
gateway_eventtable keyed on Stripe's event id, understands failures andcharge.refunded, and reconciles refunds to the difference so the loop cannot run. Suggested: ledger, with sentinel reading the webhook handler. - #17 — The stranger's purchase is one test, and the README walks it in five minutes. Blocked by #11, #15, #16. The direction's own sentence as
src/journey.test.ts— guest browses, signs in, buys, the admin's stream carries it, the trail reconciles — plus a Try it in the README. Suggested: ledger, with juniper walking it on a phone.
Ready now: #9 (in flight as #13) and #14 — they share no files and can run side by side. Then #10 and #11 when #9 merges, then #12, then #15 and #16 together, and #17 last. #7 stays open.
The message here carried no words again, so I read the repository as it stands on main (8874981), walked a purchase through a local boot, and curated the map rather than repeating it. Four new issues, one parked issue split, four stale or duplicated ones marked. Nothing committed, nobody assigned.
What I checked by hand
- A guest boot:
/17 ms cold and 4 ms after, category 4 ms, product 3.5 ms, cart, checkout, mock pay, order page, receipt,/admin— all server-rendered, no script tags anywhere,/admin403 for anon and for a signed-inuser, 200 for anadmin. The shape holds. POST /checkoutwith no identity headers still places an order (#27, #10)./admin/inventoryhas nodata-liveregion and no script: the live half of the direction is genuinely unbuilt (#11).bun testis green, 67 pass across 4 files, and each file makes its own database before importing anything — #25 reads as already fixed.X-Forwarded-Foris read nowhere; buckets key on the socket — #29 reads as already fixed.
Opened
- #31 — A payment short of what the order owes does not mark it paid. One
settleFromGatewayfor the return, the webhook and the mock; the payment row written whatever arrives; short money leaves the orderpending_paymentwith an event and plain words on the page. Ready now. Suggested: ledger, with sentinel reading it. - #33 — The webhook settles only the session its own order started. The session match,
unmatchedrecorded rather than ignored, the return path held to the same rule. Waits for #31. Suggested: ledger. - #34 — An admin's change to the catalog, a discount or a tax rate is a row that names who made it. The half of the money trail that is not an order:
admin_change,/admin/changes, the same view in the terminal. Ready now. Suggested: ledger. - #35 — The pages carry classes, not style attributes, and the CSP drops
'unsafe-inline'for styles. 52 style attributes across five files are the last hole in the policy; the product tile's per-slug gradient becomes one of a fixed set of classes. Ready now. Suggested: mason, with juniper on the preview.
Parked, split
- #19 was parked because it reached a review-only agent, and it was also two changes in one body. It is now #31 and #33. Nothing is left to do in #19 — a person should close it; my token cannot.
Marked rather than closed (same reason — I cannot close issues, so each carries a comment with the evidence): #25 and #29 look already fixed by the merge of #5; #27 is the same ground as #10 and now waits for it; #30 is #11 plus #34 and now waits for both.
Ready now: #31, #34, #35, and #11, #20, #22, #24, #26, #28 — plus #10, already in hand. Nothing ready touches the same files as anything else ready except #31 and #33, which are ordered. #7 stays open.
This was a planning run: the clone is untouched and nothing is pushed — the map is the result.
Read first. main at 8874981, all of src/, and every open issue. Then I booted the app locally and shopped it: / 17 ms cold and 4 ms warm, category 4 ms, product 3.5 ms, cart → checkout → mock pay → order page → receipt, /admin 403 for anon and for a signed-in user and 200 for an admin, no script tags anywhere. POST /checkout with no identity headers still places an order; /admin/inventory has no data-live region; bun test is green (67 pass, 4 files) and X-Forwarded-For is read nowhere.
Opened, in order
- #31 — A payment short of what the order owes does not mark it paid. One
settleFromGatewayfor the return, the webhook and the mock; the payment row written whatever arrives; short money leaves the orderpending_paymentwith an event and plain words on the page. Ready. Suggested: ledger, with sentinel reading it. - #33 — The webhook settles only the session its own order started. Session match,
unmatchedrecorded rather than ignored, same rule on the return path. Waits for #31. Suggested: ledger. - #34 — An admin's change to the catalog, a discount or a tax rate is a row that names who made it. The half of the money trail that is not an order:
admin_change,/admin/changes, the same view in the terminal. Ready. Suggested: ledger. - #35 — The pages carry classes, not style attributes, and the CSP drops
'unsafe-inline'for styles. 52 style attributes across five files; the per-slug gradient becomes one of a fixed set of classes. Ready. Suggested: mason, with juniper on the preview.
Parked #19 was two changes in one body and had reached a review-only agent; it is now #31 and #33, and nothing is left in it. #25 and #29 carry comments with the evidence that the merge of #5 already fixed them; #27 now waits for #10 (same ground) and #30 waits for #11 plus #34. I could not close any of these four — my token lacks the scope, so a person should close #19, #25 and #29.
Ready now: #31, #34, #35, plus #11, #20, #22, #24, #26, #28, and #10 already in hand. #7 stays open.
The message here carried no words again, so I read main as it stands (f1ec6cb, with #6's discounts, gift cards and tax now merged), walked a purchase end to end on a local boot, and extended the map rather than repeating it. Four new issues, linked; the parked issue curated; nothing committed and nobody assigned.
What I checked by hand
bun testis green: 81 pass across 5 files.- A guest boot:
/12 ms cold and 2.5 ms warm, category 2.6 ms, cart 2.6 ms, order page 2.5 ms;/admin403 for anon and 200 for a forge-signed admin; the security floor andscript-src 'none'on every response. - A whole purchase: product → cart → checkout → the mock gateway → the order page → the receipt. It works, and it ends at "Thank you": there is no dispatch, no tracking, nothing that tells a buyer what happens next (#40).
- Every picture in the shop is a CSS gradient from
art()insrc/html.ts; there is no image table, no upload and nostatic/directory (#38). - The cart is a signed cookie, so an account's cart does not follow it between browsers (#39).
GET /orders/:numbercalls Stripe inside the render, with a fifteen-second timeout (#41).
Opened, in order
- #38 — A product has a photograph: an admin uploads it, the shop serves it by content hash, and nothing shifts.
src/images.ts, magic-byte sniffing, dimensions from the file's own header,/images/:hashimmutable for a year,art()kept as the fallback. Waits for #28 so there is one body cap and one 413 path. Suggested: ledger, with sentinel on the upload route and juniper on the preview. - #39 — A shopper's cart follows their account, not the browser.
cart_linekeyed on the buyer,ctx.cartfrom rows when signed in and from the cookie when anon, an idempotent max-quantity merge at sign-in, a cap and a sweep. Waits for #10. Suggested: ledger. - #40 — A paid order is dispatched: the buyer sees what happens next and the dispatch is a row.
fulfilled_atand oneorder_eventnaming the admin, an idempotent/admin/orders/:number/fulfil, three plain steps on the buyer's page, one more letter in the outbox, the same view in the terminal. Waits for #12 so the actor is there from the first line. Suggested: ledger, with juniper on the buyer's page. - #41 — The order page never waits on Stripe: settlement happens off the render path. No network call in the GET, the settle after the response or on a POST, bounded and one at a time, a failure as a row. Waits for #31 and #33, which rewrite the same path. Suggested: ledger.
Parked #19 is superseded by #31 (in flight as pull #37) and #33; I linked it behind both so it no longer reads as ready and said so on the issue. It wants one click to close, which my token cannot do — as do #25, whose red suite is green again and has pull #36 against it anyway, and #29, whose rate limits already key on the socket through src/peer.ts.
Ready now: #11, #28, #34, #35, #20, #22, #24, #26, #29 — plus #10, #25 and #31 already in hand as pulls #32, #36 and #37. #38 opens when #28 lands, #39 when #10 does, #40 after #12, #41 after #31 and #33. #7 stays open.
The working tree is clean at main's tip (f1ec6cb) — there is nothing to commit or push, because this was a planning run: the result is the map on the forge, not code. bun test was run against this tree earlier and is green (81 pass across 5 files).
Done. Four new issues off the direction #7, each sized for one run, each with its acceptance criteria as a checklist, the files I expect it to touch, its risks and a suggested crew member, and each linked behind what it waits for:
- #38 — A product has a photograph: an admin uploads it, the shop serves it by content hash, and nothing shifts (waits for #28) — ledger, with sentinel on the upload route and juniper on the preview.
- #39 — A shopper's cart follows their account, not the browser (waits for #10) — ledger.
- #40 — A paid order is dispatched: the buyer sees what happens next and the dispatch is a row (waits for #12) — ledger, with juniper on the buyer's page.
- #41 — The order page never waits on Stripe: settlement happens off the render path (waits for #31 and #33) — ledger.
Parked #19 was curated rather than left as it was: it is superseded by #31 (pull #37) and #33, is now linked behind both so it no longer reads as ready, and carries a comment saying so. The summary of the run is a comment on #7, which stays open.
Not done. Nothing was assigned and no code was written. Three issues want a single click I cannot make — my token lacks the scope to close an issue: #19 (superseded), #25 (its suite is green again and pull #36 covers it) and #29 (rate limits already key on the socket through src/peer.ts).
What to look at. The map at /repos/trevato/bazaar/map: ready now are #11, #28, #34, #35, #20, #22, #24, #26 and #29, with #10, #25 and #31 already in hand as pulls #32, #36 and #37. #28 is the one to take early, since #38 waits on its body cap and 413 path.
🤖 Generated with Claude Code
Budget reached at $2.51; this is where it stopped.
The message here carried no words again, so I read the repository at 5316680, ran the suite, shopped the preview as a customer, and extended the map rather than repeating it. Four new issues, linked; nothing committed and nobody assigned.
What I checked by hand
bun testis green: 85 pass across 5 files.- A local boot:
/16 ms cold and 3.3 ms warm, product 4.5 ms, cart 2.6 ms, pay page 2.5 ms, order page 5.5 ms;/admin403 for anon and for a signed-inuser, 200 for a forge-signed admin; the security floor andscript-src 'none'on every response. - A whole purchase as a guest: product → cart → checkout → the mock gateway → the order page, with tax, shipping and a receipt. It works.
- Search is where a stranger starts and it is the weakest thing here:
espressofinds two products,grinderfinds one, andespresso beansfinds none, because the needle is the whole phrase wrapped in%…%(#43). orders.place()rolls back stock, a redeemed code and a spent gift card by throwing, and not one test exercises that path (#44).- Four refusals in
src/server.ts— 400, 403 twice, 415, 500 — aretext/plainon a white page, whileshop.tooManyalready renders a proper 429 in the shop's shell (#45). /admin/ordersisorders.recent(100, q)rendered whole: no paging, no status filter, no totals, in an app whose storefront pages properly (#46).
Opened, in order
- #43 — A two-word search finds the thing: “espresso beans” returns the espresso. Terms split and AND-ed, still escaped and bound,
countSearchon the same predicate, and an empty result that offers the categories instead of "Nothing here yet.". Ready now. Suggested: mason. - #44 — The last unit goes to exactly one shopper, and the one who missed it loses nothing. The contended path proved row by row: one order, one stock movement, stock never below zero, the discount's uses and the card's balance untouched, the loser's cart and code intact and a link back to the cart. Ready now. Suggested: ledger.
- #45 — Every refusal at the front door is a page of the shop, with a way back. One
src/pages/oops.tsfor 400/403/413/415/500, the CSRF page that can be recovered from, no stack in a 500, plain text kept forrawroutes and non-HTML callers. Waits for #28, whose 413 is the fifth refusal. Suggested: mason, with juniper on the preview. - #46 — The admin's order list is paged and can be narrowed to what needs doing. One predicate shared with
recent(), the storefront's paging helper, a status filter, counts and the day's takings in integer cents, and the same in the terminal. Waits for #11, which puts adata-liveregion on the same page. Suggested: ledger.
Parked #19 is superseded by #31 (pull #37) and #33, is already linked behind both, and carries a comment saying so — there is nothing left in it to split or rewrite. It wants one click to close, which my token is refused (PATCH state → 403), as do #25 and #29.
Ready now: #43, #44, #34, #35, #20, #22, #26 — with #10, #11, #24, #28 and #31 already in hand as pulls #32, #42, #37. #45 opens when #28 lands and #46 when #11 does. #7 stays open.
This was a planning run: the clone is untouched at 5316680, so there is nothing to commit or push — the map on the forge is the result. bun test was run against this tree and is green (85 pass across 5 files).
Done. Four new issues off the direction #7, each sized for one run, each with its intent in a sentence, acceptance criteria as a checklist, the files I expect it to touch, its risks and a suggested crew member, and each linked behind what it waits for:
- #43 — A two-word search finds the thing: “espresso beans” returns the espresso. Ready now — mason. The needle in
src/catalog.tsis the whole phrase wrapped in%…%, soespressofinds two products,grinderfinds one, andespresso beansfinds none. - #44 — The last unit goes to exactly one shopper, and the one who missed it loses nothing. Ready now — ledger.
orders.place()rolls back stock, a redeemed code and a spent gift card by throwing, and no test exercises that path. - #45 — Every refusal at the front door is a page of the shop, with a way back (waits for #28) — mason, with juniper on the preview. Four refusals in
src/server.tsaretext/plainon a white page while 429 is already a proper page. - #46 — The admin's order list is paged and can be narrowed to what needs doing (waits for #11) — ledger.
/admin/ordersisorders.recent(100, q)rendered whole: no paging, no filter, no totals.
Parked #19 was checked rather than left as it was: nothing of it remains after #31 (pull #37) and #33, it is already linked behind both, and it now carries a comment saying it should be closed.
Not done. No code, no assignments, no pull opened by me. Three issues want a single click I cannot make — PATCH state answers 403 for my token: #19 (superseded), #25 (its suite is green again) and #29 (rate limits already key on the socket).
What to look at. The run summary is a comment on #7, which stays open, and the shape of the plan is /repos/trevato/bazaar/map: ready now are #43, #44, #34, #35, #20, #22 and #26, with #10, #11, #24, #28 and #31 already in hand as pulls #32, #42 and #37. #43 and #44 touch no file any other ready issue touches, so they can be taken side by side.
🤖 Generated with Claude Code
Budget reached at $2.50; this is where it stopped.
The message here carried no words again, so I read the repository as it now stands (dc3c613, with #24's mid-purchase sign-in merged), ran the suite, shopped the preview, and extended the map rather than repeating it. Four new issues, linked; nothing committed, nobody assigned.
What I checked by hand
bun testis green: 87 pass across 5 files. Four pulls are in flight — #32 (#10), #37 (#31), #47 (#11), #48 (#28) — and no issue in the map carries a "Parked by the crew" comment this run.- A guest purchase end to end on a local boot: product → cart → checkout → the mock gateway → the order page, with shipping, tax and a receipt. Fast:
/16 ms cold and 3 ms warm, product 4.5 ms, cart 2.6 ms, order 5.5 ms. - Postage is
SHIPPING_CENTS = 600andFREE_SHIPPING_OVER_CENTS = 7500insrc/cart.ts. Tax is a table an admin edits; the other money line on every order needs a deploy to change (#49). - Every page's
<head>is charset, viewport, title and the stylesheet: no description, no canonical, no OpenGraph, no/robots.txt, no/sitemap.xml— and nothing tells a crawler that a preview deployment is not the shop (#50). - No
aria-live,role="alert",aria-invalidoraria-describedbyanywhere insrc/pages/: a wrong email at checkout is a paragraph at the top of a re-rendered page and nothing says which field it belongs to (#51). orders.cancel()puts stock back, gives a code's use back and writes the rows — and the only way to reach it isPOST /admin/orders/:number/cancel. A shopper with second thoughts at the payment page can only close the tab (#52).
Opened, in order
- #49 — What postage costs is a setting an admin can change, not a constant in the code.
src/shipping.tsin the shape ofsrc/tax.ts, an admin page and a terminal key, the figure still copied onto the order at sale, and the change a row that names who made it. Waits for #34. Suggested: ledger. - #50 — A product link is worth sharing, the catalog can be crawled, and a preview is never indexed. Description, canonical and card tags from
layout(),noindexon every private path and on every non-production deployment,/robots.txtand a capped/sitemap.xml. Ready now. Suggested: mason. - #51 — Every problem on the way to buying is announced and tied to the field that caused it. One alert block,
aria-invalidandaria-describedbyon the field that was wrong, focus where the trouble is, a skip link — no JavaScript and no layout shift. Waits for #35, which rewrites the same page files. Suggested: mason, with juniper on a 360px phone. - #52 — A shopper can cancel their own unpaid order, and the stock goes back.
POST /pay/:number/cancelthrough the samecancel()an admin uses, only whilepending_payment, the reason naming the shopper, and the race with a settling payment written down. Waits for #15, which opens the same path for the sweep. Suggested: ledger.
Ready now: #50, #43, #44, #20, #22, #26, #29, #35 — with #10, #11, #28 and #31 in hand as pulls #32, #47, #48 and #37, and #34 being worked. #49 opens when #34 lands, #51 when #35 does, #52 after #15. #7 stays open.
This was a planning run: the clone is clean at dc3c613 and nothing is pushed — the map on the forge is the result. bun test is green against this tree (87 pass across 5 files).
Done. Four new issues off the direction #7, each sized for one run, each with its intent in a sentence, acceptance criteria as a checklist, the files I expect it to touch, its risks and a suggested crew member, and each linked behind what it waits for:
- #49 — What postage costs is a setting an admin can change, not a constant in the code (waits for #34) — ledger.
SHIPPING_CENTSandFREE_SHIPPING_OVER_CENTSlive insrc/cart.tswhile tax is a table an admin edits. - #50 — A product link is worth sharing, the catalog can be crawled, and a preview is never indexed. Ready now — mason. Every
<head>is charset, viewport, title and the stylesheet; there is norobots.txt, and previews are as indexable as production. - #51 — Every problem on the way to buying is announced and tied to the field that caused it (waits for #35) — mason, with juniper on a phone. No
role="alert",aria-invalidoraria-describedbyexists anywhere insrc/pages/. - #52 — A shopper can cancel their own unpaid order, and the stock goes back (waits for #15) — ledger.
orders.cancel()already writes every row; only an admin can reach it.
Not done. No code, no assignments, no pull opened by me, and nothing merged. No issue carried a "Parked by the crew" comment this run, so there was nothing to split or rewrite; #19, #25 and #29 still want the one click to close that my token is refused (PATCH state → 403).
What to look at. The run summary is a comment on #7, which stays open, and the shape of the plan is /repos/trevato/bazaar/map: ready now are #50, #43, #44, #35, #20, #22, #26 and #29, with #10, #11, #28 and #31 in hand as pulls #32, #47, #48 and #37 and #34 being worked. #50 and #43 touch no file any other ready issue touches, so they can be taken side by side.
🤖 Generated with Claude Code
The message here carried no words again. main has not moved since my last run (dc3c613), five pulls are in flight — #32 (#10), #37 (#31), #47 (#11), #48 (#28), #53 (#34) — and no issue carries a "Parked by the crew" comment, so this run is four more issues off the direction rather than any curation. Nothing committed, nobody assigned.
What I checked by hand
bun testis green: 87 pass across 5 files. A guest purchase still walks product → cart → checkout → mock gateway → order page.- I went looking for the holes I would have expected and found them already closed, which is worth writing down: the cart caps at 20 lines,
quote()drops a withdrawn line and says so,place()refuses when the price or the discount moved between the page and the button, the rate-limit buckets sweep themselves,/admin/inventoryand the dashboard already put low stock first. #54's body deadline is the real remaining one and it is filed. - What is not there: nothing keyed on a buyer beyond the order itself (#55), no way to get the money trail out as a file (#56), the front page renders
products({ limit: 60 })with no paging and no count while every other listing pages (#57), and the whole shop is one SQLite file with no copy of it and nothing that would notice if it went bad (#58).
Opened, in order
- #55 — A signed-in buyer types their address once. An
addresstable keyed on the forge id, offered back at checkout, a cap, and every read and write scoped toctx.who.id. Waits for #10. Suggested: ledger, with sentinel on the ownership checks. - #56 — The money trail leaves the shop: orders and payments as a file an accountant can open.
/admin/orders.csvon #46's filters, cells escaped against spreadsheet formula injection, cents and decimal both, streamed, and the export itself a row. Waits for #46. Suggested: ledger, with sentinel. - #57 — Every listing shows everything and can be sorted: the front page pages, and price is a choice. One paging helper for all three listings and a
sortvalidated against a fixed table, never a string nearORDER BY. Ready now. Suggested: mason. - #58 — The shop can be backed up while it runs, and the copy is proved good.
VACUUM INTOunderFORGE_APP_DATA,integrity_checkand row counts before it counts, retention, an admin page and a terminal key, and a download treated as the sensitive thing it is. Ready now. Suggested: ledger, with sentinel on the download route.
Ready now: #57, #58, #50, #43, #44, #22, #26, #29, #35, #54 — with #10, #11, #20, #28, #31 and #34 in hand as pulls. #55 opens when #10 lands and #56 when #46 does. #7 stays open.
Ten issues are ready and the tree is clean — nothing to commit or push, since the map is the result of a planning run.
This was a planning run at dc3c613: the clone is untouched, nothing is pushed, and bun test is green against this tree (87 pass across 5 files).
Done. Four new issues off the direction #7, each sized for one run, each with its intent in a sentence, acceptance criteria as a checklist, the files I expect it to touch, its risks and a suggested crew member, linked behind what they wait for:
- #55 — A signed-in buyer types their address once (waits for #10) — ledger, with sentinel on the ownership checks.
- #56 — The money trail leaves the shop: orders and payments as a file an accountant can open (waits for #46) — ledger, with sentinel on the escaping and the guard.
- #57 — Every listing shows everything and can be sorted: the front page pages, and price is a choice. Ready now — mason.
shop.homerendersproducts({ limit: 60 })with no count and no paging, while every other listing keeps the README's promise. - #58 — The shop can be backed up while it runs, and the copy is proved good. Ready now — ledger, with sentinel on the download route.
Not done. No code, no assignments, no pull opened by me. main has not moved since my last run, five pulls are in flight (#32, #37, #47, #48, #53), and no issue carried a "Parked by the crew" comment, so there was nothing to split, rewrite or retire. #19, #25 and #29 still want the single click to close that my token is refused (PATCH state → 403).
What to look at. The run summary is a comment on #7, which stays open, and the shape of the plan is /repos/trevato/bazaar/map: ready now are #57, #58, #50, #43, #44, #22, #26, #29, #35 and #54. Worth noting in the summary is what I went looking for and did not find: the cart cap, the withdrawn-line message, the price-moved refusal in place(), the rate-limit sweep and the low-stock ordering are all already there, so this run's issues are the gaps that remain rather than repeats.
🤖 Generated with Claude Code
The message here carried no words again, so this run is four more issues off the direction, grounded in what the app does now. main moved to f3e7e8a since my last run (#36, #42, #37, #48 merged), no open issue carries a "Parked by the crew" comment, and nothing needed splitting or retiring. Nothing is assigned; the branch is a fast-forward to main and carries no changes.
What I did by hand. Ran the shop at FORGE_APP_DATA=/tmp/probe and walked a guest purchase: home, product, add to cart (the header reads "Cart · 1"), /checkout, POST /checkout → 303 /pay/BZ-…?token=…, with the same idempotency token replayed and getting the same order back. Every page answered in 2–4 ms and 1.5–5 KB, the security floor was on all of them, and bun test is green (95 pass across 6 files). Then I went looking for what a demanding customer and a demanding admin would still hit.
Opened, in order
- #60 — A refund from the terminal moves the money: a Stripe order is refunded at Stripe, or not at all. Ready now.
src/admin.ts:263-289asks the gateway first and writes the ledger second;refundRest()insrc/tty.ts:242-256skips the gateway entirely and writes atty_refund_…reference for every order, Stripe's included — the books say refunded and the customer is not. One gateway-aware refund both callers use, a refusal that writes nothing, and the fake Stripe from #14 to prove it. Suggested: ledger, with sentinel on the failure branch. - #61 — The books balance: every running total is proved against the ledger under it. Ready now.
src/db.tspromisesvariant.stock,gift_card.balance_centsanddiscount.used_countare the sums of their rows, and nothing in the app ever checks.src/reconcile.tsas three or fourGROUP BYs, a line on/admin, the same answer in the terminal. Suggested: ledger. - #62 — An admin can tell the money path is wired: which gateway, whether the webhook is heard, and what is stuck unpaid. Waits for #61, because both grow the dashboard.
chosen()already knows why the mock is in use and only the checkout button ever says so;STRIPE_WEBHOOK_SECRETis read at delivery time and nowhere else, so a key without a webhook secret looks healthy until money goes missing. The test that greps the page for a secret is the point of it. Suggested: mason, with sentinel. - #63 — Every date in the shop says when it happened, the same way, and a machine can read it too. Waits for #35, which rewrites the same markup.
datetime('now')is stored without aZ,src/pages/checkout.tsandsrc/pages/find.tsappend " UTC" by hand,src/pages/admin.tsdoes not, and there is no<time>element in the app.src/when.ts, server-rendered words, no JavaScript. Suggested: mason, with juniper on a phone.
Ready now: #60, #61, #33, #35, #22, #26, #29, #38, #43, #45, #50, #54, #57, #58 — with #10, #11, #20 and #34 in hand as pulls #32, #47, #59 and #53. #60 and #61 touch different files and can be taken side by side; #62 opens when #61 lands and #63 when #35 does. #7 stays open.
This was a planning run: the map on the forge is the result, and the branch carries no changes of its own — it is a fast-forward to main at f3e7e8a, where bun test is green (95 pass across 6 files).
Four new issues off the direction #7, each sized for one run, each with its intent in a sentence, acceptance criteria as a checklist, the files it should touch, its risks and a suggested crew member, and each linked behind what it waits for:
- #60 — A refund from the terminal moves the money: a Stripe order is refunded at Stripe, or not at all. Ready now — ledger, with sentinel on the failure branch.
src/admin.ts:263-289asks the gateway before it writes the ledger;refundRest()insrc/tty.ts:242-256skips the gateway for every order and invents atty_refund_…reference, so the books say refunded and the customer is not. - #61 — The books balance: every running total is proved against the ledger under it. Ready now — ledger.
src/db.tspromises stock, gift-card balances and discount use counts are the sums of their rows; nothing checks. - #62 — An admin can tell the money path is wired: which gateway, whether the webhook is heard, and what is stuck unpaid (waits for #61, which grows the same dashboard) — mason, with sentinel on what the panel exposes.
- #63 — Every date in the shop says when it happened, the same way, and a machine can read it too (waits for #35, which rewrites the same markup) — mason, with juniper on a phone.
I walked a guest purchase against a local run first — home, product, cart, checkout, the idempotency token replayed and returning the same order — and every page answered in 2–4 ms with the security floor on it; the four issues are the gaps that walk left behind. No code, no assignments, no pull opened by me, and nothing merged; no issue carried a "Parked by the crew" comment, so there was nothing to split or rewrite. Ready now are #60, #61, #33, #35, #22, #26, #29, #38, #43, #45, #50, #54, #57 and #58; the shape of the plan is /repos/trevato/bazaar/map and the run summary is a comment on #7, which stays open.
🤖 Generated with Claude Code
Planning run at f3e7e8a. bun test is green (95 pass across 6 files); I booted the shop at FORGE_APP_DATA=/tmp/probe and bought a bag of espresso end to end — home, product, cart, checkout, the mock gateway, the order page, the receipt, and /admin as a signed admin — every page 3-16 ms with the security floor on it. No open issue carries a "Parked by the crew" comment except #19, which is handled below.
What the walk turned up. At /checkout I typed country usa and region California. The order, the pay page, the receipt and the outbox all read Tax 8.50% and the address line usa, while the seeded table has UNITED STATES / CA / 875 / California sales tax: the shop billed $2.72 where $2.80 was owed and labelled it nothing. UK misses UNITED KINGDOM the same way and is charged 8.5% instead of 20% VAT. The cart's estimate is right because it uses tax.home(); only the bill is wrong, which is the one thing HOME_COUNTRY exists to prevent. That is #66. Then: a paid order has no way for its buyer to ask for their money back (#67); the shop makes no promises anywhere a stranger can read before typing an address (#68); and five admin lists stop at a cap nobody is told about, one of them — discounts.all() — having no cap at all (#69).
Opened, in order
- #66 — Country is chosen from a list and a region is understood, so the tax charged is the tax the shopper was shown.
src/places.tsas plain data, a country<select>, a region resolved through the country's own aliases with no client-side JavaScript, the canonical name and code on the order,/admin/taxandsrc/desk.tsoffering the same list. Waits for #64, which signs the same form's figures. Suggested: ledger, with sentinel on the boundary. - #67 — A buyer asks for a refund from their order page, and an admin answers it. A
refund_requestrow, one open per order enforced in SQL, the order's token in the form like the mock gateway's, a capped and escaped reason on its own rate-limit bucket, and the admin's existing refund closing the request in the transaction it already has — never a second refund path. Ready now. Suggested: ledger, with sentinel. - #68 — The shop's promises are pages: delivery and returns, privacy, terms. Three server-rendered pages whose every sentence is true of this code, the four cookies named, links in the footer and at the till, and a test that reads the privacy page for every cookie the app can set. Ready now. Suggested: mason, with juniper on a phone.
- #69 — Every admin list says how many there are and pages past its silent cap. Products at 500, gift cards at 200, the outbox at 50 whole receipts, the stock ledger at 50 and the discounts uncapped, all on #46's pager, with counts, a validated
?page=and the terminal kept in step. Waits for #46. Suggested: mason.
Parked #19 was checked rather than left as it was: #31 is merged, #33 carries the rest, and it now says so again and asks for the click to close that my token is refused.
Ready now: #67, #68, #60, #61, #33, #35, #20, #22, #26, #29, #38, #43, #45, #50, #54, #57, #58 — with #10, #11, #20 and #44 in hand as pulls #32, #47, #59 and #65. #66 opens when #64 lands and #69 when #46 does. #7 stays open.
This was a planning run at f3e7e8a: the branch carries no changes — the map on the forge is the result — and bun test is green against this tree (95 pass across 6 files). I booted the shop locally and bought a bag of espresso end to end (home → product → cart → checkout → mock gateway → order → receipt, plus /admin as a signed admin), every page answering in 3–16 ms with the security floor on it, and filed what the walk turned up.
Four new issues off the direction #7, each sized for one run, each with its intent in a sentence, acceptance criteria as a checklist, the files it should touch, its risks and a suggested crew member:
- #66 — Country is chosen from a list and a region is understood, so the tax charged is the tax the shopper was shown (waits for #64, which signs the same form's figures) — ledger, with sentinel on the boundary. Typing
usa/Californiaat checkout billedTax 8.50%where the seeded table saysCalifornia sales taxat 8.75%;UKmissesUNITED KINGDOMand pays 8.5% instead of 20% VAT. The cart's estimate is right and only the bill is wrong. - #67 — A buyer asks for a refund from their order page, and an admin answers it. Ready now — ledger, with sentinel. A
refund_requestrow, one open per order enforced in SQL, the order's token in the form like the mock gateway's, and the admin's existing refund closing the request inside the transaction it already has. - #68 — The shop's promises are pages: delivery and returns, privacy, terms. Ready now — mason, with juniper on a phone. Three server-rendered pages whose sentences are true of this code, the four cookies named, and a test that turns red if a cookie is added without saying so.
- #69 — Every admin list says how many there are and pages past its silent cap (waits for #46, which is where the admin's pager is born) — mason. Products at 500, gift cards at 200, fifty whole receipts in the outbox, the stock ledger at 50, and
discounts.all()with no cap at all.
Parked #19 was checked rather than left as it was: #31 is merged and #33 carries the rest, so it now says so again and asks for the click to close that my token is refused (PATCH state → 403). No code, no assignments, no pull opened by me, nothing merged. Ready now are #67, #68, #60, #61, #33, #35, #20, #22, #26, #29, #38, #43, #45, #50, #54, #57 and #58, with #10, #11, #20 and #44 in hand as pulls #32, #47, #59 and #65; the shape of the plan is /repos/trevato/bazaar/map and the run summary is a comment on #7, which stays open.
🤖 Generated with Claude Code
Planning run at 70fc345. bun test is green (120 pass, 7 files). I bought espresso to a UK address and a $25 gift card end to end on a local boot; every page answered in 1-19 ms with the security floor on it. Opened, in order: #71 — a gift-card-only cart is asked for a postal address and then the order, the pay page and the receipt all say "Shipping to" for a code that is emailed (ready now, ledger); #72 — every total is shown in $ while the row says USD and Stripe is charged in it, so name the currency where a stranger reads it (waits for #71, which touches the same totals blocks, mason); #73 — categories are seeded in code and no route manages them, so an admin can add a product but not a shelf to put it on (ready now, mason with sentinel). A fourth, tests for src/tty.ts, was scoped but not filed: the run ended first. No issue carries a "Parked by the crew" comment. Nothing assigned, no code, #7 stays open.
A planning run against direction #7: the branch carries no code changes — it is fast-forwarded to main at 70fc345, where bun test is green (120 pass across 7 files) — and the map on the forge is the result. I read the tree after the live-events and admin-changes merges, then booted the shop at FORGE_APP_DATA=/tmp/probe with a signed forge identity and bought two things end to end: a bag of espresso to a UK address (VAT 20% charged and labelled correctly once the country is typed in full) and a $25 gift card whose code arrived on the receipt. Home, product, cart, checkout, the mock gateway, the order page, the receipt, /orders, /admin, the outbox, the changes log and the /admin/events stream all answered in 1–19 ms with the security floor and the CSP on every response.
Three issues opened, each sized for one run, each with its intent, acceptance criteria as a checklist, the files it should touch, its risks and a suggested crew member:
- #71 — A gift card is not posted anywhere: a cart of only gift cards asks for no address. Ready now, ledger. Checkout demands an address line, city, postcode and country for a code that is emailed, then the order, pay page and receipt print "Shipping to 1 Road, Boston, MA 02101" and a
$0.00line labelled "US sales tax 8.50%". - #72 — Every total says which money it is. Waits for #71, which edits the same totals blocks. mason. A UK buyer is charged 20% VAT and shown
$25.20, while the order row saysUSDand Stripe is charged in it. - #73 — A section of the shop is the admin's to make: categories are managed, not seeded in code. Ready now, mason with sentinel. There is no route in
src/routes.tsthat touchescategory; an admin can add a product but not a shelf for it.
What is not done: a fourth issue — tests for src/tty.ts, which is 334 lines of role-gated screens that no test imports because the module boots on import — was scoped but not filed; the rules it enforces live in src/desk.ts and are covered, so this is a gap in the terminal's own key handling, not in its gates. No issue carries a "Parked by the crew" comment, so nothing needed splitting or rewriting. Nobody is assigned and no pull was opened by me.
Look at #71 first — it is the only one a shopper feels — and at /repos/trevato/bazaar/map for the shape of the plan; ready now are #71, #73, #67, #68, #60, #61, #35, #46, #22, #26, #29, #30, #38, #43, #45, #49, #50, #54, #57 and #58, with #10, #20, #33 and #44 in hand as pulls #32, #59, #70 and #65. The run summary is a comment on #7, which stays open.
🤖 Generated with Claude Code
Budget reached at $2.52; this is where it stopped.
Planning run at 9c72f19 (the branch was behind main; #32 had landed, so the tree was re-read after fast-forwarding). bun test is green — 120 pass, 7 files — and I walked a purchase end to end on a local boot: home, product, cart, checkout, the mock gateway, the order page, the receipt, /orders and every /admin page, each answering in 1-19 ms with the security floor and the CSP on it.
Opened, in order
- #74 — The terminal is proved by tests:
src/tty.tshas none, and it is 348 lines of role-gated screens. No test imports it, because it boots on import; the seam —run()behindimport.meta.main, key handling as a function of (who, tab, key) — is the work, andsrc/tty.test.tsis the result. Ready now. Suggested: mason, with sentinel reading the pull. - #75 — A payment form with no decision is a form error, not a "declined" row on the buyer's order. In
shop.payMockeverydecisionthat is notapprovewritesdeclinedand answers 402; I hit it by posting the pay form without the button's value, and the order's history — and the admin's order page — carry a decline the shopper never chose. Ready now. Suggested: ledger. - #76 — A signed-in shopper reaches their orders from the header, not only the footer.
src/html.tslinks "Your orders" once, below the fold; since #10 an account's orders open without a token, so the page the account is for is the hardest to reach. Ready now. Suggested: mason, with juniper on a phone.
A fourth was not filed — the run's budget ended. No open issue carries a "Parked by the crew" comment, so nothing needed splitting or rewriting. Nobody is assigned, no code was pushed, and #7 stays open.
One thing for a person: the map treats #27 ("checkout requires a signed-in customer") and #30 ("admin sees stock and orders change live, and every admin action leaves a trail") as ready, but #32, #47 and #53 have landed the behaviour they ask for; my token cannot close an issue (PATCH state → 403), so they want a click.
A planning run against direction #7: no code changed — the branch is fast-forwarded to main at 9c72f19 and the map on the forge is the result. bun test is green on this tree (120 pass, 7 files), and I bought a bag of espresso end to end on a local boot — home, product, cart, checkout, the mock gateway, the order page, the receipt, /orders and every /admin page, each answering in 1–19 ms with the security floor and the CSP on it. The tree was re-read after fast-forwarding, because #32 (an order belongs to an account) had landed and my clone was behind it.
Three issues opened, each sized for one run, each with intent, acceptance criteria as a checklist, the files it should touch, its risks and a suggested crew member:
- #74 — The terminal is proved by tests:
src/tty.tshas none, and it is 348 lines of role-gated screens. No test imports it because it boots on import; the seam is the work. Ready now — mason, with sentinel. - #75 — A payment form with no decision is a form error, not a "declined" row on the buyer's order. In
shop.payMockanydecisionthat is notapprovewrites a decline and answers 402; posting the pay form without the button's value put a decline the shopper never chose on the order's history and on the admin's page. Ready now — ledger. - #76 — A signed-in shopper reaches their orders from the header, not only the footer. Since #10, an account's orders open without a token, and
src/html.tslinks them once, below the fold. Ready now — mason, with juniper on a phone.
A fourth was scoped but not filed: the run's budget ended first. No open issue carries a "Parked by the crew" comment, so nothing needed splitting or rewriting; nobody is assigned and #7 stays open. Worth a person's click: the map still lists #27 and #30 as ready, but #32, #47 and #53 have landed what they ask for, and my token cannot close an issue (PATCH state → 403). Look at /repos/trevato/bazaar/map for the shape of the plan and at the run summary on #7; ready now are #74, #75, #76, #67, #68, #71, #73, #61, #35, #46, #22, #26, #29, #38, #39, #43, #45, #49, #50, #54, #55, #57, #58 and #60.
🤖 Generated with Claude Code
Budget reached at $2.55; this is where it stopped.
You've hit your weekly limit · resets Sep 20, 2am (UTC)
You've hit your weekly limit · resets Sep 20, 2am (UTC)
You've hit your weekly limit · resets Sep 20, 2am (UTC)
You've hit your weekly limit · resets Sep 20, 2am (UTC)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Planning run at bb98ad6, after #77 landed. bun test is green here — 131 pass across 7 files — and the branch carries no code: the map is the result.
Ten issues were parked, and every one is handled. Nine of the ten parks are the crew's own infrastructure rather than the work — a weekly limit reached (#15, #16, #26, #27) and the API unreachable, EAI_AGAIN (#22, #29, #35, #46, #61, #71) — so no branch was pushed and nothing was learned that changes what done looks like. Each now carries a comment saying so: it stands as written and is ready now, with a note to re-read the tree first, because #32, #47, #53 and #77 have landed since those issues were written and the files they name have moved. Two needed more than that:
- #22 was declined, not attempted: the run that took it was a reviewer's, and the issue asks for an implementation. That was the right refusal; the issue is unchanged and wants a builder — mason or ledger.
- #27 is done: #32 landed the rule it asks for —
shop.placeOrderrefuses an anonymous shopper before it writes anything (src/shop.ts:286), the checkout page offers the forge's sign-in rather than a form of our own, browsing and the cart stay open to anybody. It wants the click to close, which my token is refused (PATCH state→ 403).
Two more want a person's click for the same reason: #30 (live admin events and an admin trail) is carried by #47 and #53, and #12's audit page landed as #77.
No new issues this run — the map is far from dry with #74, #75 and #76 from the last run still untouched and seventeen more ready — and nobody is assigned. #7 stays open.
A planning run against direction #7: no code changed — the branch is fast-forwarded to main at bb98ad6, where bun test is green (131 pass across 7 files) — and the map on the forge is the result.
Ten issues had been parked since the last run, and all ten are handled. Nine of the parks are the crew's own infrastructure rather than the work — a weekly limit reached (#15, #16, #26, #27) and the API unreachable, EAI_AGAIN (#22, #29, #35, #46, #61, #71): no branch was pushed and nothing was learned that changes what done looks like, so each now carries a comment saying it stands as written and is ready now, with a note to re-read the tree first because #32, #47, #53 and #77 have landed since those issues were written. Two needed more: #22 was declined rather than attempted — the run that took it was a reviewer's and the issue asks for an implementation, which was the right refusal — so it is unchanged and wants a builder, mason or ledger; #27 is done, since #32 landed the rule it asks for (shop.placeOrder refuses an anonymous shopper before it writes anything, src/shop.ts:286).
What a person should look at: #27, #30 and #12 all want the click to close — the behaviour they ask for landed in #32, #47/#53 and #77 — and my token is refused on PATCH state (403). Ready now, beyond the ten un-parked, are #74, #75 and #76 from the previous run plus #38, #40, #43, #45, #49, #50, #54, #55, #57, #58, #60, #67, #68 and #73. No new issues were opened this run and nobody is assigned; #7 stays open. Earlier in this session I left two stray probe comments on #15 while testing the comment endpoint after a 403 — the real comment on that issue says so.
🤖 Generated with Claude Code
Planning run at bb98ad6. bun test is green (131 pass, 7 files) and I walked the shop on a local boot with a signed identity — home, product, cart, checkout, the mock gateway, the order page, the receipt, /orders, /admin, /admin/audit and the new /admin/orders/<number> — every page answering in 1–10 ms with the security floor on it. The map came into this run with no parked issues and 26 ready, so this is an increment rather than a refill.
What the walk turned up. #77's audit page is properly cursor-paged and its balance line adds up, and the seeds are all guarded by counts, so a deploy does not overwrite an admin's catalog. What no test touches is the other half of a deploy: migrate() in src/db.ts only ever runs against data it created, because every test starts from an empty freshData directory and a preview's directory is empty too. That is #80, and it is the one path where a mistake costs the shop's real rows. The other two are things a person feels: a keyboard loses the cursor the moment it lands on a link (src/style.ts gives :focus-visible to form controls only) and there is no skip link past the header — #81; and an operator watching the log sees nothing but the boot line and a thrown error, so a shop refusing every POST looks like a quiet afternoon — #82.
Opened, in order
- #80 — A database from the last release opens after a deploy: the migration ladder is proved against an old file with rows in it. Ready now. Suggested: ledger, with sentinel.
- #81 — A keyboard gets past the header: every page has a skip link and every link shows focus. Ready now. Suggested: mason, with juniper on a phone and on the keyboard. It touches the same two files as #35, so whoever takes both should take #35 first.
- #82 — Every refusal and every failure leaves one line in the log, and nothing a customer typed. Ready now. Suggested: mason, with sentinel — a log is where secrets leak.
A fourth was looked for and not filed: I checked whether seedGiftCards() could recreate the gift-card product on a boot after an admin had emptied it (a unique-slug crash at import), and no admin route can delete a variant, so the crash is not reachable from the web. Nothing was assigned, no code was pushed, no issue carries a "Parked by the crew" comment, and #7 stays open. #27, #30 and #12 still want a person's click to close: the behaviour they ask for is in main, and #78 is the last gap of #30.
A planning run against direction #7: no code changed — the branch sits at main's tip bb98ad6, where bun test is green (131 pass across 7 files) — and the map on the forge is the result. I walked the shop on a local boot with a signed identity: home, product, cart, checkout, the mock gateway, the order page, the receipt, /orders, /admin, /admin/audit and /admin/orders/<number>, each answering in 1–10 ms with the security floor on it.
Three issues opened, each sized for one run, each with its intent, acceptance criteria as a checklist, the files it should touch, its risks and a suggested crew member:
- #80 — A database from the last release opens after a deploy: the migration ladder is proved against an old file with rows in it.
migrate()insrc/db.tshas grown to fourteen columns, five indexes and two backfill defaults, and it only ever runs against data it created: every test starts from an emptyfreshDatadirectory, and a preview's directory is empty too, so production is the only place the upgrade path runs. Ready now — ledger, with sentinel. - #81 — A keyboard gets past the header: every page has a skip link and every link shows focus.
src/style.tsgives:focus-visibleto form controls only, so a person tabbing loses the cursor on every link, and there is no skip link past the header's wordmark, five categories and search box. Ready now — mason, with juniper. - #82 — Every refusal and every failure leaves one line in the log, and nothing a customer typed. A 403, 413, 415, 429 or 404 leaves no trace at all today; the line wants the route pattern, never the path, and no query string. Ready now — mason, with sentinel.
What I checked and did not file: #77's audit page is correctly cursor-paged and its balance line adds up, the three seeds are all guarded by counts so a deploy cannot overwrite an admin's catalog, and the gift-card re-seed cannot crash a boot because no admin route deletes a variant. The ten issues parked earlier were cleared in the previous run and none are parked now. Still wanting a person's click to close: #27, #30 and #12, whose behaviour is already in main (#78 is the last gap of #30) — my token is refused on PATCH state. Nobody is assigned and #7 stays open; the shape of the plan is /repos/trevato/bazaar/map.
🤖 Generated with Claude Code
Planning run at bb98ad6 — main has not moved since the last one, no issue is parked, and 28 were already ready, so this run went looking for what a shopper and an operator still hit rather than refilling the map. I booted the shop and bought a $25 gift card with a discount code on it, to a London address, through the mock gateway: cart, checkout, pay, order, receipt, all in single-digit milliseconds.
What the walk turned up. Typing WELCOME10 on a cart holding only a gift card is accepted — ?m=applied, "Remove WELCOME10" under the totals — and takes nothing off: subtotal $25.00, total $25.00, no discount line, no sentence. The rule underneath is right (a gift card is money moving, so goods_cents is 0 and ten percent of nothing is nothing); what is wrong is that the shopper is told they have a discount and then charged full price. That is #84. Reading the other half of the money path: verifyWebhook refuses an empty secret before it looks at anything, so a shop with a test key and no STRIPE_WEBHOOK_SECRET answers 400 to every delivery forever and settles only when a shopper comes back from the hosted page — and nothing says so, because chosen().note is drawn on the pay page for the shopper and /admin never names the gateway at all. That is #85.
Opened, in order
- #84 — A code that takes nothing off this cart is refused with a reason, not applied in silence. Ready now. Suggested: ledger, with juniper on the wording.
- #85 — The shop says whether the money path is wired: Stripe with no webhook secret is named, and the admin sees which gateway takes the money. Ready now. Suggested: ledger, with sentinel. #62 now waits for it — #62 asks the larger question (has a delivery ever been heard, what is stuck unpaid) and should absorb this rather than fight it.
Also checked and not filed: #77's audit page is cursor-paged and balances; the three seeds are count-guarded, so a deploy cannot overwrite an admin's catalog; the gift-card order still prints a shipping address and a $0.00 VAT line, which is #71 and #72 already. Nothing assigned, no code pushed, #7 stays open. Still wanting a person's click: #27, #30 and #12, whose behaviour is in main.
A planning run against direction #7: no code changed — the branch sits at main's tip bb98ad6 — and the map on the forge is the result. main has not moved since the last run and nothing is parked, so instead of refilling the map I bought a $25 gift card with a discount code on it, to a London address, through the mock gateway, and read the money path's configuration.
Two issues opened, both ready now, each with intent, acceptance criteria as a checklist, the files it should touch, its risks and a suggested crew member:
- #84 — A code that takes nothing off this cart is refused with a reason, not applied in silence. Typing
WELCOME10on a cart holding only a gift card is accepted (?m=applied, "Remove WELCOME10" under the totals) and takes nothing off: subtotal $25.00, total $25.00, no discount line and no sentence. The rule underneath is right — a gift card is money moving, sogoods_centsis 0 — but the shopper is told they have a discount and then charged full price. Suggested: ledger, with juniper on the wording. - #85 — The shop says whether the money path is wired: Stripe with no webhook secret is named, and the admin sees which gateway takes the money.
verifyWebhookrefuses an empty secret before it looks at anything else, so a shop with a test key and noSTRIPE_WEBHOOK_SECRETanswers400to every delivery forever and settles only when a shopper comes back from the hosted page — and nothing says so, becausechosen().noteis drawn on the pay page for the shopper and/adminnever names the gateway. Suggested: ledger, with sentinel. #62 now waits for it, since #62 asks the larger question and should absorb this.
Checked and deliberately not filed: #77's audit page is cursor-paged and its balance line adds up; the three seeds are count-guarded, so a deploy cannot overwrite an admin's catalog; the gift-card order's shipping address and $0.00 VAT line are #71 and #72 already. Nobody is assigned and #7 stays open. #27, #30 and #12 still want a person's click to close — their behaviour is in main and my token is refused on PATCH state.
🤖 Generated with Claude Code
Planning run at 03ab5b0, after #87 landed the body deadline. bun test is green here — 136 pass across 7 files — and I booted the shop to try the edges rather than the happy path: a trickled POST is refused with 408 and an ordinary one is untouched, a tampered cart cookie yields an empty cart, /c/nope and an order number that does not exist are both the shop's own 404 with no leak, and “<script>” comes back as text. The map came in with 31 ready and nothing parked, so this run added one issue and sharpened two rather than filling it further.
Opened
- #90 — How big a body may be, and how long it may take, is a property of the route.
BODY_MAXis 64 KB andBODY_MSis 5 s for everything, which is right for every form this shop has and impossible for the first route that carries a file. #38 (a product has a photograph) now waits for it — a photograph is 100 KB to 5 MB, so under today's numbers every upload is a413, and a slow 2 MB upload would be a408even with the cap raised.readFormalready takesmaxandms; what is missing is a way for the route table to say so and a ceiling that a typo cannot pass. Ready now — mason, with sentinel.
Sharpened rather than duplicated
- #43 now carries the empty search:
/search?q=zzzznothingsays0 matches. Nothing here yet.and then stops — no categories, no way back into the catalog — with three lines added to its checklist.
Nothing else was filed: what a walk turns up now is mostly already on the map (#71, #72, #84, #85 from the last two runs), and #86 from mason is the real fragility in the suite — the audit paging test reads rows other tests left behind and can fail under --randomize. Nobody is assigned, no code was pushed, no issue is parked, and #7 stays open. #27, #30 and #12 still want a person's click: their behaviour is in main.
A planning run against direction #7 at 03ab5b0, after #87 landed the body deadline: no code changed — the branch is fast-forwarded to main, where bun test is green (136 pass across 7 files) — and the map on the forge is the result. This run went at the edges rather than the happy path: a trickled POST is refused with 408 and an ordinary one is untouched, a tampered cart cookie yields an empty cart, /c/nope and a non-existent order number are both the shop's own 404 with nothing leaked, and “<script>” comes back as text.
- #90 — How big a body may be, and how long it may take, is a property of the route.
BODY_MAXis 64 KB andBODY_MSis 5 s for everything, which is right for every form this shop has and impossible for the first route that carries a file. #38 (a product has a photograph) now waits on it: a photograph is 100 KB to 5 MB, so today every upload would be a413, and a slow 2 MB upload a408even with the cap raised.readFormalready takesmaxandms— what is missing is a way for the route table to say so, and a ceiling a typo cannot pass. Ready now, suggested mason with sentinel. - #43 was sharpened instead of duplicated:
/search?q=zzzznothingrenders0 matches. Nothing here yet.and then stops — no categories, no way back into the catalog — so three lines went onto its checklist.
Nothing else was filed: what a walk turns up now is largely already on the map (#71, #72, #84, #85 from the last two runs), and #86 from mason names the real fragility in the suite — the audit paging test reads rows other tests left behind and can fail under --randomize. Nobody is assigned, no issue is parked, and #7 stays open. #27, #30 and #12 still want a person's click to close, since their behaviour is in main and my token is refused on PATCH state.
🤖 Generated with Claude Code
Sell one real thing end to end. A visitor browses the catalog as a guest, keeps a cart, and buys with an account. Identity comes from the forge on every request (x-forge-identity and x-forge-signature, verified with FORGE_APP_IDENTITY_KEY; anon, user, admin; the forge's new-app template has src/who.ts showing the exact check) — never a sign-in of our own; Sign in is a link to /__forge/signin?to=<path>. Payment through Stripe in test mode, with STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET from the app's environment; when they are absent, a clearly labelled simulated checkout that walks the same order path. An admin sees stock and orders change live as they happen (server-sent events over one in-process bus). Every order, payment event and stock change is a row we can audit. Server-rendered, fast on a phone, no framework, no new dependencies without a reason in the pull. Done looks like: a stranger can buy the one thing, the admin sees it appear, and the money trail is complete.
This issue carries the repository's direction. The planner is given it when the map has run dry; it opens and links the issues that follow from it, and this one stays open.