Public: anyone can read this; the crew can change it.
Launching an address you already launched asks first, and links to the listing you have #74
Check · Review · Land · Live done — Landed and live in production.
Launching an address you already launched now asks first. The form comes back with a link to the listing you have, and the JSON API and MCP tool answer 409. The full suite passes except `deps.test.ts` ("no lockfile or node_modules: nothing is installed"), which fails only because `bun install` created `node_modules` and a lockfile here. Nothing in it is related to this change. I did not run `tsc`…
shipped production is serving it deploy 957b843
Launching an address you already launched now asks first. The form comes back with a link to the listing you have, and the JSON API and MCP tool answer 409. The full suite passes except deps.test.ts ("no lockfile or node_modules: nothing is installed"), which fails only because bun install created node_modules and a lockfile here. Nothing in it is related to this change. I did not run tsc because the repo has no tsconfig.json, and there is no scripts/gate.ts or scripts/fast.ts to run.
What changed
- New module:
src/duplicate.tsholdsearlierAt(db, who, address). It matches case- and whitespace-insensitively and reads only rows the person authored, so another person's listing is never matched or named. - Launch action:
launchListingtakesagain. It checks for a duplicate after validation and before any photo is read or written. If it finds one it returns a newduplicate_addressfailure carrying the existing listing. That failure maps to HTTP 409 and the API returns{error, message, id}. - Page:
POST /listingsre-renders/with every typed value kept and escaped. The notice reads "You launched 412 Alder Lane on Mar 3 — open it, or launch another anyway". "Open it" links to/listings/<id>. The button resubmits the form withagain=1. - Files and the sample: a refused launch writes no photo, so the notice tells the agent to choose photos again.
?from=("Start a new one from this") is a GET and never shows the notice. - MCP: the
launch_listingtool acceptsagain, and its error text carries the existing id. - Docs: the OpenAPI summary,
contract/openapi.json(regenerated) and the README agent section describe all of this. - Tests:
src/duplicate.test.tscovers the notice for the author, none for another user,againlaunching, the API's 409 and itsagain: trueoverride, a hostile address escaped in the notice, no photo files written when refused, and the sample prefill. The speed test still passes.
Existing tests
Twelve existing tests launch the same address repeatedly as one user. That is now a duplicate, so I added again to their launch helpers in listings, prefill, signin and contract tests. The assertions are unchanged.
What I chose not to do
- Terminal:
src/tty.tshas no launch command, so there is nothing for it to ask and I did not add one. - Contract:
contract.jsonand the permission cells are untouched. - MCP test: the MCP path has no dedicated test. It shares the action, which the API test covers.
To try it
Launch an address, then launch it again (any case or spacing) on /. You should see the notice. Click "launch another anyway" to launch a second one.
Two things to fix. (1) Pressing Enter in any text field of the re-rendered form submits with again=1 and so skips the check. (2) The tests leave behaviours uncovered. Security and the contract are otherwise fine: the lookup only reads the author's own rows, output is escaped, no permit cell changed, and the terminal has no launch command.
Fix first: in the re-rendered form the 'launch another anyway' button comes before the real Launch button in tree order. Browsers use the first submit button for implicit submission, so Enter in a field silently resubmits with again=1. Move the notice out of the form's submit order, for example render it above the form and carry again as a separate form or a button with formaction/form attributes placed after the Launch button. Add a test that the first submit button in the returned form has no name=again.
Tests: (a) the MCP launch_listing path (again and the 'id:' text in the error) has no test, and the changed error text is untested. (b) Box 4 is only checked for ?from=1 with no notice; add a test that launching from a sample with a changed address launches with no notice. (c) The speed test is not touched, so confirm it still passes.
Pressing Enter in a field of the re-rendered form can no longer skip the duplicate check, and the tests now cover the MCP path and the sample start. The full suite passes except deps.test.ts ("no lockfile or node_modules: nothing is installed"), which fails only because bun install created node_modules here. The design lint is clean. I did not run tsc because the repo has no tsconfig.json.
Enter no longer skips the check
- The notice is now text plus the "open it" link, with no button inside it.
- "Launch another anyway" (
name=again value=1) is a separate button placed after the real Launch button. Enter therefore submits with the first submit button, which carries noagain. - The notice wording changed from a "launch another anyway" button to "launch another anyway with the button after Launch". The "launch another anyway" button now sits beside Launch. The issue's text had the button inside the sentence, and I did not keep that.
- A test checks that the first
<button>in the returned form has noagain.
Tests added in src/duplicate.test.ts
- MCP:
launch_listingon a duplicate address returns an error containingduplicate_addressandid: <existing id>. Withagain: trueit launches. - Box 4: the
?from=1page shows the sample address, and posting a changed address launches with a 303 and no notice. I also post the sample's own address unchanged as a different user, and that launches without a notice too. The sample belongs to another author, so it never counts as the user's duplicate. - Speed:
speed.test.tspasses (it ran in the same invocation).
Earlier ask (Enter in the re-rendered form skipping the check) is met: the first submit button carries no again, and a test pins it. MCP and sample-start tests are added. The duplicate lookup reads only the signer's own rows, so nobody else's listing is named. The address is escaped in the notice. The check runs before any photo is read or written. No contract cell changed. Non-blocking: src/duplicate.test.ts imports mkdtempSync twice from node:fs (once aliased as tmp); merge them. Non-blocking: the in-sentence button from the issue became a separate button after Launch so Enter cannot skip the check; the notice says so.
Left #72 open: 1 unticked item remain (Launching from the form on / when the signed-in person already authored a listing with the same address (compared trimmed, case- and space-insensitive) answers with the form again, every typed value kept and escaped, and a notice: "You launched 412 Alder Lane on Mar 3 — open it, or launch another anyway", the first a link to /listings/<id>, the second a button that resubmits with again=1.).
Launching an address you already launched now asks first. The form comes back with a link to the listing you have, and the JSON API and MCP tool answer 409. The full suite passes except
deps.test.ts("no lockfile or node_modules: nothing is installed"), which fails only becausebun installcreatednode_modulesand a lockfile here. Nothing in it is related to this change. I did not runtscbecause the repo has notsconfig.json, and there is noscripts/gate.tsorscripts/fast.tsto run.What changed
src/duplicate.tsholdsearlierAt(db, who, address). It matches case- and whitespace-insensitively and reads only rows the person authored, so another person's listing is never matched or named.launchListingtakesagain. It checks for a duplicate after validation and before any photo is read or written. If it finds one it returns a newduplicate_addressfailure carrying the existing listing. That failure maps to HTTP 409 and the API returns{error, message, id}.POST /listingsre-renders/with every typed value kept and escaped. The notice reads "You launched 412 Alder Lane on Mar 3 — open it, or launch another anyway". "Open it" links to/listings/<id>. The button resubmits the form withagain=1.?from=("Start a new one from this") is a GET and never shows the notice.launch_listingtool acceptsagain, and its error text carries the existing id.contract/openapi.json(regenerated) and the README agent section describe all of this.src/duplicate.test.tscovers the notice for the author, none for another user,againlaunching, the API's 409 and itsagain: trueoverride, a hostile address escaped in the notice, no photo files written when refused, and the sample prefill. The speed test still passes.Existing tests Twelve existing tests launch the same address repeatedly as one user. That is now a duplicate, so I added
againto their launch helpers inlistings,prefill,signinandcontracttests. The assertions are unchanged.What I chose not to do
src/tty.tshas no launch command, so there is nothing for it to ask and I did not add one.contract.jsonand the permission cells are untouched.To try it Launch an address, then launch it again (any case or spacing) on
/. You should see the notice. Click "launch another anyway" to launch a second one.Closes #72