{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"0543f750-a284-4783-a62f-1ae098fbff5a","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"e640e285ee9622b9310612654396b88d53d0c601a4a92c001f39f6c3558eadb8","dependsOn":["build_website"],"execution":{"network":false,"optionalTools":["browser"],"profile":"foundry","requires":[],"skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","state":"accepted"},{"acceptedSubmissionHash":"fac142fb4b1ea480ab6ffc090c86d8b2949dfedf7c8bd2c5e6b91e6510743fce","dependsOn":[],"execution":{"mustProduce":["dist/index.html"],"network":true,"optionalTools":["browser"],"profile":"none","requires":["network"],"skillHash":"a60bc8b42446c13d993341179fe13c712a5addb118fc532fb2d15e71f912efc8","skillId":"build-website","tools":[]},"key":"build_website","kind":"code","role":"implement","skillHash":"a60bc8b42446c13d993341179fe13c712a5addb118fc532fb2d15e71f912efc8","skillId":"build-website","state":"accepted"},{"acceptedSubmissionHash":"046b454af054841ba2def0b31049ca57f46a2af6288984877a4b37962253ab64","dependsOn":["build_website"],"execution":{"network":true,"optionalTools":["browser"],"profile":"none","requires":["network"],"skillHash":"fd33b6741366d364a55a4c0ba2bc4f5d329cec0b62f27a83289dc5b30ab03575","skillId":"design-critic","tools":[]},"key":"design_critic","kind":"code","role":"review","skillHash":"fd33b6741366d364a55a4c0ba2bc4f5d329cec0b62f27a83289dc5b30ab03575","skillId":"design-critic","state":"accepted"}],"objective":"The Hooks Store, job 9: proven launches and the wait, plus the open carry-overs. Carry over first, a regression from job 8 that blocks every launch: src/lib/compose.ts body() now always sends owner (s.owner or the connected wallet) but no longer lowercases it, and a connected wallet arrives with checksum capitals, so IMD's check answers 400 'owner: expected a lowercase EVM address' and a paid request would be refused on chain with the fee spent; lowercase owner, remainderTo and every address the body carries, add a unit test that composes with a checksummed wallet, and have the consistency check refuse any body field that is not a lowercase address. Carry over first, from launch 1 (SPEC 7.3, decision 25): for a contracts-only launch the result file behind the Completed event predates deployment and names no address, so the hook page takes the deployed addresses from the launch record through the relay (GET /launches/:id, artifacts[].address, attestation.contracts for the names), reads hook() when the artifact is a deployer and shows both addresses, says 'hook not deployed yet' while hook() is zero, and turns the Live step green when the Completed event has status 0 and the launch record reads live; the hook's address is never a condition for Live. Also carry job 8's residual review findings (evidence/job-8-reviews.md): (a) the written-fee detector in src/lib/writtenFees.ts is a word allowlist, so 'The owner receives 1% of every swap', '1% of each trade goes to the owner', '2% commission', 'toll', 'route', 'divert', 'accrues' still produce 'Hook fee rate: 0%' with no fee-side sentence and no finding; make it structural: any written amount (percent, basis points or a spelled fraction) near a trade word with no fee block counts as fee intent and composes the fee-side, settlement, recipient and partial-fill sentences, and an amount the parser cannot attach to a charge raises the clarification finding instead of writing zero; (b) 'payable to IMD's treasury' written in words leaves the paying wallet as the recipient in the deployer sentence and the fee footer, contradicting the written destination: the recipient named in words flows into both; (c) a spelled threshold such as 'one percent' or '1 percent' lets 'charge a small hook fee' pass without a rate: spelled numbers count as amounts and a fee without a rate is a finding; (d) the combined 5% cap adds written and block maxima across directions; apply it per direction. Then: Then implement SPEC.md section 4.17: the site fetches https://raw.githubusercontent.com/the-hook-store/hooks-store-spec/main/evidence/proven-launches.json at load (never bundled; a fetch failure means no proven launches); a draft matches an entry when the launch type, the set of block ids and the chain are the same, with parameters, extra words or a Custom rule allowed; nothing is asked: the match is applied in the background, the summary shows one line 'Built from a proven launch: {name} (launch {number}, audited {date})' and the hook page carries the same line after launch; a creator's own repository in Advanced overrides the match and there is no other control; when a match applies the body carries repoUrl and baseCommit from the entry, resolved first through the relay's /import, and if the import fails or the relay is absent the request is composed from scratch with no notice beyond the longer wait estimate; and the composed text says after the first sentence 'Start from the audited source at {repoUrl} commit {commit}: keep its behavior, apply the parameters and rules of this request, adapt the deployer to mine its salt inside its constructor if it mines after deployment, and audit it as imported code', while every other sentence of 5.1 and 4.16 stays. The Builder's summary and the Launch page say 'Typically 1 to 3 hours: the swarm writes, tests, audits with four auditors and a judge, then deploys. From a proven launch, usually less.' The hook page offers Copy link and asks once for permission to show a browser notification when the launch goes live. Tests: composer tests for a matching and a non-matching draft, the import path with the relay mocked, and Playwright for the summary line and the fallback at 320, 390 and 1440. Refresh SPEC.md from https://raw.githubusercontent.com/the-hook-store/hooks-store-spec/main/SPEC.md (now 1.34). Everything else from job 8 stays as delivered. Deliver source, dist/ and test results under tests/results/job9. The second step is an adversarial review by a different seat, read-only, of the composer's output with and without a proven launch, the import path, the owner and address lowercasing, the written-fee detector and the Live reading for contracts-only launches: it reports concrete reproductions of any request a builder would have to guess at or any body IMD would refuse, and the owner carries confirmed findings into the next job.","parentJobId":"cdfac8f6-434f-4234-8a6b-bb1762c06d87","planHash":"4d21ad319dd0a40a39db8902427c5c81c46073b8d683647b66da0d59c367726f","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"4cc83fbc-42d7-49fe-a9db-4310fb61a570","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2369-build-hooks-store-phase"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52707","feedbackHash":"e0f864de296349195c96b4c1adc536ee633ff719595a71b7903e7ae0a609baf4","nodeKey":"adversarial_review","submissionHash":"39ab0739d9f0aa2af8cd1168dd47ca4d2eb2df6930fbbac56dad0e1e73347dab","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51198","feedbackHash":"f179a77a0e5320bf3e949cbc05dcd82da6595cec61d3126c5fc3b216835ed97b","nodeKey":"adversarial_review","submissionHash":"e640e285ee9622b9310612654396b88d53d0c601a4a92c001f39f6c3558eadb8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50953","feedbackHash":"1d88070895aa7b1a4429f24d743b422549fbd495acc436e584a97df9ede982f7","nodeKey":"adversarial_review","submissionHash":"f1154d0e1a34202ff9c6aa40001253a1499e36b0b82cd9056c6620441d5f6a52","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50985","feedbackHash":"d762765b64a2821045b30571bfd2ebd6e8c0fed4383b1cda60abd7c39dd3cfa3","nodeKey":"build_website","submissionHash":"4c1b16f671892608c0d99cb7661f09713b71c65d6263f7b2ea83af9aeaceee7d","tag1":"verification:structural","tag2":"acceptance-v2","value":1},{"agentId":"51740","feedbackHash":"48f853bffe9e37dbe34988ae8ef70528a700fd14f852bee41f07deb91711eecb","nodeKey":"build_website","submissionHash":"f7709247c71a0555d6377b3713b81028d50a9f0621feb53119063c4a271dd3e2","tag1":"verification:structural","tag2":"acceptance-v2","value":1},{"agentId":"51189","feedbackHash":"58fa8851603586f703974cd45e8f9909fefdc4330d1b0aa482a43a111f963862","nodeKey":"build_website","submissionHash":"fac142fb4b1ea480ab6ffc090c86d8b2949dfedf7c8bd2c5e6b91e6510743fce","tag1":"verification:structural","tag2":"acceptance-v2","value":1},{"agentId":"51041","feedbackHash":"08f109b61ad9e8a37acff12861e8dff9eef38474f6f5b1993bf280ef1d0a8c1b","nodeKey":"design_critic","submissionHash":"eee2c343a4413c71ba94f5a393fe532805604084bc24c38626f2273af295fffb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"fb35774e13629b606ae7167d7d1a80208960e1afc6b79fd0b5ec238c3676fe2e","nodeKey":"design_critic","submissionHash":"046b454af054841ba2def0b31049ca57f46a2af6288984877a4b37962253ab64","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51758","feedbackHash":"274fefa6b3c0c4c12538b9d8a9fdf0f445c21f443263ba8cc7983e74d9f78053","nodeKey":"design_critic","submissionHash":"0cd40a2ad1e437847fb819e8f9779143fccc968f4869780e01c07471b7833b5a","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"2f32a43711074b2d43d024604c724227d084223836ecefb1d24af7eda95ab64b","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"468e82a89b9bfe18","findings":[{"citation":"resolved","description":"Now: the revision fixes the reported cases. 'The owner is paid/gets/is credited/will be paid 1% of every swap.' now resolves to settings.owner in the deployer constructor and in 'Fee recipient:'. 'Alice is paid 1% of every swap.' and '1% of every swap goes to Bob.' now raise 'recipient'. But passivePayee (line 138) only matches a payee followed by a passive participle (is paid/credited/owed/given/sent), and ownerReceives (line 135) only matches owner, my wallet, treasury or a literal address. So when the payee is anyone else and the verb is a receipt verb ('Alice receives 1% of every swap.', 'The marketing wallet receives 1% of every swap.', 'Bob earns 1% of every swap.', 'The dev fund gets 1% of every swap.'), writtenFeeTargets returns [] and feeRecipient falls back to the paying wallet. The body then quotes 'Alice receives 1% of every swap.' inside <description> and names the payer in the deployer constructor and in 'Fee recipient:'. validate() and checkConsistency() both return []. This is the 'cannot be resolved -> require clarification, never default to the payer' half of the original finding. Change to: let the clause-initial unknown-payee rule accept the same receipt verbs ownerReceives already accepts. Extend passivePayee's verb group to (?:(?:is|are|will be|shall be|gets?)\\s+(?:paid|credited|owed|given|sent)\\b(?!\\s+(?:to|by|in|as|out)\\b)|receives?|accrues?|gets?|earns?|keeps?|collects?)\\b. notPayee already stops 'the hook takes', 'traders get' and fee subjects from counting. With that change, an unresolved name reaches checkConsistency's 'recipient' finding, which makes launchBody() throw, exactly as 'goes to Bob' does today. Add these four prompts to the regression in tests/job9-revision.test.mjs. Why: the brief's carry-over (b) says 'the recipient named in words flows into both' the deployer sentence and the fee footer. The review step must report 'any request a builder would have to guess at'. Here the request names Alice and the payer as the recipient at once and still passes the payment gate.","line":138,"path":"src/lib/writtenFees.ts","reproduction":"Width: not applicable (pure composer, same at every viewport). Node 22, repository root, node --experimental-strip-types: import * as C from './src/lib/compose.ts'; import {checkConsistency} from './src/lib/safety.ts'; import {writtenFeeTargets} from './src/lib/writtenFees.ts'; const d=C.newDraft(); d.type='B'; d.mode='prompt'; d.prompt='Alice receives 1% of every swap.'; Object.assign(d.settings,{chain:4663,pool:50,hname:'PondHook',owner:'0x9a50fd22bcefd3774d796e775a467a7f743e0ab0'}); const payer='0x6cba4751742f6989fa224b2dc5d5d52fcef98a34'; const b=C.body(d,payer). Expected (as for 'Alice is paid 1% of every swap.' and '1% of every swap goes to Bob.' on this tree): writtenFeeTargets returns ['Alice'] and checkConsistency(b) contains 'recipient', so launchBody() throws. Actual: writtenFeeTargets(d.prompt) = []; C.feeRecipient(d,payer) = 0x6cba…8a34; the body contains '<description>Alice receives 1% of every swap.</description>' and also '...$owner and the fee recipient 0x6cba4751742f6989fa224b2dc5d5d52fcef98a34, takes no salt argu...' and 'Fee recipient: 0x6cba4751742f6989fa224b2dc5d5d52fcef98a34.'; C.validate(d,payer) = [] and checkConsistency(b) = []. web/src/chain/launch.ts launchBody() gates only on those two, so it serialises the body. The same holds for 'The marketing wallet receives 1% of every swap.', 'Bob earns 1% of every swap.' and 'The dev fund gets 1% of every swap.'. Controls on the same tree: 'Alice is paid 1% of every swap.' -> cc ['recipient','fee-recipient']; '1% of every swap is paid to Bob.' -> cc ['recipient','fee-recipient']; 'The owner gets 1% of every swap.' -> owner in both places, cc []. tests/job9-revision.test.mjs passes 10/10 because it has no active-voice unknown payee.","severity":"high","snippet":"  const passivePayee = /^(?:then\\s+)?((?:(?:the|my|our)\\s+)?[a-z][\\w'’-]*(?:\\s+[a-z][\\w'’-]*){0,2}|0x[\\da-fA-F]{40})\\s+(?:is|are|will be|shall be|gets?)\\s+(?:paid|credited|owed|given|sent)\\b(?!\\s+(?:to|by|in|as|out)\\b)/gi;","title":"bug: an unknown payee written with 'receives', 'gets' or 'earns' still makes the paying wallet the fee recipient"}],"hash":"046b454af054841ba2def0b31049ca57f46a2af6288984877a4b37962253ab64","nodeId":"3b81142c-6aca-48c0-92ce-ccb75550d559","outcome":"completed","summary":"Your earlier high finding is mostly fixed. It still fails in one case, so I've kept it open as one high finding in `.imd-findings.json`. I changed no other file.\n\n| Area | This round |\n|---|---|\n| Brief requirements (met / missing / faked) | Only carry-over (b), \"the recipient named in words flows into both\", re-checked: still missing for one kind of wording (below) |\n| Broken features | 1 (the payee issue below) |\n| Product on the first screen at 390 | Not re-measured; this round only settles the composer finding |\n| taste@1 checks passed | Probe not re-run this round |\n| Copy findings | 0 new |\n| Share card | Not re-checked |\n\n**What the fix gets right.** I re-ran your reproduction: draft type B, owner `0x9a50…0ab0`, paying wallet `0x6cba…8a34`. The deployer constructor and the `Fee recipient:` footer now both name the owner when the prompt says:\n- \"The owner is paid 1% of every swap.\"\n- \"The owner gets 1% of every swap.\"\n- \"The owner will be paid 1% of every swap.\"\n- \"The owner is credited 1% of each trade.\"\n\nTypes A and B give no findings. Type C only asks for the existing token, as it should. In type A the paying wallet still appears, but only as the token's remaining-supply recipient, which is by design. \"Alice is paid 1% of every swap.\" and \"1% of every swap goes to Bob.\" now raise `recipient`, and `launchBody()` in `web/src/chain/launch.ts` refuses the request. `tests/job9-revision.test.mjs` passes 10 of 10.\n\n**Why it doesn't fully hold** (`src/lib/writtenFees.ts:138`). The new rule only catches an unknown payee followed by \"is paid / credited / owed / given / sent\". An unknown payee followed by \"receives\", \"gets\" or \"earns\" still defaults to the paying wallet, with no finding. For \"Alice receives 1% of every swap.\":\n- The body quotes the creator's words: `<description>Alice receives 1% of every swap.</description>`.\n- The same body also says `$owner and the fee recipient 0x6cba4751742f6989fa224b2dc5d5d52fcef98a34` and `Fee recipient: 0x6cba4751742f6989fa224b2dc5d5d52fcef98a34.`\n- `validate()` and `checkConsistency()` both return nothing, so the payment gate lets it through.\n\n\"The marketing wallet receives…\", \"Bob earns…\" and \"The dev fund gets 1% of every swap.\" behave the same way. A builder reading this request would have to guess who gets paid. That is the half of the original finding that asked for \"require recipient clarification rather than defaulting to the payer\".\n\n**Fix:** extend `passivePayee`'s verb group to also accept `receives? | accrues? | gets? | earns? | keeps? | collects?`. The existing `notPayee` filter already excludes fee words, traders and \"it/them\". Then add those four prompts to the revision test.\n\nI reported nothing else new.","treeHash":null,"usage":{"cachedInputTokens":759484,"inputTokens":28,"model":"claude-opus-5-5","outputTokens":9081,"runtime":"claude","turns":15,"wallClockMs":102977}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b94d8a9fa9123fb1","findings":[{"citation":"resolved","description":"Now: writtenFeeTargets recognises a subject that 'receives' or 'accrues', but misses 'The owner is paid 1% of every swap.' With a distinct explicit owner, body() names that owner while both the deployer constructor and fee footer name the payer; validate() and checkConsistency() return no findings.\nChange to: recognise the named payee in passive payment clauses, resolve 'the owner' to settings.owner, and use that address in both constructor and footer. If a named destination cannot be resolved, require recipient clarification rather than defaulting to the payer. Add a regression with different payer and owner addresses.\nWhy: the brief requires the written destination to flow into the deployer and fee footer, and the adversarial review must catch requests whose contradictory instructions require the builder to guess. The current request can pass the local payment gate despite specifying two different fee recipients.","line":135,"path":"src/lib/writtenFees.ts","reproduction":"Width: not applicable (pure composer; identical at every viewport). Node 24, repository root: import * as C from './src/lib/compose.ts'; import {checkConsistency} from './src/lib/safety.ts'; const d=C.newDraft(); d.type='B'; d.mode='prompt'; d.prompt='The owner is paid 1% of every swap.'; Object.assign(d.settings,{chain:4663,pool:50,hname:'PondHook',owner:'0x9a50fd22bcefd3774d796e775a467a7f743e0ab0'}); const payer='0x6cba4751742f6989fa224b2dc5d5d52fcef98a34'; const b=C.body(d,payer); inspect b.owner, b.objective, C.feeRecipient(d,payer), C.validate(d,payer), checkConsistency(b). Expected: constructor and 'Fee recipient:' both use 0x9a50fd22bcefd3774d796e775a467a7f743e0ab0, or a recipient clarification blocks the request. Actual: b.owner is 0x9a50fd22bcefd3774d796e775a467a7f743e0ab0 but both generated recipient locations and feeRecipient() are 0x6cba4751742f6989fa224b2dc5d5d52fcef98a34; both check arrays are []. writtenFeeTargets(d.prompt) returns []. Controls 'Swappers pay one percent to the owner.' and 'Buyers and sellers pay 1% to the owner.' correctly use the distinct owner and have no zero-fee footer. Production export confirmation in Chromium at 390 x 844: seed that exact draft into localStorage key hooks-store:draft:v2, open #builder, connect the injected fixture payer, and choose Continue to launch. There is no .safety-notice; Continue is enabled; #launch's launch-json test element still has the distinct body.owner and the payer in both fee-recipient locations. The actual launchBody(d, siteConfig, payer) also returns serialized JSON without throwing. No transaction is submitted. Across A/B/C, both supported chains, both pairs and Describe/Custom editors, the two previously reported prompts pass 48 variants; the passive-payee prompt exhibits the same wrong destination in all 24 variants.","severity":"high","snippet":"  const ownerReceives = /\\b((?:the )?owner|my wallet|(?:(?:the|IMD['’]s) )?treasury|0x[\\da-fA-F]{40})\\s+(?:receives?|accrues?)\\b/gi;","title":"bug: a passive written owner payment silently selects the paying wallet as fee recipient"}],"hash":"0cd40a2ad1e437847fb819e8f9779143fccc968f4869780e01c07471b7833b5a","nodeId":"3b81142c-6aca-48c0-92ce-ccb75550d559","outcome":"completed","summary":"| Area | Previous report | This review |\n|---|---:|---:|\n| Requirements | — | 35 met, 1 missing, 0 faked, 1 after review |\n| Broken features | 1 high | Previous fixed; 1 new high |\n| Product/action on first screen, 390px | — | Yes |\n| taste@1 | — | 13/16 passed |\n| Copy findings | — | 0 |\n| Share card | — | Present |\n\nThe previously reported zero-fee bug is **fixed**. Both original prompts now compose the required fee sentences; 48 variants passed across launch types, chains, pairings and editors.\n\n**High — bug: a passive written owner payment selects the payer as fee recipient.** With a distinct owner, “The owner is paid 1% of every swap.” puts the payer in both the deployer sentence and fee footer. Validation and consistency checks return no findings. Chromium at **390 × 844** also lets this contradictory request reach Launch review. Recognize the passive payee and resolve it to the owner, or require clarification before proceeding. The exact inputs, measured output and source snippet are recorded in [.imd-findings.json](/home/imdc2/.identitymd/work/0543f750-a284-4783-a62f-1ae098fbff5a/3b81142c-6aca-48c0-92ce-ccb75550d559/.imd-findings.json), citing [writtenFees.ts:135](/home/imdc2/.identitymd/work/0543f750-a284-4783-a62f-1ae098fbff5a/3b81142c-6aca-48c0-92ce-ccb75550d559/src/lib/writtenFees.ts:135).\n\n| Explicit requirement | Status and evidence |\n|---|---|\n| Lowercase owner, remainderTo and every carried address | Met — `src/lib/compose.ts` |\n| Unit test using a checksummed wallet | Met — `tests/job9.test.mjs` |\n| Consistency rejects invalid or nonlowercase address fields | Met — `src/lib/safety.ts` |\n| Read contracts-only launches through relay GET `/launches/:id` | Met — `web/src/chain/store.ts` |\n| Use artifact addresses and attestation contract names | Met — `web/src/chain/decode.ts` |\n| Read deployer `hook()` and show both addresses | Met — store, Hook and browser tests |\n| Show “hook not deployed yet” for zero `hook()` | Met — `web/src/Hook.tsx`, browser tests |\n| Live requires Completed status 0 plus live record, independently of hook address | Met — decode and browser tests |\n| Recognize written percentages, basis points and fractions near trade words | Met — `writtenFees.ts`, regression tests |\n| Compose correct fee-side, settlement, recipient and partial-fill sentences | **Missing correctness for passive named recipients** — finding above |\n| Unattached amounts require clarification instead of zero | Met — composer consistency tests |\n| Written “payable to IMD’s treasury” reaches deployer and footer | Met — job-9 tests |\n| Spelled thresholds cannot substitute for a missing fee rate | Met — job-9 tests |\n| Apply the combined 5% cap per direction | Met — composer and consistency tests |\n| Fetch the specified proven index at load; never bundle it | Met — `proven.ts`, `ProvenLaunches.tsx` |\n| Failed index fetch means no proven launches | Met — unit and browser fallback tests |\n| Match type, block-id set and chain; allow parameters, words and Custom rule | Met — matching tests |\n| Apply matches automatically in the background | Met — `useProvenSource`, rendered summary |\n| Show the exact provenance line in the summary | Met — browser tests |\n| Carry the same provenance onto the hook page | Met — Hook browser tests |\n| Advanced own repository overrides matching; no additional control | Met — browser override test |\n| Carry entry `repoUrl` and `baseCommit` | Met — composer tests |\n| Resolve proven source through relay `/import` first | Met — mocked import tests |\n| Failed import or absent relay silently composes from scratch | Met — fallback tests at all three widths |\n| Insert the specified audited-source instruction after the first sentence | Met — composer and revision tests |\n| Retain other composed request sentences | Met — fresh/proven output comparison |\n| Show the exact wait estimate in Builder and Launch | Met — both rendered surfaces |\n| Offer Copy link | Met — actual clipboard browser assertion |\n| Ask once abo","treeHash":null,"usage":{"cachedInputTokens":3079808,"inputTokens":154792,"model":"gpt-6.1-sol","outputTokens":16129,"runtime":"codex","turns":5,"wallClockMs":502919}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2293d6b19e81ef1a","findings":[{"citation":"resolved","description":"compose() inserts the 'Start from the audited source…' sentence by cutting lines[0] at the first '. '. For a type A launch the first line is 'Launch {name} ({symbol}) with a Uniswap v4 hook on {chain}, paired with {pair}.' and the token name is free text (catalog: type text, 1 to 40 characters, no pattern), so a name such as 'Dr. Pond' or 'St. Croix' is cut inside the name. The request then opens with 'Launch Dr.' followed by the instruction and 'Pond (DRP) with a Uniswap v4 hook on Robinhood Chain, paired with IMD.' checkConsistency() reads only text.split('\\n')[0] for the pairing check, so it reports 'pairing' and launchBody() refuses a draft that validate() accepts; the same draft without a proven match composes and passes. The user sees a pairing error for a request whose pairing is correct and cannot launch while the index matches their stack; and if the gate were bypassed, the first sentence sent to IMD would be 'Launch Dr.' Split at the end of the first sentence that is not inside the name (or build the first line from its parts) instead of the first '. ' in the string.","line":247,"path":"src/lib/compose.ts","reproduction":"d = newDraft(); d.settings.chain = 4663; d.settings.pool = 50; d.mode = 'blocks'; d.blocks.push(entry('swap-fee')); d = setParam(d, 0, 'name', 'Dr. Pond'); d = setParam(d, 0, 'symbol', 'DRP'); d.proven = { launchId: 'l1', launchNumber: 1, chainId: 4663, type: 'A', blocks: ['token','swap-fee'], repoUrl: 'https://github.com/identity-md-launches/launch-1-x', commit: 'a'.repeat(40), name: 'PondsideGuard', verifiedAt: '2026-10-22' }; b = body(d, '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'). Expected: first line 'Launch Dr. Pond (DRP) with a Uniswap v4 hook on Robinhood Chain, paired with IMD.' then the instruction, and checkConsistency(b) === []. Actual: b.objective.split('\\n')[0] === 'Launch Dr.', line 3 is 'Pond (DRP) with a Uniswap v4 hook on Robinhood Chain, paired with IMD.', and checkConsistency(b) returns [{ code: 'pairing', sentence: 'Launch Dr.', message: 'The request and launch body must both name the IMD pairing.' }]. validate(d, wallet) returns [] for the same draft, and with d.proven removed the body passes.","severity":"high","snippet":"    const end = lines[0].indexOf('. ');","title":"Proven-launch instruction splits the first line inside a token name containing \". \", garbling the request and locking Launch with a false \"pairing\" finding"},{"citation":"resolved","description":"The detector is still a word allowlist in two places: chargePattern has no 'pay/pays/paid' verb and tradePattern (line 66) has no agent nouns (swapper, buyer, seller, trader). A clause such as 'Swappers pay 1 percent.' or 'Every buyer pays 1%.' has a written amount but neither a trade word nor a charge word, so writtenFeeClauses() returns [] (line 83-84 requires charge || trade), compose() writes 'Hook fee rate: 0%.' with no fee-side, settlement, recipient or partial-fill sentence, and checkConsistency() returns no finding. The paid request therefore says both 'pays 1%' and 'Hook fee rate: 0%', which the builder must guess at after the fee is spent. Job 9's requirement was that any written amount near a trade word with no fee block counts as fee intent and an amount the parser cannot attach raises the clarification finding instead of writing zero. 'Buyers pay 2% to the owner.' is blocked only by an unrelated 'recipient' finding about 'the owner' while the fee line still reads 0%.","line":68,"path":"src/lib/writtenFees.ts","reproduction":"For p of ['Swappers pay 1 percent.', 'Every buyer pays 1%.']: d = newDraft(); d.settings.chain = 4663; d.settings.pool = 50; d.mode = 'prompt'; d.prompt = p; b = body(d, '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'). Expected: either the fee-side sentence ('Fees are always taken from the IMD (pair currency) side… settled in IMD… Fee recipient: 0xab58…') with writtenFeeRates(p) = {buy:1,sell:1} (or {buy:1,sell:0} for the buyer sentence), or a 'fee-rate' finding from checkConsistency(b). Actual: writtenFeeClauses(p) === [], writtenFeeAnalysis(p).unattached === ['1%'], b.objective contains 'Hook fee rate: 0%.', checkConsistency(b) === [] and validate(d) === []; the request can be paid as is.","severity":"high","snippet":"const chargePattern = /\\b(?:take|takes|taking|charge|charges|charging|collect|collects|collecting|deduct|deducts|withhold|withholds|levy|levies|skim|skims|skimming|keep|keeps|keeping|retain|retains|retaining|route|routes|routing|divert|diverts|diverting|accrues?|receives?)\\b/i;","title":"Written charges phrased with 'pay(s)' and a trader noun ('Swappers pay 1 percent', 'Every buyer pays 1%') still compose 'Hook fee rate: 0%' with no finding"},{"citation":"resolved","description":"writtenFeeTargets() captures only the payee that directly follows 'to' or 'for'. In 'Take 1% of every swap for IMD's treasury and the owner.' the second payee ('and the owner') is never extracted, so feeRecipient() sees one target, compose() writes 'Fee recipient: 0x3f252e859a277a86a3967c11d4e054b2f07a1f22.' and, for type B/C, forwards the treasury alone in the deployer constructor; checkConsistency() returns []. The request contradicts the creator's words (two recipients) and the builder must guess between a split and the footer. The same text with 'paid to the owner and to IMD's treasury equally' is correctly refused ('recipient' and 'fee-recipient'), so only the coordinated 'X and Y' after one preposition escapes.","line":130,"path":"src/lib/writtenFees.ts","reproduction":"d = newDraft(); d.settings.chain = 4663; d.settings.pool = 50; d.mode = 'prompt'; d.prompt = \"Take 1% of every swap for IMD's treasury and the owner.\"; b = body(d, '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'). Expected: writtenFeeTargets(d.prompt) names both \"IMD's treasury\" and 'the owner', feeRecipient() returns 'the unresolved written fee recipient' and checkConsistency(b) reports 'recipient'. Actual: writtenFeeTargets(d.prompt) === [\"IMD's treasury\", \"IMD's treasury\"], b.objective contains 'Fee recipient: 0x3f252e859a277a86a3967c11d4e054b2f07a1f22.', checkConsistency(b) === [], validate(d) === [].","severity":"medium","snippet":"  const explicitTarget = /\\b(?:to|for)\\s+((?:the )?owner|my wallet|(?:IMD['’]s )?treasury|0x[\\da-fA-F]{40})\\b/gi;","title":"A written fee naming two destinations with 'for X and Y' silently drops the second: 'Take 1% of every swap for IMD's treasury and the owner' composes a treasury-only recipient with no finding"},{"citation":"resolved","description":"feeClauses() splits a prohibition off only when it precedes a comma (line 6-7) or when 'and' is followed by a charge verb (line 3). With the charge first and the prohibition second ('Collect a 2% fee on sells and no fee on buys.'), the whole clause stays together, isFeeProhibition() matches 'no fee on buys', and writtenFeeClauses() drops the clause. compose() then writes 'Hook fee rate: 0%.' and omits the fee facts, while checkConsistency() finds the 2% sell charge via chargedScopes() and reports two 'contradiction' findings against the generated zero-fee line. The creator wrote a consistent rule and is told it contradicts itself; Launch is locked with a message that names no fix. Job 9's earlier review fixed the reverse order ('No buy fee, and the owner receives 1% of every sell.'), which passes.","line":80,"path":"src/lib/writtenFees.ts","reproduction":"d = newDraft(); d.settings.chain = 4663; d.settings.pool = 50; d.mode = 'prompt'; d.prompt = 'Collect a 2% fee on sells and no fee on buys.'; b = body(d, '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'). Expected: writtenFeeClauses(d.prompt) keeps the sell charge, writtenFeeRates(d.prompt) = {buy:0, sell:2}, the fee-side sentence is composed and checkConsistency(b) === [] (as for 'No buy fee, and the owner receives 1% of every sell.'). Actual: writtenFeeClauses(d.prompt) === [], writtenFeeRates(d.prompt) = {buy:0, sell:0}, b.objective contains 'Hook fee rate: 0%.' and checkConsistency(b).map(f => f.code) === ['contradiction', 'contradiction'].","severity":"medium","snippet":"    if (isFeeProhibition(clause)) return false;","title":"A charge followed by a scoped prohibition in the same clause ('Collect a 2% fee on sells and no fee on buys') is treated as a prohibition: zero fee is written and a false 'contradiction' blocks the la"},{"citation":"resolved","description":"feeRecipient() resolves 'treasury' and \"IMD's treasury\" to IMD_TREASURY, but writtenFeeTargets() returns the article with the noun ('the treasury'), which the anchored regex rejects. The footer then reads 'Fee recipient: the treasury.' and checkConsistency() blocks with a generic 'recipient' finding although SPEC decision 17 defines treasury as IMD's fee recipient and the block option with the same meaning resolves. The user is stopped with no hint that the word is accepted in another form.","line":261,"path":"src/lib/compose.ts","reproduction":"d = newDraft(); d.settings.chain = 4663; d.settings.pool = 50; d.mode = 'prompt'; d.prompt = 'Collect 1% for the treasury.'; b = body(d, '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'). Expected: b.objective contains 'Fee recipient: 0x3f252e859a277a86a3967c11d4e054b2f07a1f22.' and checkConsistency(b) === []. Actual: writtenFeeTargets(d.prompt) === ['the treasury'], feeRecipient(d, wallet) === 'the treasury', b.objective contains 'Fee recipient: the treasury.' and checkConsistency(b).map(f => f.code) === ['recipient'].","severity":"low","snippet":"        : /^(?:IMD['’]s )?treasury$/i.test(target) ? IMD_TREASURY","title":"'the treasury' as a written fee destination is not resolved to IMD's treasury and leaves 'Fee recipient: the treasury.' in the request"},{"citation":"resolved","description":"matchProvenLaunch() compares the set of non-custom block ids. In prompt mode the draft's set is empty (type B/C) or {token} (type A), so it equals any index entry with blocks [] or ['custom'] of the same type and chain, whatever the creator wrote. The body would then carry that entry's repoUrl/baseCommit and the text would say 'Start from the audited source at … keep its behavior' for a hook the prose does not describe, and the summary would announce 'Built from a proven launch'. The live index (one type B entry with four blocks) does not trigger this today, so this is latent; it becomes live the first time the owner lists a words-only launch. A words-only draft should match only when the entry is also words-only and the words are the same, or never.","line":33,"path":"src/lib/proven.ts","reproduction":"f = newDraft(); f.type = 'B'; f.settings.chain = 4663; f.settings.hname = 'PondHook'; f.mode = 'prompt'; f.prompt = 'Reject every swap larger than 10 ETH.'; e = { launchId: 'l1', launchNumber: 1, chainId: 4663, type: 'B', blocks: [], repoUrl: 'https://github.com/identity-md-launches/launch-1-x', commit: 'a'.repeat(40), name: 'PondsideGuard', verifiedAt: '2026-10-22' }. Expected: matchProvenLaunch(f, [e]) === undefined. Actual: matchProvenLaunch(f, [e]) returns e, and matchProvenLaunch(f, [{ ...e, blocks: ['custom'] }]) returns the entry too; with f.proven = e, body(f, wallet).repoUrl === e.repoUrl and the second line of the objective is the 'Start from the audited source' instruction.","severity":"low","snippet":"  return entries.find(p => p.type === draft.type && p.chainId === draft.settings.chain","title":"A words-only draft matches a proven entry whose block list is empty or Custom-only, so arbitrary prose would be told to keep an unrelated audited hook's behavior"}],"hash":"39ab0739d9f0aa2af8cd1168dd47ca4d2eb2df6930fbbac56dad0e1e73347dab","nodeId":"84ea3aa6-e0b0-4dff-a7b0-5bb9e5ae560c","outcome":"completed","summary":"Review complete. Six findings are in `.imd-findings.json`, each verified by running the real `compose()`, `body()`, `validate()` and `checkConsistency()` against the tree (in a temp copy, since the root `package.json` here is an empty, git-excluded file that breaks Node's module resolution for the repo in place). No repository file was changed.\n\n**What held up.** Owner, remainder, fee recipient and every address in the objective arrive lowercase with a checksummed wallet, and the consistency check refuses non-lowercase body fields. The seven phrasings named in the brief (\"owner receives 1%\", \"goes to the owner\", \"2% commission\", \"toll\", \"route\", \"divert\", \"accrues\"), spelled amounts (\"one percent\", \"half a percent\", \"one per cent\"), \"payable to IMD's treasury\" flowing into footer and deployer constructor, a rate-less fee raising a finding, and the per-direction cap (4% buys plus 4% sells accepted, 3% swap plus 3% buy refused) all behave as specified. Proven matching ignores parameters, words and Custom, is overridden by an own repository, the instruction lands after the first sentence for type B with the deployer check still passing, the relay import is compared against the pinned entry, and contracts-only Live requires Completed status 0 plus a live record with the hook address never a condition.\n\n**Findings, by severity**\n\n- **High, `src/lib/compose.ts:247`.** The proven instruction is spliced at the first `. ` in line one. A token name like \"Dr. Pond\" (free text, allowed by the catalog) is cut in half, the request opens with \"Launch Dr.\", and the consistency check raises a false \"pairing\" finding that locks Launch while the index matches the stack.\n- **High, `src/lib/writtenFees.ts:68`.** \"Swappers pay 1 percent.\" and \"Every buyer pays 1%.\" produce \"Hook fee rate: 0%\" with no fee facts and no finding. \"pay\" is not a charge verb and agent nouns are not trade words, so the detector is still an allowlist for this class.\n- **Medium, `src/lib/writtenFees.ts:130`.** \"Take 1% of every swap for IMD's treasury and the owner.\" drops the second payee silently and composes a treasury-only recipient with no finding.\n- **Medium, `src/lib/writtenFees.ts:80`.** \"Collect a 2% fee on sells and no fee on buys.\" is classed as a prohibition, writes 0%, and then triggers two false \"contradiction\" findings. The reversed order fixed in the prior review passes.\n- **Low, `src/lib/compose.ts:261`.** \"Collect 1% for the treasury.\" leaves \"Fee recipient: the treasury.\" and a generic recipient block, although the spec defines treasury as IMD's address.\n- **Low, `src/lib/proven.ts:33`.** A words-only draft matches any index entry with an empty or Custom-only block list. Latent today, since the live index has one four-block type B entry.\n\n**Not covered.** No browser inspection of the summary line, Copy link or the notification prompt at the three widths, and no live relay or IMD check was called. The job 9 unit file could not run in this tree because `web/node_modules` and `viem` are not installed here, so I relied on direct probes rather than the shipped test run.","treeHash":null,"usage":{"cachedInputTokens":1211416,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":27019,"runtime":"claude","turns":26,"wallClockMs":416602}},{"artifacts":[],"attempt":1,"bundleHash":"be3b1075c90162843a76361fa3d3a81a468ae1a5f06d5c8a3fed5567d4bb5c8f","device":"410247426ec5a6df","findings":[],"hash":"4c1b16f671892608c0d99cb7661f09713b71c65d6263f7b2ea83af9aeaceee7d","nodeId":"9e4778fc-dd82-4afb-ac1e-0db645f60966","outcome":"completed","summary":"","treeHash":"0e866ba60ab74f3c8c9205d88182bc0ecb2170f5","usage":{"cachedInputTokens":4846136,"inputTokens":104,"model":"claude-opus-5-5","outputTokens":41476,"runtime":"claude","turns":53,"wallClockMs":1073270}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"29ea8835baa3dfe1","findings":[{"citation":"resolved","description":"The site resolves a matched proven launch through the relay's /import (forwarded unchanged to https://api.imd.fun/requests/import). IMD normalises the source URL and returns it with a '.git' suffix (same shape already recorded in tests/results/job5/live-relay-summary.json for launch 2335). importProvenSource compares source.repoUrl to entry.repoUrl strictly and returns null on any difference, and useProvenSource treats null as 'import failed'. So for the only listed entry (PondsideGuard, launch 2424) and every future entry from identity-md-launches, the draft is always composed from scratch: no 'Built from a proven launch' line in the summary or on the hook page, no repoUrl/baseCommit in the body, and no 'Start from the audited source' sentence. Everything required by SPEC 4.17 is dead in production while every test passes, because both the Playwright mock (web/tests/job9.spec.ts: source: { repoUrl: imports.at(-1).url, ... }) and the unit test echo the request URL back verbatim instead of IMD's real response.","line":58,"path":"web/src/relay.ts","reproduction":"Live (2026-10-22): POST https://hooks-store-relay.hooks-store.workers.dev/import {\"url\":\"https://github.com/identity-md-launches/launch-2424-deploy-one-contract-pondsideguard\",\"ref\":\"5a9f6a36d659084e084a8abc08a55213c9516f07\",\"kind\":\"contracts\"} answers {\"ok\":true,\"source\":{\"kind\":\"contracts\",\"repoUrl\":\"https://github.com/identity-md-launches/launch-2424-deploy-one-contract-pondsideguard.git\",\"baseCommit\":\"5a9f6a36d659084e084a8abc08a55213c9516f07\",...}}. Mock globalThis.fetch to return exactly that JSON and call importProvenSource('https://hooks-store-relay.hooks-store.workers.dev', <the entry from evidence/proven-launches.json>): it returns null (expected: the entry). As a result, a Type B draft on chain 4663 with blocks swap-fee, anti-sniper, jar, pause (the exact proven set) shows no proven line and body() has no repoUrl/baseCommit. Fix direction (preserves intent): compare the pinned commit and the owner/repo identity (e.g. source.owner/source.repo, or the URL with a trailing '.git' or '/' stripped) instead of the raw string, and make the test mocks return IMD's real response shape.","severity":"high","snippet":"    if (data.ok !== true || source.repoUrl !== entry.repoUrl || String(source.baseCommit).toLowerCase() !== entry.commit) return null;","title":"Proven-launch import always fails against the real relay: IMD's /import returns repoUrl with '.git' and importProvenSource requires exact equality, so SPEC 4.17 never applies in production"},{"citation":"resolved","description":"Carry-over (a)/(c) requires that any written amount (percent, basis points or a fraction) near a trade word with no fee block counts as fee intent, and that an amount the parser cannot attach raises the clarification finding instead of writing zero. normalizeFeeAmounts only recognises '%', 'percent', 'per cent', 'basis points', 'bps' and spelled fractions ('a hundredth of'), and the clause filter only matches those. A numeric fraction ('1/100 of every swap') or the common abbreviation 'pct' is not an amount, there is no fee word, and 'goes to' is not a charge verb, so writtenFeeClauses returns [] and compose() falls through to line 233 'Hook fee rate: 0%'. The request then tells the builder there is no fee while the creator's words direct 1% of every swap to the owner, and checkConsistency/validate raise nothing, so a contradictory request can be paid for.","line":83,"path":"src/lib/writtenFees.ts","reproduction":"node (Node 24, from repo root): import * as C from './src/lib/compose.ts'; const d=C.newDraft(); d.type='B'; d.mode='prompt'; d.settings.chain=4663; d.settings.hname='Hk'; d.prompt='1/100 of every swap goes to the owner.'; C.compose(d,'0x6cBA4751742F6989fa224b2dC5d5d52fcEf98A34') contains 'Hook fee rate: 0%' and no 'Fees are always taken' or 'Fee recipient' sentence; checkConsistency(C.body(d,wallet)) returns [] and C.validate(d,wallet) returns []. Same for d.prompt='1 pct of every swap goes to the owner.' and 'One in every hundred tokens traded goes to the owner.'. Expected: either fee intent (fee-side, settlement, recipient and partial-fill sentences with 1% buy/sell) or the fee-rate clarification finding. Compare 'A hundredth of the ETH in every swap goes to the owner.', which correctly raises fee-rate.","severity":"medium","snippet":"    const writtenAmount = amounts.length > 0 || /%|unresolved-amount|\\b(?:percent(?:age)?|per cent|basis points?|bps|(?:half|quarter|third|[a-z]+ths?) of)\\b/i.test(normalizeFeeAmounts(clause));","title":"Written fee detector still composes 'Hook fee rate: 0%' for a numeric fraction ('1/100') or 'pct' amount near a trade word, with no finding"},{"citation":"resolved","description":"For Type A, matchProvenLaunch puts the always-present token block ('token' or 'own-token') into the id set and then requires the entry's block set to have the same size. The only published entry (launch 2424, Type B) lists behavior blocks only: swap-fee, anti-sniper, jar, pause. SPEC 4.17 says 'the set of block ids', and nothing tells the owner to list the fixed Token block. If the owner adds a Type A entry the same way, no Type A draft can ever match it. This is a scope question about the entry format: either document that Type A entries must list 'token' or 'own-token', or compare the behavior-block sets plus the token kind. No Type A entry exists today, so this is not blocking.","line":32,"path":"src/lib/proven.ts","reproduction":"d = addBlock(addBlock(addBlock({...newDraft(), type:'A', mode:'blocks', settings:{...emptySettings(), chain:4663}}, 'swap-fee'), 'anti-sniper'), 'jar'); matchProvenLaunch(d, [{...entry2424, type:'A', blocks:['swap-fee','anti-sniper','jar']}]) returns undefined. With blocks:['token','swap-fee','anti-sniper','jar'] it matches. Expected (per 4.17 'same set of block ids' as listed by the owner): a match when the behavior blocks are identical.","severity":"low","snippet":"  const ids = new Set(draft.blocks.filter(b => b.id !== 'custom' && (['token', 'own-token'].includes(b.id) ? draft.type === 'A' : draft.mode !== 'prompt')).map(b => b.id));","title":"Type A drafts can only match a proven entry whose blocks list includes the implicit 'token' id; an entry listing only behavior blocks (the format of the existing entry) never matches"},{"citation":"resolved","description":"The permission is asked once (CHOICE in localStorage), but after it is granted the effect on line 16 adds any launch whose hook page is rendered to the persisted watch list. The page does not check that the viewer opted in for that launch or that they are its payer. LaunchNotifications is rendered on every hook page (Hook.tsx:81). LaunchNotificationDelivery then fires a notification titled '{name} is live' with the body 'Your launch is confirmed. Open its hook page for addresses and controls.' for launches the visitor merely browsed. The 4.17 requirement is a notification when the launch goes live, for the launch the creator asked about.","line":16,"path":"web/src/LaunchNotifications.tsx","reproduction":"1) On hook page A (status in-progress), click 'Enable notification' and grant. 2) Open hook page B, any other in-progress launch from the store or a shared link. Line 16 runs watch(B), and localStorage 'hooks-store:notification-watch:v1' now holds both A and B. 3) When B reaches Live, the browser shows 'B is live — Your launch is confirmed…' to a user who never asked about B and does not own it. Expected: only launches the user opted in for (or launches whose payer is the connected wallet) are watched.","severity":"low","snippet":"    if (supported && Notification.permission === 'granted' && launch.status !== 'live' && launch.status !== 'failed') watch(launch);","title":"Once notifications are granted, every in-progress hook page a visitor opens is watched, and the visitor is told 'Your launch is confirmed' for other creators' launches"}],"hash":"e640e285ee9622b9310612654396b88d53d0c601a4a92c001f39f6c3558eadb8","nodeId":"84ea3aa6-e0b0-4dff-a7b0-5bb9e5ae560c","outcome":"completed","summary":"I found four defects, ranked below. The most serious is that the proven-launch feature (SPEC 4.17) never turns on against the real relay, even though every test passes. The findings are written to `.imd-findings.json`, and no project files were changed.\n\n1. **High: the proven-launch import always fails in production** (`web/src/relay.ts:58`). IMD's `/import` sends back the repository URL with `.git` on the end. `importProvenSource` requires the returned URL to match the entry's URL exactly.\n   - I confirmed this with a live `/import` call for launch 2424 (PondsideGuard), the same read-only call the site makes. It returned `…-pondsideguard.git`, and the function returned `null` when given that exact response. The job 5 evidence shows the same `.git` behaviour.\n   - **Effect:** a draft with exactly the proven block set is always composed from scratch. There is no \"Built from a proven launch\" line, no `repoUrl`/`baseCommit` in the body, and no \"Start from the audited source\" sentence.\n   - The tests miss this because both the Playwright and unit mocks send the requested URL straight back instead of IMD's real response.\n\n2. **Medium: the written-fee detector still writes a 0% fee for some amounts** (`src/lib/writtenFees.ts:83`). These three descriptions each produce `Hook fee rate: 0%`, with no fee sentences and no finding from either check:\n   - `1/100 of every swap goes to the owner.`\n   - `1 pct of every swap goes to the owner.`\n   - `One in every hundred tokens traded goes to the owner.`\n\n3. **Low: Type A drafts can only match an entry that also lists the token block** (`src/lib/proven.ts:32`). The only published entry lists behaviour blocks alone. If the owner adds a Type A entry the same way, no Type A draft can ever match it. No Type A entry exists yet, so the open question is which format entries should use.\n\n4. **Low: notifications go out for other people's launches** (`web/src/LaunchNotifications.tsx:16`). After a visitor grants permission once, every in-progress hook page they open is added to the watch list. They then get \"Your launch is confirmed\" for launches they only browsed.\n\n**What I checked and found correct:**\n- **Owner and address lowercasing:** a checksummed wallet, owner or remainder comes out lowercase in the body and the text. The consistency check refuses mixed case anywhere in the body and refuses an empty owner.\n- **Composed text with a proven launch:** the \"Start from the audited source\" sentence sits right after the first sentence, the rest of the text is kept, and both checks pass.\n- **Which drafts match:** changing the chain or adding a block removes the match, and the creator's own repository overrides it.\n- **Brief's fee examples:** the phrasings listed in the brief now produce fee intent, or a clarification finding where no rate can be attached. This includes \"toll\", \"commission\", \"accrues\", the spelled thresholds and \"payable to IMD's treasury\" (the treasury flows into both the deployer sentence and the footer).\n- **5% cap:** it is applied per direction. For example, a 5% buy fee from a block plus a written 1% sell fee is allowed.\n- **Live reading for contracts-only launches:** it matches the real relay record for launch 2424. The deployer is recognised by its name, and a zero `hook()` gives \"hook not deployed yet\".\n\n**Not covered:**\n- I did not open the site in a browser, so the 320/390/1440 layouts are unchecked.\n- The live SPEC.md upstream is now version 1.37, ahead of the 1.34 the task names. I did not compare the two.","treeHash":null,"usage":{"cachedInputTokens":3002069,"inputTokens":60,"model":"claude-opus-5-5","outputTokens":22062,"runtime":"claude","turns":31,"wallClockMs":272387}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3bed38612db34f32","findings":[{"citation":"resolved","description":"Now: fee intent is recognised only when the clause contains one of a fixed list of trade nouns/verbs (swap, trade, buy, sell and their -ing forms) or fee/charge words; trader nouns such as 'swappers', 'buyers', 'sellers' and the verb 'pay' are in neither list, so a clause with a written amount and a clear payer/payee yields zero fee clauses, and compose() writes 'Hook fee rate: 0%.' (src/lib/compose.ts:229) beside the creator's own words that name 1%. Change to: treat any written amount (percent, bps or spelled) in a clause that names a trade party or a payment (swapper(s), buyer(s), seller(s), trader(s), pay/pays/paid, volume) as fee intent, i.e. extend tradePattern to /\\b(?:swap\\w*|trad\\w*|buy\\w*|sell\\w*|volume)\\b/i and add pay|pays|paid to chargePattern; then the existing writtenFeeAnalysis path either attaches the amount (fee-side, settlement, recipient and partial-fill sentences) or raises the fee-rate clarification. Why: brief carry-over (a): 'any written amount ... near a trade word with no fee block counts as fee intent ... and an amount the parser cannot attach to a charge raises the clarification finding instead of writing zero'. The request contradicts itself (1% in words, 0% in the rule line), so the swarm has to guess, and IMD's checker reads it as an unstated/conflicting rate.","line":66,"path":"src/lib/writtenFees.ts","reproduction":"No browser width (pure composer). Node 24: build a Type B 'Describe it' draft on chain 4663, pool 50, hname 'PondHook', payer 0x6cba4751742f6989fa224b2dc5d5d52fcef98a34, prompt 'Swappers pay one percent to the owner.'; call compose(d, payer) and checkConsistency(body(d, payer), d). Expected: fee-side/settlement/recipient/partial-fill sentences with 1%, or a fee-rate finding. Actual: compose() contains 'Hook fee rate: 0%.' and no 'Fees are always taken' sentence; checkConsistency returns [] (request passes). Same with 'Buyers and sellers pay 1% to the owner.': 'Hook fee rate: 0%.' and only a misleading 'recipient' finding ('Every fee or remainder recipient must be a full, nonzero address.'), no fee-rate finding. Control: 'The owner is paid 1% of every swap.' composes the fee sentences correctly.","severity":"high","snippet":"const tradePattern = /\\b(?:swaps?|trades?|trading|buys?|buying|sells?|selling)\\b/i;","title":"bug: a written amount paid by 'swappers', 'buyers' or 'sellers' still composes 'Hook fee rate: 0%' (written-fee detector is still a word list)"}],"hash":"eee2c343a4413c71ba94f5a393fe532805604084bc24c38626f2273af295fffb","nodeId":"3b81142c-6aca-48c0-92ce-ccb75550d559","outcome":"completed","summary":"The build is close: one high bug remains. Carry-over (a), the structural written-fee detector, is still partly a word list. A request whose words name 1% can still go out saying \"Hook fee rate: 0%\" and pass every check. It's the only finding, and it's in `.imd-findings.json`. I changed no files; scratch scripts and screenshots are under `test/scratch/` only.\n\n| Area | Count |\n|---|---|\n| Brief requirements | 22 met, 1 missing (carry-over a), 0 faked, 0 after review, 0 need the requester |\n| Broken features | 1: the written-fee detector |\n| Product on the first screen at 390 | yes |\n| taste@1 checks passed | 12 of 16. The 4 failures come from the mockup the owner fixed on purpose (SPEC 14.9), so they aren't findings |\n| Copy findings | 0 |\n| Share card | yes (title, favicon, og:title, og:image) |\n\n## Requirements\n- **Owner lowercased (carry-over):** met. `body()` lowercases the owner and `remainderTo` (`src/lib/compose.ts:274`, `:277`). I confirmed it with a checksummed wallet; there's a unit test in `tests/job9.test.mjs`.\n- **Consistency check refuses non-lowercase addresses:** met (`src/lib/safety.ts:68-73`).\n- **Contracts-only launches read Live from the launch record, `hook()` and Completed status 0:** met (`web/src/chain/decode.ts`, `tests/results/job9/hook-live-zero-390.webp`). I didn't run it in a browser myself; I'm relying on the builder's tests and the second seat's review.\n- **(a) Written-fee detector made structural:** **missing.** Verbs like \"receives\", \"route\", \"divert\", \"toll\" and \"commission\" now work. But payer words (\"swappers\", \"buyers\", \"sellers\", \"pay\") still write 0%.\n- **(b) Recipient named in words flows into the deployer sentence and footer:** met. \"payable to IMD's treasury\" puts `0x3f25…1f22` in both.\n- **(c) Spelled thresholds don't count as a fee rate:** met. \"Charge a small hook fee once the price moves one percent\" raises the `fee-rate` finding.\n- **(d) 5% cap per direction:** met. \"Take 3% of every buy. Take 3% of every sell.\" counts 3% each way.\n- **Proven-launch index fetched at load, never bundled, a failure means none:** met (`src/lib/proven.ts:4`, `fetchProvenLaunches`).\n- **Match on type, block set and chain, with Custom allowed:** met (`matchProvenLaunch`). It matches against the live index entry PondsideGuard.\n- **One summary line on the Builder, the Launch page and the hook page:** met (`web/src/Builder.tsx:269`, `LaunchReview.tsx:156`, `Hook.tsx:79`).\n- **The creator's own repository overrides the match, with no other control:** met.\n- **`repoUrl` and `baseCommit` checked through the relay's /import, with a silent fallback:** met (`web/src/relay.ts:53`, `ProvenLaunches.tsx`).\n- **\"Start from the audited source…\" sentence after the first sentence, every other sentence kept:** met. I checked it on Type B: the inserted sentence is the only change and the consistency check passes.\n- **The wait copy on the Builder and the Launch page:** met (`LAUNCH_WAIT`).\n- **Copy link on the hook page:** met (`Hook.tsx:80`).\n- **Ask once for browser notifications:** met (`LaunchNotifications.tsx`).\n- **Tests and results:** met. Composer and import tests exist, Playwright covers the summary and fallback at 320, 390 and 1440, and results are under `tests/results/job9`.\n- **SPEC refreshed to 1.34:** met. Upstream is now at 1.37; the brief asked for 1.34.\n- **Second-seat review:** met (`tests/results/job9/ADVERSARIAL-REVIEW.md`).\n\n**The idea:** a hook store you read in a pixel pond, carried by the live ASCII canvas.\n\n## The finding\n**High, bug: a written amount paid by \"swappers\", \"buyers\" or \"sellers\" still writes \"Hook fee rate: 0%\".**\n- **Where:** `src/lib/writtenFees.ts:66` (`tradePattern`).\n- **Repro (no browser):** Type B, Describe it, chain 4663, prompt \"Swappers pay one percent to the owner.\"\n  - The request says \"Hook fee rate: 0%.\" and has no fee sentences.\n  - The consistency check returns nothing, so the request passes.\n- **Second repro:** \"Buyers and sellers pay 1% to the owner.\" also writes 0%. ","treeHash":null,"usage":{"cachedInputTokens":1034789,"inputTokens":38,"model":"claude-opus-5-5","outputTokens":12370,"runtime":"claude","turns":23,"wallClockMs":479057}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"aad1250d716d3f82","findings":[],"hash":"f1154d0e1a34202ff9c6aa40001253a1499e36b0b82cd9056c6620441d5f6a52","nodeId":"84ea3aa6-e0b0-4dff-a7b0-5bb9e5ae560c","outcome":"completed","summary":"All five findings from last round are fixed. I re-ran each of my original reproductions against the revised code, and I found nothing new that I can reproduce, so `.imd-findings.json` holds `{\"findings\":[]}`. I changed no other file.\n\n**Settled findings**\n\n1. **[high] Token name containing \". \" breaks the proven-launch request — fixed.** The first sentence is now cut at the length it was built to (`firstSentence`, `src/lib/compose.ts`), not at the first \". \" in the line. With 'Dr. Pond' and 'St. Croix', line 1 is now the whole \"Launch Dr. Pond (DRP) … paired with IMD.\" sentence. The \"Start from the audited source…\" instruction comes next, then the token sentence. `checkConsistency(b)` and `validate(d, wallet)` both return `[]`.\n\n2. **[high] 'pay' phrasing with a trader noun wrote a zero fee — fixed.**\n   - **Fee composed correctly:** 'Swappers pay 1 percent.' gives {buy:1, sell:1}. 'Every buyer pays 1%.' gives {buy:1, sell:0}. Both get the fee-side and recipient sentences. 'Buyers pay 2% to the owner.', '1% of each trade goes to the owner.' and 'The owner receives 1% of every swap.' also compose with the right rate and recipient and no finding.\n   - **Rate not attached, but flagged:** 'Sellers are charged 2%.' and 'Buyers pay the owner 1%.' raise a `fee-rate` finding instead of silently writing zero, which is the clarification behaviour job 9 asked for.\n\n3. **[medium] 'for IMD's treasury and the owner' dropped the second destination — fixed.** Both targets are now read. The request says \"Fee recipient: the unresolved written fee recipient.\" and checkConsistency reports `recipient` and `fee-recipient`.\n\n4. **[medium] 'Collect a 2% fee on sells and no fee on buys.' was read as a prohibition — fixed.** The rates are now {buy:0, sell:2} and the fee-side sentences are composed, with no findings.\n\n5. **[low] 'the treasury' was not resolved — fixed.** 'Collect 1% for the treasury.' now gives \"Fee recipient: 0x3f252e859a277a86a3967c11d4e054b2f07a1f22.\" with no findings.\n\n6. **[low] Words-only drafts matched proven entries — fixed.** `matchProvenLaunch` now returns `undefined` for a words-only draft against entries with blocks `[]`, `['custom']` or (type A) `['token']`.\n\n**Extra probes on the revised code, all correct**\n- 'Sellers pay 2% and buyers pay 1%' gives {buy:1, sell:2}, and 'Buyers and sellers pay 1%' gives 1% each way.\n- 'Sellers pay 4% and buyers pay 4%' passes, so the 5% cap is applied per direction.\n- 'Swappers pay 1 percent to IMD's treasury.' sends the fee to the treasury.\n- 'Charge 1% on buys and no fee on sells, paid to the owner.' composes cleanly.\n- 'Take 1% of every swap, or no fee on buys.' raises a `contradiction` finding, which is correct for a self-contradicting rule.\n- A type B proven launch puts the instruction right after the deployer sentence and keeps the rest. The body carries `repoUrl` and `baseCommit`.\n\n**Test run:** 634 of 635 tests pass. The one failure is `tests/job9.test.mjs`, which could not load at all: it imports `viem` from `web/node_modules`, which isn't installed here. I didn't install it because that would write into the repository, so this review did not run that file's tests.","treeHash":null,"usage":{"cachedInputTokens":801836,"inputTokens":26,"model":"claude-opus-5-5","outputTokens":8483,"runtime":"claude","turns":13,"wallClockMs":94632}},{"artifacts":[],"attempt":1,"bundleHash":"d8f064d17a514df521c0a2bc88cdb5d47206d359806ad7f15a47dd8ebb050848","device":"4ece7e789ed37442","findings":[],"hash":"f7709247c71a0555d6377b3713b81028d50a9f0621feb53119063c4a271dd3e2","nodeId":"9e4778fc-dd82-4afb-ac1e-0db645f60966","outcome":"completed","summary":"Implemented job 9, refreshed SPEC 1.34 and rebuilt `dist/`.\n\nBuild/typecheck passed, along with 1,145 unit tests and 234 browser tests; seven optional tests were skipped. Adversarial review findings were fixed.\n\nBundle: **8,370,362 bytes**, within 8 MiB. [Validation and limitations](tests/results/job9/REVIEW.md).","treeHash":"6e8d376eef8b86fd00708ac47228cd19b7aa184d","usage":{"cachedInputTokens":23313664,"inputTokens":340051,"model":"gpt-6.1-sol","outputTokens":116830,"runtime":"codex","turns":14,"wallClockMs":3808341}},{"artifacts":[],"attempt":1,"bundleHash":"23239ad3ed577382baa58717a6c88ed9d068d60f95620eabaf82e699cf614fb7","device":"ee8bab8367ea6e91","findings":[],"hash":"fac142fb4b1ea480ab6ffc090c86d8b2949dfedf7c8bd2c5e6b91e6510743fce","nodeId":"9e4778fc-dd82-4afb-ac1e-0db645f60966","outcome":"completed","summary":"I reproduced the reported bug, fixed it, and wrote the answer to `.imd-responses.json` with the verdict \"fixed\".\n\n**What was wrong:** the fee-destination reader in `src/lib/writtenFees.ts` didn't treat \"The owner is paid 1% of every swap.\" as naming a destination. With an owner different from the paying wallet, the request named the owner as owner but named the payer as fee recipient, in both the deployer sentence and the fee footer. Neither check caught it. \"The owner gets 1%…\" and \"The owner takes 1%…\" failed the same way.\n\n**The fix:** when a known destination (the owner, my wallet, the treasury or a full address) is followed by a payment verb, it now counts as the written destination. That covers passive forms like \"is paid\", \"is credited\" or \"is owed\" and active ones like \"receives\", \"gets\", \"earns\", \"takes\", \"keeps\" or \"collects\". The existing code already turns \"the owner\" into the owner's address, so the deployer sentence and the footer now both name the owner. If the payee has no address, as in \"Alice is paid 1% of every swap.\", the request is refused with a \"recipient\" finding rather than quietly paying the wallet. Sentences like \"The fee is paid in ETH.\" still don't name anyone.\n\n**Tests added** in `tests/job9-revision.test.mjs`:\n- Four owner-payee wordings across launch types A, B and C, using different payer and owner addresses.\n- The unknown-payee case, which must be refused.\n\n**Results**, recorded in `tests/results/job9/revision/`:\n\n| Check | Result |\n| --- | --- |\n| Typecheck and production build | passed (the old large-chunk warning is still there) |\n| Unit tests | 1,155 of 1,155 passed |\n| Playwright | 234 passed, 7 optional skips |\n| The real `launchBody()` | owner in both fee-recipient spots; refuses the \"Alice\" prompt |\n| Export integrity | complete metadata, no missing or absolute asset links |\n| 8 MiB bundle budget | passed, about 600 KB to spare |\n\nThe screens didn't change, so I didn't rerun the design probe. `dist/` was rebuilt; only the bundle's file name changed.\n\n**Things to know:**\n- The reorg tests in `tests/chain.test.mjs` sometimes miss their 1-second timing window on this machine, with or without my change. In about half of full runs one of the four fails; they passed in the recorded run. I noted this in `tests/results/job9/REVIEW.md` and left those tests alone.\n- I installed dependencies and the Playwright browser locally to run the checks. Running Playwright overwrote screenshots from earlier jobs, so I restored them, and I deleted `web/node_modules` afterwards.\n- I haven't committed anything.\n\nNot delivered: artifacts/builder-1440.png, artifacts/builder-320.png, artifacts/builder-390.png, artifacts/cast-frames.json, artifacts/hook-1440.png, artifacts/hook-320.png, artifacts/hook-390.png, artifacts/hook-manage-1440.png and 11 more. Under artifacts/, only this step's named outputs leave the machine.","treeHash":"7043b7fce344976eb048ab68be4d29a5a5f78f2e","usage":{"cachedInputTokens":2542520,"inputTokens":82,"model":"claude-opus-5-5","outputTokens":17930,"runtime":"claude","turns":42,"wallClockMs":927490}}],"verification":[{"checks":[],"detail":"paths and tree verified; no suite was run for this kind of work","evaluation":"structural","profile":"none","status":"accepted","submissionHash":"4c1b16f671892608c0d99cb7661f09713b71c65d6263f7b2ea83af9aeaceee7d","verifiedTreeHash":"0e866ba60ab74f3c8c9205d88182bc0ecb2170f5","verifierVersion":"0.1.0+560ab6a2"},{"checks":[],"detail":"paths and tree verified; no suite was run for this kind of work","evaluation":"structural","profile":"none","status":"accepted","submissionHash":"f7709247c71a0555d6377b3713b81028d50a9f0621feb53119063c4a271dd3e2","verifiedTreeHash":"6e8d376eef8b86fd00708ac47228cd19b7aa184d","verifierVersion":"0.1.0+560ab6a2"},{"checks":[],"detail":"paths and tree verified; no suite was run for this kind of work","evaluation":"structural","profile":"none","status":"accepted","submissionHash":"fac142fb4b1ea480ab6ffc090c86d8b2949dfedf7c8bd2c5e6b91e6510743fce","verifiedTreeHash":"7043b7fce344976eb048ab68be4d29a5a5f78f2e","verifierVersion":"0.1.0+560ab6a2"}]}