{"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":"ca404dfe-00d1-4682-83cb-fa1b28f9826c","kind":"audit","nodes":[{"acceptedSubmissionHash":"d1fa0010a26900d46716adaf30f09328d661b7ea3fc36b702abb5fb214eb82c9","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"2152c2898ca9ffcd009ac72edae2dd51afa89a6c672ab82a5e49e7c16b231b60","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"e8142def83ae60132b12bdb6824bdddf46b1c7bd207f4d83a096d254ced0d79b","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"4c6360a9dca13e0f31b8735a85e550194106f60e9057c761de4994595cec736f","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"af1d1176a53fce4fba967d1a12d75980f892da254e559027ba4ff45726eb1c7f","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit commit cc847d8f35bc0b814d390eb76092eed6051c5ae3. Focus on the oracle-gated generated-hook deployer and its integration, including changes since 2aef6408eabb45b8f149e16a96c7734d4b4b5b07. Review only; no deployment, paid Intake calls or source fixes.\n\nPrimary scope: GatedHookDeployer.sol, AcceptedArtifact.sol, ResultJson.sol, the signer-epoch change in HookJobRequester.sol, SDK/templates and deployment configuration.\n\nAttack the complete authorization chain: authenticated resultHash -> exact IMD completion snapshot -> unique accepted source/approval file -> exact artifact -> correctly bound signed verdict. Test duplicate/nested/escaped fields, parser limits and gas, hash/metadata mismatches, source/build substitutions, forged/unbound/missing/failed reviews, and replay. Check CREATE2, constructor/reentrancy, runtime/manager/permission readback and every deployment entry point.\n\nVerify signer provenance and revocation in BOTH callback/binding orders, including missing epochs, rotation and preserved historical verdicts. Preserve the 200k callback limit and 24,576-byte runtime limit.\n\nUse docs/checks/approval-binding and reproduce the regressions. Independently verify the real IMD schema and declared trust assumptions; fixtures/free request checks are not live delivery evidence. Automatic binding is explicitly disabled pending an authenticated protocol mapping proof: do not certify a founder-free or deployment-ready flow.\n\nReturn severity-ranked findings with file/line, runnable reproductions and material audit blockers. Separate code safety, owner/oracle trust, protocol gaps and untested live behavior.","parentJobId":null,"planHash":"f26cef6f82b4387c0b79d63e6dae12ed00d9be94063936b279da578150279111","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"ca404dfe-00d1-4682-83cb-fa1b28f9826c","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52173","feedbackHash":"84a7ab482d672437c37bcb82c00130a588bbfacbee4c619aea23ec2d5d87165e","nodeKey":"audit_economics","submissionHash":"d1fa0010a26900d46716adaf30f09328d661b7ea3fc36b702abb5fb214eb82c9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51445","feedbackHash":"2288cc418b4431a3afbcb642f1ff2fef04d3860ade3521d37dc8b8c62420747c","nodeKey":"audit_flow","submissionHash":"2152c2898ca9ffcd009ac72edae2dd51afa89a6c672ab82a5e49e7c16b231b60","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51018","feedbackHash":"25a89575b6972744d2cddd0e6bba038f8113b8829da063735fe7455165af63b1","nodeKey":"audit_judge","submissionHash":"e8142def83ae60132b12bdb6824bdddf46b1c7bd207f4d83a096d254ced0d79b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52481","feedbackHash":"d4701b27b6a0231952066a7c29f7027d27f6b036e903a94bfcebbcfbca48da5c","nodeKey":"audit_math","submissionHash":"4c6360a9dca13e0f31b8735a85e550194106f60e9057c761de4994595cec736f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51540","feedbackHash":"bb01965fa6f16bd8b5889262183c5c1b729a5ce1bdbf23b6b6e8c4255ac1cffa","nodeKey":"audit_permissions","submissionHash":"af1d1176a53fce4fba967d1a12d75980f892da254e559027ba4ff45726eb1c7f","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"b569bdad24cdc4a7c3720380771fc261b26de1bfabe4432f28e41794951af4da","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa8fc4653a9e883d","findings":[{"citation":"resolved","description":"Code safety / liveness. After a review finalizes as Passed (bind-first via onReviewResult's matching path, or receipt-first via bindReview), `reviewOf[ask]` is deleted and `job.review` is terminal. If the owner later rotates the signer and calls `revokeSignerEpoch(oldEpoch, incident)`, GatedHookDeployer correctly refuses the deployment (`ReviewRevoked`), as intended. But the requester contract offers no way out: `closeRevokedReviewReceipt(ask)` reverts `UnknownOrSettled` because `reviewOf[ask]` was deleted at finalization, and `requestReview(job)` reverts `NotReviewable` because `job.review == Passed`. The paid job therefore can never be re-reviewed under the new signer; the creator must open and pay for a brand-new job (job.open + build/test/review chain + oracle.request) even though the delivered code and snapshot are unchanged. The revocation design note says 'Finalized verdicts stay historical', but it does not provide the recovery that a compromised-epoch incident requires. Expected: an owner incident path that marks a finalized verdict from a revoked epoch as Unanswered (keeping history), so the requester can `requestReview` again under the new epoch. Actual: both recovery entry points revert; the job is stuck forever. Minimal fix: extend `closeRevokedReviewReceipt` (or add a sibling) to accept a finalized ask whose `reviewReceiptSignerEpoch` is revoked when `jobs[job].reviewRequestId == ask` and `review == Passed/NotPassed`, setting `review = Unanswered` and emitting the revocation events, without touching `consumed` or the deployer's replay maps.","line":386,"path":"src/imd/HookJobRequester.sol","reproduction":"Foundry (uses the repo fixture GatedHookDeployerTest): c = _generic(FLAGS); ask = _pass(c.jobId, true) [or _passReceiptFirst]; as owner: requester.setOracleSigner(newSigner); requester.revokeSignerEpoch(1, keccak256('incident')); then requester.closeRevokedReviewReceipt(ask) reverts UnknownOrSettled(ask); as creator: requester.requestReview(c.jobId, 0.5 ether) reverts NotReviewable(c.jobId); deployer.deploy(...) reverts ReviewRevoked(ask). Expected: some owner/requester path to obtain a fresh verdict for the same delivered job. Actual: none exists; confirmed by test/scratch/Leads.t.sol testRevokedEpochPassedJobHasNoRecovery and testRevokedEpochReceiptFirstPassedJobHasNoRecovery (both pass, i.e. the dead end is reproduced).","severity":"medium","snippet":"        if (\n            (job.status != JobStatus.Delivered && job.status != JobStatus.Failed && job.status != JobStatus.Expired)\n                || (job.review != ReviewStatus.None && job.review != ReviewStatus.Unanswered)\n        ) revert NotReviewable(jobRequestId);","title":"Finalized Passed verdict under a revoked signer epoch is a dead end: no re-review path, job permanently undeployable in both callback orders"},{"citation":"resolved","description":"Code safety / gas. GatedHookDeployer.MAX_RESULT_DOCUMENT_BYTES (65,536) and docs/GATED_DEPLOYMENT.md advertise a 64 KiB, 2,048-token snapshot as supported. Measured on this commit with the project's compiler settings (0.8.26, optimizer 200): ResultJson.parse of a 1,000/2,000/4,000-byte single-string document costs 504k/1,028k/2,183k gas (~500 gas/byte in _string's byte loop), and a 3,927-byte document with 1,938 numeric tokens costs 10.1M gas (~5k gas/token). A 65,536-byte, 2,048-token snapshot that otherwise passes every gate makes GatedHookDeployer.check cost 34.9M gas and deploy 35.2M gas. Ethereum mainnet's block gas limit at review time is 60M, so the transaction fits but costs ~35M gas; most public RPC providers cap eth_call at 25-50M, so sdk/gated_deploy.py's mandatory `check` eth_call and the `mine` pagination fail before any transaction can be built. Nobody but the control plane controls snapshot size (realistic snapshots are 1-3 KB: the real previous-job-result.json is 903 bytes / 52 tokens and parses for ~1.2M gas), so this is not attacker-triggerable; it is a cost/availability gap between the documented bound and what is deployable. Fix: scan strings word-at-a-time in assembly (or cache `d.data` pointer/length in locals and avoid struct-member round trips per byte), and lower MAX_RESULT_DOCUMENT_BYTES to a value whose worst case was measured (e.g. 8 KiB ~ 3.8M gas).","line":153,"path":"src/deployment/ResultJson.sol","reproduction":"test/scratch/Leads.t.sol:testMaxSnapshotGas builds a valid 65,536-byte snapshot (fixture source/file entries plus 15 padding arrays of 128 numbers and a filler string), delivers keccak256 of it as resultHash, passes the review, then logs: check gas (max doc): 34,897,311; deploy gas (max doc): 35,165,362. test/scratch/Micro.t.sol:testMicro logs parse gas 503,924 / 1,027,525 / 2,182,618 for 1,000 / 2,000 / 4,000-byte string documents. Expected: a documented-maximum snapshot deploys at a cost comparable to the fixture case (~1-2M gas). Actual: ~35M gas.","severity":"low","snippet":"            bytes1 c = d.data[d.pos++];","title":"Snapshot parser costs ~500 gas per string byte and ~5k gas per token: a snapshot near the documented 65,536-byte ceiling needs ~35M gas for check()/deploy(), above common eth_call caps"},{"citation":"resolved","description":"Untested live behaviour / availability. The parser rejects every string containing a backslash or any byte outside 32..126, at any depth, including fields the gate never reads (`delivery.failure`, `delivery.repoUrl`, `launch`, `source[].url`, `files[].url`). The snapshot bytes are fixed by the Intake's resultHash and cannot be normalized, so a single escaped quote in a GitHub delivery error message, a non-ASCII character in a path, or a `\\/`-escaped URL produced by a different serializer makes the job's approval unusable forever (ArtifactNotInResult/InvalidResultJSON on every deploy), with no retry other than a new paid job. The one real snapshot in the tree (docs/checks/approval-binding/sources/previous-job-result.json, 903 bytes) parses (52 tokens), but it has `files: []` and `failure: null`; no completed snapshot with a non-empty `files` array, a delivery failure or a pretty-printed/UTF-8 body has been observed, and docs/checks/approval-binding/README.md itself lists 'actual completion encoding' as an unresolved gate. Expected: the gate's result should depend only on the fields it binds (`jobId`, `state`, `complete`, `source`, `files`). Actual: it depends on the byte-level encoding of every unrelated field. Fix options: tolerate JSON escapes inside strings that are not compared (skip `\\x` pairs and bytes >= 0x80 when scanning a string token, still refusing escapes in compared keys/values), or obtain a first-party guarantee of the serializer's output (ASCII-only, no escaped slashes) and record it as a trust assumption.","line":158,"path":"src/deployment/ResultJson.sol","reproduction":"Take any passing fixture snapshot (test/GatedHookDeployer.t.sol _doc) and append `,\"delivery\":{\"requested\":true,\"failure\":{\"message\":\"push rejected: \\\"main\\\" is protected\"}}` before the closing brace, or `,\"delivery\":{\"repoUrl\":\"https:\\/\\/github.com\\/x\"}`, deliver keccak256 of those bytes as resultHash and pass the review. Expected: deploy succeeds (the bound fields are identical). Actual: ResultJson.parse reverts InvalidResultJSON at the first backslash, so GatedHookDeployer.deploy reverts for that job permanently. AcceptedArtifactTest.testDuplicateKeysAnywhereAndEscapedAliasesFailClosed already shows the `\\\\n` case reverting. Confirmed on this commit by test/scratch/Leads.t.sol EscapeTest.testEscapedDeliveryFailureBlocksApprovedArtifact: with review Passed and the snapshot hash delivered, deploy reverts InvalidResultJSON().","severity":"low","snippet":"            if (uint8(c) < 32 || uint8(c) > 126 || c == \"\\\\\") revert InvalidResultJSON();","title":"Any escaped or non-ASCII byte anywhere in the live completion snapshot (e.g. a delivery failure message or URL) permanently blocks an otherwise approved artifact; live snapshot encoding is untested"},{"citation":"resolved","description":"Protocol gap / untested live behaviour (not a code defect). HookJobRequester.openJob assembles the body as prefix ++ 0x<specHash> ++ middle ++ brief ++ suffix (src/imd/HookJobRequester.sol:309). The Intake request id is only returned by intake.requestWithFailure after the body is fixed, so it can never be in the body; the deployer address, chain id, PoolManager, its code hash and the HookSpec itself are not in the body either (only the spec hash is), and the template declares no `inputs`. The research-report step is told to compute HookArtifact 'per the supplied GatedHookDeployer ABI' with 'jobRequestId is the originating Intake job id', yet nothing supplies them: the worker must discover the Intake id from chain logs (JobOpened's requestId matched by bodyHash) or from the unauthenticated HTTP /intake/:chainId/:txHash record, and must be told the deployer address out of band. If any parameter is wrong the approval names a digest the deployer will never accept; if the worker follows the 'emit no passed approval' rule, no approval is ever produced. docs/GATED_DEPLOYMENT.md acknowledges 'supply ... via the actual job inputs' as future work. Declared trust assumptions checked against the captured docs hold (SHA-256 file/bundle hashes, keccak-256 resultHash over /intake-results/:id bytes, accepted-only `files`), but no completed job with a non-empty `files` array exists in the evidence; fixtures/ and the free request checker are not delivery evidence; automatic binding is disabled by design and must not be certified as founder-free or deployment-ready.","line":21,"path":"sdk/request_templates.py","reproduction":"State: deploy HookJobRequester with deployment/hook-templates.json fragments; call openJob(specHash, brief, max). Inspect the emitted JobOpened body: it contains the spec hash and brief only. Expected (per the research-report objective): the worker can read jobRequestId, deployer, chainId, poolManager and permissions from its inputs. Actual: none of these appear in the body or in any declared input, so the approval digest cannot be derived from the job's own data; the end-to-end path resultHash -> accepted approval file -> artifact has never been exercised against a live job (docs/checks/approval-binding/README.md:82).","severity":"info","snippet":"         'objective': 'Publish evidence only after the independent adversarial review is accepted with no unresolved findings. Identify the exact earlier accepted source bundle reviewed, so its SHA-256 does not include this report. Reproduce the build. Compute HookArtifact per the supplied GatedHookDeployer ABI: jobRequestId is the originating Intake job id; sourceHash is that accepted bundle SHA-256; buildHash is keccak256 of the exact build identity file; bind creation code, constructor args, runtime, chain, deployer, manager/code hash and permissions. Emit hooksStoreApproval as the exact approvalDocument(artifact) bytes without a newline, and evidence with the source submission, build identity, bytecode and independent review submission references. If any fact or parameter is missing, or review failed, emit no passed approval and state why. Never invent parameters, attestations, a review, a top-level completion field or a source hash.',","title":"Protocol gap: the job body gives the publishing worker no Intake request id, deployer, chain or manager parameters, so the artifact it must commit to cannot be computed from the job alone (no live app"},{"citation":"resolved","description":"Trust assumptions, documented separately as the task requires; none is a permission bypass. (1) Deployer owner: setMaxJobAge(1 days) makes every job older than one day revert ApprovalStale (jobs take up to jobTimeout 7 days + review), and setManager(manager, hash, false) reverts ManagerNotEnabled for every artifact on that manager; both are reversible and emit events, but they let the owner veto any creator's deployment. (2) Requester owner: bindReview(intakeId, bodyHash, oracleRequestId, questionHash, chainId, from, to) is owner-asserted context; the oracle attestation signs the oracle UUID and questionHash but not the Intake id or body, so an owner who binds a Passed answer for a different question to a job makes that job deployable; conversely the owner can refuse to bind forever. (3) Intake: onJobResult is unsigned beyond msg.sender; whatever keccak the Intake delivers becomes the only snapshot the gate accepts. (4) Oracle panel: the contract verifies that the accepted approval file names the artifact and that a source bundle with the claimed SHA-256 was accepted; whether the bytecode was built from that bundle, whether the review was independent, and whether 'no unresolved findings' holds are entirely the panel's judgment. The pinned PoolManager code hashes for Ethereum, Base and Sepolia were re-read live during this review and match the constants; the Robinhood chain (4663) pin could not be checked.","line":163,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"(1) As deployer owner on any chain: setMaxJobAge(1 days); then for any job with openedAt < now - 1 day, deploy reverts ApprovalStale(openedAt, 86400) although review Passed. (2) As requester owner: after requestReview for job J, call bindReview with the UUID/questionHash of an unrelated Passed oracle answer; onReviewResult's matching path then finalizes J as Passed and GatedHookDeployer accepts it. Expected per design: these are accepted operator powers; they are listed here so the requester can decide whether multisig/timelock control is required before a paid pilot.","severity":"info","snippet":"    function setMaxJobAge(uint64 age) external onlyOwner {","title":"Owner and oracle trust assumptions: deployer owner can stall every deployment; requester owner's bindReview is the only tie between a signed answer and a job; Intake alone fixes resultHash"}],"hash":"2152c2898ca9ffcd009ac72edae2dd51afa89a6c672ab82a5e49e7c16b231b60","nodeId":"c8c21ac5-c243-4cdc-b799-0d44aee353f7","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` (five entries, validated against the tree). The working tree shows no modified tracked files; only the untracked scratch tests and the findings file were added.\n\n**Outcome.** The oracle-gated deployer's authorization chain holds up under code-level attack: I found no permission bypass, replay, parser smuggling, or substitution path. The concrete defects are a liveness dead end after signer-epoch revocation, parser cost, and encoding brittleness, plus protocol gaps that mean the live flow remains unproven.\n\n**Findings, ranked**\n\n- **Medium, HookJobRequester.sol:386.** A Passed verdict finalized in either callback order, whose epoch is later revoked, is stuck forever. `closeRevokedReviewReceipt` reverts `UnknownOrSettled` because `reviewOf` was deleted at finalization, and `requestReview` reverts `NotReviewable`. The deployer correctly refuses with `ReviewRevoked`, but the creator's only remedy is a new paid job. Reproduced for both orders in test/scratch/Leads.t.sol.\n- **Low, ResultJson.sol:153.** The parser costs about 500 gas per string byte and 5k per token. A snapshot at the documented 64 KiB ceiling makes `check` cost 34.9M gas and `deploy` 35.2M. That fits today's 60M mainnet limit but exceeds common eth_call caps, so the SDK's mandatory pre-check fails. Only the control plane controls snapshot size, so this is cost, not an attack.\n- **Low, ResultJson.sol:158.** Any backslash or non-ASCII byte anywhere in the snapshot, including fields the gate never reads such as a delivery failure message, permanently blocks an approved artifact. Reproduced with an escaped quote in `delivery.failure`.\n- **Info, sdk/request_templates.py:21.** The job body carries only the spec hash and brief. The Intake request id does not exist when the body is built, and no deployer, chain or manager parameters are declared as inputs, so the publishing worker cannot derive the artifact from the job's own data. No live job with a non-empty `files` array exists in the evidence.\n- **Info, GatedHookDeployer.sol:163.** Trust assumptions documented separately: deployer owner can stall all deployments via `setMaxJobAge` or `setManager`; requester owner's `bindReview` is the only link between a signed answer and a job; Intake alone fixes `resultHash`; the panel alone judges build provenance.\n\n**What was verified clean.** Duplicate, nested, escaped and whitespace-varied fields fail closed. Hash, byte-count and media-type mismatches, source and build substitution, forged, unbound, missing, failed and replayed reviews all revert. Signer provenance is retained in both orders, rotation preserves historical verdicts, and missing epochs refuse. CREATE2 prediction, constructor reentrancy, reverting and self-destructing constructors, and runtime, manager and permission readback are covered. The real previous-job snapshot parses. The Ethereum, Base and Sepolia PoolManager pins match live code hashes; the Robinhood chain pin could not be checked. The published ABI matches the compiled contract, build fingerprints verify unchanged, and 126 Foundry plus 49 SDK tests pass.\n\n**Blockers for a deployment-ready claim.** Automatic binding is disabled by design and should stay uncertified. No live completion with an accepted approval file has been observed, so the snapshot encoding and the worker's ability to compute the artifact remain untested.","treeHash":null,"usage":{"cachedInputTokens":3153375,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":58728,"runtime":"claude","turns":45,"wallClockMs":879128}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a4e285247c6f8aed","findings":[{"citation":"resolved","description":"The signer-epoch change makes GatedHookDeployer._checkApproval (src/deployment/GatedHookDeployer.sol:275-278) refuse any job whose verification epoch was revoked, in both the bind-first and receipt-first orders. That is correct for safety, but nothing restores liveness afterwards. The job stays `review == Passed`, so `requestReview` reverts NotReviewable (line 388); `closeRevokedReviewReceipt` reverts UnknownOrSettled because `_answerReview` already deleted `reviewOf` (and in the bind-first order there is no early receipt at all); `discardMismatchedReviewReceipt` has the same `reviewOf` guard. The requester, who paid for the job and the review, has no way to buy a fresh verdict from the new signer; the only recourse is a brand-new paid job. An epoch revocation is an owner incident action that can be entirely unrelated to this job's honest verdict (e.g. a key leaked after the verdict), so an honest, already-reviewed artifact becomes undeployable forever. Expected: after revocation the owner (or the requester) can reset the review to Unanswered so a new ask under the current epoch can re-establish provenance, preserving the historical verdict in events. Actual: permanent dead end. Minimal fix: add an owner-only `closeRevokedVerdict(jobRequestId)` that requires `revokedSignerEpochs[reviewReceiptSignerEpoch(job.reviewRequestId)] != 0` and `job.review == Passed || NotPassed`, sets `job.review = Unanswered` (keeping events and consumed UUIDs), so `requestReview` becomes possible again; GatedHookDeployer keeps rejecting the old ask because `job.reviewRequestId` moves to the new ask.","line":388,"path":"src/imd/HookJobRequester.sol","reproduction":"test/scratch/LivenessProbe.t.sol (built on GatedHookDeployerTest): (1) open+deliver a job, `_pass(jobId,true)` (bind-first) or `_passReceiptFirst(jobId)`; (2) owner: `setOracleSigner(newSigner)` then `revokeSignerEpoch(1, keccak256(\"incident\"))`; (3) `deployer.deploy(...)` reverts ReviewRevoked(ask) as intended; (4) creator: `requester.requestReview(jobId, 0.5 ether)` reverts NotReviewable(jobId); (5) owner: `closeRevokedReviewReceipt(ask)` reverts UnknownOrSettled(ask) in both orders. Expected after step 3: some path to a fresh review. Actual: none; `getJob(jobId).review` stays Passed while deployment is blocked forever.","severity":"medium","snippet":"                || (job.review != ReviewStatus.None && job.review != ReviewStatus.Unanswered)","title":"[protocol gap] Revoking a signer epoch permanently bricks every finalized Passed job of that epoch: no re-review or closure path exists in either callback order"},{"citation":"resolved","description":"`maxJobAge` (default 30 days, owner-settable 1..180 days) is compared against `job.openedAt`. HookJobRequester allows `jobTimeout` up to MAX_TIMEOUT = 30 days and explicitly accepts late Intake deliveries after local expiry (`onJobResult` accepts Open or Expired), the review ask has a 24-hour oracle window plus `reviewTimeout = 1 days`, and ordinary re-asks after an Unanswered review add more days. None of that time is subtracted, so the real deployment window is `maxJobAge - (delivery delay + review latency)`, which can be zero or negative with legal parameters, and with the allowed `setMaxJobAge(1 days)` no job can ever deploy because a 24-hour oracle window alone consumes it. The gate then wastes a paid job plus a paid review for a timing reason unrelated to safety of the artifact. Expected: staleness bounds the age of the *approval* (time since the review verdict / since delivery) or at least guarantees a minimum window after the verdict. Actual: bound is from `openedAt`. Minimal fix: record the verdict timestamp in `_answerReview` (or use `job.reviewDeadline`/`reviewAsk.askedAt`) and apply `maxJobAge` from that time, keeping an absolute upper bound from `openedAt` if desired (e.g. `openedAt + MAX_JOB_AGE`).","line":280,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"test/scratch/LivenessProbe.t.sol::testStaleIsMeasuredFromOpenNotVerdict: open job at T; `vm.warp(T + 30 days - 1 hours)`; Intake delivers (`intake.complete(jobId, abi.encode(jobId, keccak256(doc), uri))`), `_pass(jobId,true)` so `getJob(jobId).review == Passed`; `vm.warp(T + 30 days + 1)`; `deployer.deploy(a, doc, code, args, salt, hook)` as the requester reverts `ApprovalStale(openedAt, 2592000)`. Expected: a verdict that is one hour old is deployable. Actual: refused; with `jobTimeout = 30 days` and a delivery at `openedAt + 30 days` the window is exactly zero seconds.","severity":"low","snippet":"        if (block.timestamp > uint256(job.openedAt) + maxJobAge) revert ApprovalStale(job.openedAt, maxJobAge);","title":"[code safety / boundary] Approval staleness is measured from job open, not from the verdict, so a job delivered late in its window can be undeployable the moment it passes review"},{"citation":"resolved","description":"`_source` calls `member(cursor,\"hash\")` and `hashValue` on every `source` entry, and `_approval` (lines 76-77) calls `member(cursor,\"name\")`/`member(cursor,\"path\")` on every `files` entry, before deciding whether the entry is the one being looked for. ResultJson additionally rejects the entire document if any string anywhere contains a byte outside 32..126 or a backslash (src/deployment/ResultJson.sol:158). The snapshot is produced by the control plane from every accepted step of the four-step chain, is immutable, and its keccak256 is pinned by the Intake callback, so the requester cannot repair it. Hence an entry the gate does not even need can make the approval permanently unusable: a step whose accepted entry has `\"hash\":null` (a review step that publishes no bundle), an extra accepted file without `name`, or any accepted file/report whose `path`, `url` or `repoUrl` is serialized with a `\\u00e9`/`\\/` escape or a raw UTF-8 byte. The documented IMD schema does not promise ASCII-only serialization, and the project's own README lists 'actual completion encoding' as an unresolved live gate. Expected: entries that are not the approval file / not the target source are skipped; only the matching entries are strictly validated. Actual: whole-document revert (InvalidResultJSON / InvalidArtifactEvidence), deployment impossible, paid review wasted. Minimal fix: in `_source`/`_approval` look up keys with a non-reverting `tryMember` and skip entries whose `hash`/`name`/`path` is absent or not a string; accept JSON string escapes for keys/values that are only compared by equality (or at least tolerate `\\/` and `\\uXXXX` in strings that are never compared), keeping strictness for the fields actually matched.","line":56,"path":"src/deployment/AcceptedArtifact.sol","reproduction":"test/scratch/MathProbe.t.sol: (a) testSourceEntryWithNullHashRevertsWholeSnapshot: snapshot = valid completed snapshot with the correct accepted source and the unique correct hooksStoreApproval file, plus a second source entry `{\"node\":\"adversarial_review\",\"submissionHash\":\"11..11\",\"hash\":null}`; `AcceptedArtifact.verify(snapshot, DIGEST, SOURCE)` reverts InvalidResultJSON instead of returning true. (b) testFileWithoutNameRevertsWholeSnapshot: same valid snapshot plus a second accepted file `{\"path\":\"artifacts/report.md\",\"hash\":\"ac68...\",\"bytes\":1,\"mediaType\":\"text/markdown\",\"submissionHash\":\"11..11\"}` (no `name`); verify reverts InvalidResultJSON. (c) existing test testDuplicateKeysAnywhereAndEscapedAliasesFailClosed shows `,\"note\":\"escaped\\\\ntext\"` anywhere in the document reverts. Expected in all three: true (the approval entry is unique, exact and accepted). Actual: revert, and `GatedHookDeployer.deploy` reverts with the same error for that job forever.","severity":"low","snippet":"            if (d.hashValue(d.member(cursor, \"hash\")) == sourceHash) {","title":"[code safety / liveness] One unrelated accepted entry in the snapshot (null source hash, file without name, or any non-ASCII/escaped byte) reverts the whole gate for an otherwise valid approval"},{"citation":"resolved","description":"`_uniqueKey` rehashes every previous key of the object for each new key (O(n^2) keccak + token reads, n <= 128), and the document may hold up to 2048 tokens (`parse`, line 28) in 65,536 bytes. Measured with the probe harness: a 1,202-byte real-shaped snapshot verifies in 1,015,540 gas; a 2,404-byte four-source/two-file snapshot deploys end to end in 2,333,464 gas; a 65,535-byte snapshot with one long string verifies in ~8.3M gas; a syntactically valid 8,686-byte snapshot with six 128-key objects (1,615 tokens) parses in 35,649,941 gas. Seven such objects (the 2048-token limit) extrapolate to ~41M gas, above Ethereum's 36M limit and far above the 100k-gas scale of the rest of the gate. This is not an attacker lever: only the job's own immutable snapshot (hash pinned by the Intake callback) is parsed, and the caller pays. It matters only if the control plane ever emits wide objects, which the documented result schema does not. Reported so the bound is explicit; a cheaper structure (hash-set over key hashes, or checking uniqueness only for the top-level members the gate reads) would make the cost linear.","line":137,"path":"src/deployment/ResultJson.sol","reproduction":"test/scratch/MathProbe.t.sol::testDenseTokensGas: snapshot = valid completed snapshot plus six extra top-level members `\"o<i>\":{\"k<i*1000+j>\":0, ... 128 keys}`; `ResultJson.parse` returns count 1615 and consumes 35,649,941 gas (logged); `AcceptedArtifact.verify` on the same bytes consumes 35,843,133 gas. testRealShapeVerifies logs 1,015,540 gas for the 1,202-byte real-shaped document. Expected: parse cost roughly linear in bytes; actual: dominated by the per-object quadratic key scan.","severity":"info","snippet":"        while (previous < key) {","title":"[gas / boundary] ResultJson duplicate-key scan is quadratic per object: a 2048-token snapshot costs ~40M gas to parse, above several chains' block gas limit; real-shaped snapshots cost ~1M"},{"citation":"resolved","description":"This is the design's declared trust assumption, recorded here so the deployment decision is explicit, not a permission bypass. Every input the gate trusts is set by one role: `setIntake` (line 877) chooses who may call `onJobResult`/`onReviewResult`; `setOracleSigner` (line 898) chooses whose signature makes a verdict `Passed`; `bindReview` supplies the oracle UUID/question/window the callback is matched against; `setJobTemplate`/`setReviewTemplate` define what question is asked. GatedHookDeployer only reads `getJob`, `revokedReviewReceipts`, `reviewReceiptSignerEpoch` and `revokedSignerEpochs` from the immutable requester, and its own owner controls `setManager` on non-pinned chains and `setMaxJobAge`. Therefore a malicious or compromised requester owner can: deploy a contract that implements IIntake, `setIntake` to it, have it call `onJobResult(jobId, keccak256(anySnapshot), uri)`, `setOracleSigner` to a key they hold, `bindReview` to any UUID, and deliver a self-signed Passed attestation, after which any artifact named by `anySnapshot`'s approval file deploys. The test fixture (MockIntake + local signer key) performs exactly these steps, which shows the capability concretely. The repository documents this in docs/GATED_DEPLOYMENT.md and `automaticBindingSupported()` returns false; no founder-free or deployment-ready flow should be certified until the requester owner is a multisig/timelock and the oracle signer is the protocol's published signer with an authenticated Intake-to-oracle binding proof.","line":898,"path":"src/imd/HookJobRequester.sol","reproduction":"Owner-controlled chain, any chain: (1) owner deploys `MockIntake`-like contract M and calls `requester.setIntake(M)`; (2) creator opens a job through M; (3) M calls `requester.onJobResult(jobId, keccak256(snapshot), \"\")` with a snapshot whose `files` names `approvalDocument(artifact)` by SHA-256 and whose `source` carries `artifact.sourceHash`; (4) owner calls `setOracleSigner(ownerKey)`; creator `requestReview`; owner `bindReview(ask, bodyHash, uuid, qh, chainId, 1, 2)`; M calls `onReviewResult(ask, attestation{requestId:uuid, questionHash:qh, ...}, sign(ownerKey))`; (5) `deployer.deploy(artifact, snapshot, code, args, salt, hook)` succeeds with no independent review having happened. Expected per the stated threat model: this is possible and is the trusted-owner assumption; it must stay documented as such in the deployment configuration.","severity":"info","snippet":"    function setOracleSigner(address to) external onlyOwner {","title":"[owner/oracle trust] The HookJobRequester owner alone can manufacture a complete passing authorization chain; GatedHookDeployer adds no independent check"},{"citation":"resolved","description":"The gate assumes (docs line 1554 of the preserved IMD docs) that `resultHash` is keccak256 of the exact bytes served at `/intake-results/:id` (or the IPFS `result.json`), that those bytes are the same serialization as `/jobs/:id/result` (compact, ASCII, lowercase hex, `\"bytes\"` as an integer, `mediaType` exactly `application/json`, every accepted file carrying `name` and `path`, every accepted source carrying a 64-hex `hash`). The only real sample in the tree (docs/checks/approval-binding/sources/previous-job-result.json) has `files: []`, so no live accepted file entry has ever been parsed by this code; `fixtures/completion.json` is synthetic. Each assumption is enforced with a revert (AcceptedArtifact lines 56, 76-83; ResultJson line 158; `uintValue` line 73), so any deviation blocks deployment rather than weakening it. Because the snapshot is immutable per job, a deviation discovered after a paid job cannot be fixed for that job. Not a code defect; listed as a material audit blocker for a go-live decision: obtain one real completed chain job with the two declared output files and verify (a) `keccak256(GET /intake-results/:id bytes) == resultHash` from the real Intake callback, (b) `AcceptedArtifact.verify` returns true on those bytes, before enabling production chains. Also note the measured cold callback margin: the heaviest local path (wrong pre-binding, 22-cold-read ERC-1271 signer) uses 192,286 of the 200,000 forwarded gas; the new bind-first SSTORE (line 747) is included in those numbers, but a real signer heavier than the stress fixture fails closed and burns the one-shot delivery.","line":332,"path":"src/imd/HookJobRequester.sol","reproduction":"State: a real IMD chain job completes; Intake calls `onJobResult(jobId, resultHash, uri)`. Run `python3 -m sdk.gated_deploy ... --binding-mode owner-confirmed --result-document <bytes fetched from uri>`; expected `result document hash == job resultHash` ok and `verify_snapshot` ok. No such run exists in docs/checks (summary.json: liveReadinessClaim false). Local reproduction of the brittleness: test/scratch/MathProbe.t.sol::testFileWithoutNameRevertsWholeSnapshot and ::testSourceEntryWithNullHashRevertsWholeSnapshot (both revert on a single unrelated entry). Local evidence that the intended shape works: test/scratch/LivenessProbe.t.sol::testFourSourcesTwoFilesSnapshotDeploys (4 source entries + hooksStoreApproval + hooksStoreEvidence, 2,404 bytes, deploys in 2,333,464 gas), a shape the committed test suite does not exercise (it only ever uses one source entry and one file).","severity":"info","snippet":"    function onJobResult(bytes32 requestId, bytes32 resultHash, string calldata uri) external onlyIntake {","title":"[untested live behavior] resultHash-to-snapshot byte identity, source/file field presence and mediaType spelling are only shown on fixtures; the parser's strictness makes each a hard gate"}],"hash":"4c6360a9dca13e0f31b8735a85e550194106f60e9057c761de4994595cec736f","nodeId":"8ad3415b-cd14-4e63-8374-0347fe4622ef","outcome":"completed","summary":"The review is complete. Findings are written to `.imd-findings.json` (six entries, all snippets verified against the tree), and the scratch reproductions under `test/scratch/` pass as described.\n\n**Outcome.** No critical or high defect in the authorization chain. The parser, hash binding, CREATE2 prediction, permission-bit and readback checks, replay guards, and the signer-epoch provenance in both callback orders all held against the inputs I tried: duplicate and nested keys, escapes, token/depth/size limits, number and hex boundaries, source and build substitution, forged or unbound verdicts, revoked epochs, rotation, and the real IMD result sample. Both runtime sizes stay under the limit and the measured callback paths stay under the 200k stipend.\n\n**Findings, ranked:**\n- **Medium, protocol gap.** Revoking a signer epoch leaves every finalized Passed job of that epoch permanently undeployable. The job stays Passed so `requestReview` reverts, and no owner closure path exists in either order. Reproduced in `test/scratch/LivenessProbe.t.sol`.\n- **Low, boundary.** Approval staleness runs from job open, not from the verdict. A job delivered and passed one hour before the 30-day mark cannot deploy afterwards, and legal parameters can make the window zero.\n- **Low, liveness.** A single unrelated accepted entry reverts the whole gate: a null source hash, a file without a name, or any escaped or non-ASCII byte anywhere in the immutable snapshot. Reproduced in `test/scratch/MathProbe.t.sol`.\n- **Info, gas.** The duplicate-key scan is quadratic per object. A 1,615-token snapshot parses in about 36M gas, while real-shaped snapshots cost about 1M to 2.3M. Only the job's own pinned snapshot can trigger it.\n- **Info, owner/oracle trust.** The requester owner alone can manufacture a full passing chain via `setIntake` and `setOracleSigner`. Documented trust assumption, not a bypass.\n- **Info, untested live behavior.** Byte identity of `resultHash` with the served snapshot, presence of file fields, and mediaType spelling have only fixture evidence. The committed suite never parses a multi-source or two-file snapshot; my probe shows that shape deploys.\n\n**Not done.** I did not run the full 358-test suite or any live IMD request. Automatic binding remains disabled and nothing here certifies a founder-free or deployment-ready flow.","treeHash":null,"usage":{"cachedInputTokens":1941686,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":55240,"runtime":"claude","turns":38,"wallClockMs":844015}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"589ef002581a5371","findings":[{"citation":"resolved","description":"Code safety / design gap. revokeSignerEpoch is incident response, but it applies to every verdict verified under that epoch, including genuine ones. GatedHookDeployer._checkApproval (GatedHookDeployer.sol:276) then reverts ReviewRevoked forever, while HookJobRequester.requestReview refuses a job whose review is already Passed (only None/Unanswered are reviewable). closeRevokedReviewReceipt and confirmReviewExpiry both revert UnknownOrSettled because reviewOf was deleted at finalization, so neither the owner nor the creator can obtain a fresh verdict under the new signer. The creator's paid job.open and oracle.request are lost and the hook can never be deployed through the gate. Preserving the historical verdict is correct; the gap is that there is no 'Passed-but-revoked' re-review transition. Fix: in requestReview, also allow review == Passed (or NotPassed) when revokedSignerEpochs[reviewReceiptSignerEpoch(job.reviewRequestId)] != 0, recording the superseded ask in an event; GatedHookDeployer already keys on job.reviewRequestId so a new Passed verdict under a live epoch would deploy.","line":388,"path":"src/imd/HookJobRequester.sol","reproduction":"State: job Delivered; review Passed via bind-then-callback (or receipt-then-bind) under epoch 1; owner calls setOracleSigner(new) then revokeSignerEpoch(1, h). Expected: a path to re-verify the honest verdict under epoch 2. Actual: deploy(...) reverts ReviewRevoked(ask); creator's requestReview(job, 0.5e18) reverts NotReviewable(job); owner's closeRevokedReviewReceipt(ask) and confirmReviewExpiry(ask) revert UnknownOrSettled(ask). Reproduced in test/scratch/EpochAndLimits.t.sol::testRevokedEpochLeavesHonestJobWithNoRecovery (passes = dead end confirmed).","severity":"low","snippet":"                || (job.review != ReviewStatus.None && job.review != ReviewStatus.Unanswered)","title":"Signer-epoch revocation permanently strands honest Passed jobs: no re-review or recovery path"},{"citation":"resolved","description":"Code safety (liveness only; no authorization impact). ResultJson parses byte-by-byte through memory structs and allocates up to 2048 five-word tokens. Measured with forge: a realistic 1,889-byte snapshot costs 1.49M gas in AcceptedArtifact.verify; a documented-shape snapshot with 128 accepted files (7 fields each, 44,624 bytes) parses but costs 26.9M gas; adding 10 source entries to the same 128 files exceeds the 2048-token cap (ResultJson.sol:28) and reverts InvalidResultJSON even though every array is within the documented 128-item limit (ResultJson.sol:6-7). A 65,536-byte document (MAX_RESULT_DOCUMENT_BYTES) would need roughly 40M gas for parsing alone, above the Ethereum L1 block gas limit, so the constant promises a size that can never deploy there. With the shipped template (2 outputs, 2-4 sources) snapshots are ~2-3 KB and cost ~2M gas, so production use is fine, but a job whose snapshot grows (many accepted source nodes, long URLs) fails closed with no diagnostic. Fix: document the real bounds (token cap and gas), lower MAX_RESULT_DOCUMENT_BYTES to a value that is mineable (e.g. 16 KiB), and make the token cap check report a distinct error; optionally skip tokenizing values of unused top-level members.","line":59,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"Input A: snapshot built by test/scratch/ParserGas.t.sol::testGasMaxFiles (128 files x 7 fields + 10 sources, ~48 KB). Expected per NatSpec (arrays <=128 items, doc <=64 KiB): parses. Actual: InvalidResultJSON() after 60M gas (token count > 2048). Input B: test/scratch/EpochAndLimits.t.sol::testTokenCapBelowDocumentedArrayBound logs 'largest parseable file count 128 bytes 44624' and 'parse gas at that size 26868526'. Input C: ParserGas.t.sol::testGasWideTokens, 65,581-byte document: reverts after 27M gas.","severity":"low","snippet":"    uint256 public constant MAX_RESULT_DOCUMENT_BYTES = 65536;","title":"Snapshot parser gas (~600 gas/byte) makes the documented 64 KiB / 128-item bounds unusable; token cap (2048) can trip before the 128-item bound"},{"citation":"resolved","description":"Owner/oracle trust (intentional role, documented in docs/GATED_DEPLOYMENT.md; reported as a trust assumption, not a bypass). GatedHookDeployer pins `requester` immutably, but every input it trusts is set by HookJobRequester's owner at will: setIntake (any address may then call onJobResult with an arbitrary resultHash and onReviewResult), setOracleSigner (any key may then sign a Passed attestation in the requester's EIP-712 domain), bindReview (owner supplies oracleRequestId/questionHash/window with no onchain proof), and setJobTemplate/setReviewTemplate (the question text itself, including the chainId the oracle is asked about). No timelock, no second party, and the deployer owner cannot veto. The two-step StoreAdmin ownership is the only protection. Material consequence: compromise or malice of one EOA deploys arbitrary hook bytecode at a 'reviewed' address on mainnet/Base against the pinned PoolManager. Mitigation that preserves the design: hold the requester owner in a multisig/timelock, emit and monitor IntakeSet/OracleSignerSet/ReviewBound, and consider a deployer-side minimum delay between ReviewAnswered and deploy so observers can revoke.","line":877,"path":"src/imd/HookJobRequester.sol","reproduction":"State: attacker holds the requester owner key only. Steps: setIntake(attackerIntake); attackerIntake calls onJobResult(jobId, keccak256(forgedSnapshot), uri) for a job the attacker opened (status Open -> Delivered); setOracleSigner(attackerKey); requestReview; bindReview with arbitrary context; attackerIntake calls onReviewResult with an attestation signed by attackerKey (matches binding -> Passed, epoch = current, not revoked); deploy(artifact, forgedSnapshot, anyCreationCode, args, salt, hook). Expected by a reader of GatedHookDeployer's NatSpec ('an owner allowlist is not' enough): refusal. Actual: every check in _checkApproval passes because each trusted value was owner-chosen; deployment succeeds. The same sequence with MockIntake/signerKey is exactly what the test fixture does, which is the proof that the checks are consistency checks, not authenticity checks.","severity":"info","snippet":"    function setIntake(address to) external onlyOwner nonReentrant {","title":"Owner/oracle trust: the requester owner alone can manufacture the entire authorization chain the deployer trusts"},{"citation":"resolved","description":"Protocol gap / oracle trust. approvalDocument() always contains \"review\":\"passed\"; the file carries no reviewer identity, review submission hash, or verdict beyond the digest, so an accepted hooksStoreApproval is only evidence that the research-report worker (an LLM step that also reads the creator's untrusted brief embedded in the job objective) chose this artifact. _source accepts the artifact's sourceHash if it equals ANY accepted `source[]` entry; a chained job has one accepted bundle per step (implement-contract, write-foundry-tests), so an approval may name the pre-test bundle while the independent review covered a later one. Nothing onchain ties the approval to the adversarial-review step's submissionHash. All of that is delegated to the oracle question text (sdk/request_templates.py), which is the single point where 'review passed for this exact artifact' is actually decided; the oracle is also paid by and asked through the same owner-set template. This is the declared trust model, but reviewers should not read AcceptedArtifact.verify as proof of review. Hardening that keeps the design: include the review submission hash in the approval document and require the evidence file's hash in the artifact (buildHash already binds a build identity file), so a prompt-injected worker cannot produce an approval without the real review submission reference.","line":56,"path":"src/deployment/AcceptedArtifact.sol","reproduction":"Input: snapshot with source[] = [{hash: H_impl, submissionHash: S1}, {hash: H_tests, submissionHash: S2}] (two accepted nodes, as the shipped 4-step chain produces) and a unique accepted files[] entry whose hash is sha256(approvalDocument(digest)) where digest commits sourceHash = H_impl. Expected (reader's assumption from NatSpec 'reviewed source bundle'): only the bundle the adversarial-review step accepted qualifies. Actual: AcceptedArtifact.verify returns true for H_impl and also for H_tests if the worker had chosen it; the contract cannot distinguish. Second input: a creator brief instructing the final step to emit an approval for a different creation code; verify() and the deployer accept whatever the accepted file names, and only the oracle panel can refuse.","severity":"info","snippet":"            if (d.hashValue(d.member(cursor, \"hash\")) == sourceHash) {","title":"Protocol gap: the approval file is a constant template, and any accepted source entry (not necessarily the reviewed one) satisfies the source check"},{"citation":"resolved","description":"Untested live behavior (fail-closed liveness, not an authorization issue). ResultJson rejects any backslash, control byte, or byte > 126 in ANY string of the snapshot, including fields the gate never reads (delivery.repoUrl, delivery.failure, source[].url, files[].url, projectId). The pinned real sample (docs/checks/approval-binding/sources/previous-job-result.json) is pure ASCII with no escapes, and the docs state /intake-results/:id serves the hashed bytes byte-for-byte, so the design is plausible, but it has never been exercised against a live completion of the 4-step template, and a single escaped quote in a delivery failure message or a non-ASCII character in a URL would make that job permanently undeployable (resultHash is fixed by the Intake, so the creator cannot re-encode). Recommendation: before any pilot, fetch a live /intake-results/:id for a chained job with GitHub delivery and run sdk.result_snapshot.parse_snapshot on the exact bytes; consider tolerating JSON escapes in strings that are not compared (skip over \\\\x sequences without decoding) while keeping exact-match semantics for the fields that are compared.","line":158,"path":"src/deployment/ResultJson.sol","reproduction":"Input: the pinned sample with delivery.failure set to the string push rejected: \\\"main\\\" is protected (i.e. a JSON-escaped quote), delivered by the Intake as the resultHash preimage. Expected: the gate ignores delivery.* and deploys. Actual: ResultJson._string reverts InvalidResultJSON at the backslash; GatedHookDeployer.deploy reverts; no alternative encoding can match job.resultHash. Same for a snapshot containing an e-acute byte in a URL. Reproduced offline by test/AcceptedArtifact.t.sol::testDuplicateKeysAnywhereAndEscapedAliasesFailClosed (escaped text in an unused member reverts).","severity":"info","snippet":"            if (uint8(c) < 32 || uint8(c) > 126 || c == \"\\\\\") revert InvalidResultJSON();","title":"Untested live behavior: strict ASCII/no-escape parser fails closed on any escaped or non-ASCII byte anywhere in the real control-plane snapshot"},{"citation":"resolved","description":"Material audit blockers (not code defects). Verified in this review: pinned PoolManager code hashes for chains 1, 8453, 4663 and 11155111 match live eth_getCode via public RPCs on 2026-10-17; runtime sizes HookJobRequester 24,166 B and GatedHookDeployer 16,890 B are under 24,576; site/GatedHookDeployer.abi.json equals the compiled ABI; both callback orders keep the verification epoch and revocation blocks deployment in both; nested/duplicate/escaped fields, replay, CREATE2 prediction, reentrancy and readback checks behave as documented; cold callback measurements stay under the 200k stipend. Still unverified and required before any founder-free or deployment-ready claim: (1) the protocol publishes no authenticated Intake-request-to-oracle-UUID/canonical-question mapping, so bindReview context is owner-asserted (automaticBindingSupported() correctly returns false); (2) the test fixture's questionHash is synthetic (keccak of a mock string), so no test proves the owner can compute the real canonical hash the oracle signs; (3) no live Intake callback has delivered a real attestation or job result to this contract, so the 200k stipend is validated only against MockIntake; (4) no live /intake-results/:id snapshot of a 4-step chained job with the two named outputs has been parsed; (5) the oracle panel's ability to answer the 24-hour review question reliably is untested. These should be listed as blockers in docs/GATED_DEPLOYMENT.md and the deployment manifest status, which currently already says not audited.","line":188,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"State: docs/checks/approval-binding/summary.json reports liveReadinessClaim=false and all evidence is from local fixtures (fixtures/ marked synthetic; oracle.request.check.json is a free schema check with blockers=[]). Expected for a deployment-ready verdict: a live Passed attestation delivered by the real Intake and a live snapshot verified through check(). Actual: none exists in the tree; a reviewer attempting to certify would find no artifact to point at.","severity":"info","snippet":"    function automaticBindingSupported() external pure returns (bool) {","title":"Audit blockers that remain open: automatic binding, canonical question hash, live Intake callback gas and snapshot delivery are unverified"},{"citation":"resolved","description":"Configuration asymmetry. HookJobRequester allows jobTimeout and reviewTimeout each up to 30 days (MAX_TIMEOUT), and Intake delivery can legitimately arrive after local expiry; GatedHookDeployer measures staleness from job.openedAt with maxJobAge defaulting to 30 days (max 180). A job delivered on day 29 whose review is answered on day 31 is permanently ApprovalStale even though every verdict is genuine, and the creator paid for both. The owner can raise maxJobAge afterwards, which rescues it, but this is an undocumented operational coupling. Recommendation: document the invariant maxJobAge >= jobTimeout + reviewTimeout + deploy slack in docs/GATED_DEPLOYMENT.md, or measure staleness from the ReviewAnswered time instead of openedAt.","line":76,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"State: owner setTimeouts(30 days, 30 days) on the requester; maxJobAge left at 30 days. openJob at T; onJobResult at T+29d; requestReview at T+29d; Passed callback at T+31d; deploy at T+31d. Expected: deployable (all evidence valid). Actual: deploy reverts ApprovalStale(openedAt, 30 days); only an owner setMaxJobAge(>=31 days) unblocks it.","severity":"info","snippet":"    uint64 public maxJobAge = 30 days;","title":"Timeout configuration can exceed the deployer's approval window, stranding late but valid approvals"}],"hash":"af1d1176a53fce4fba967d1a12d75980f892da254e559027ba4ff45726eb1c7f","nodeId":"660e5593-613d-47e7-88c9-c881849b3fb9","outcome":"completed","summary":"The review is complete. The findings file is at `.imd-findings.json` in the repository root with seven entries, all anchored to lines that match the tree. No source, configuration or protected path was modified. Scratch tests live only under `test/scratch/`.\n\n**Outcome: no critical or high defects in the authorization chain.** The chain from authenticated resultHash through exact snapshot, unique accepted approval file, artifact digest and bound signed verdict holds against every substitution I tried. Two low-severity defects and five informational items are reported.\n\n**Low findings**\n\n- **Signer-epoch revocation strands honest jobs permanently.** After the owner rotates and revokes an epoch, a genuine Passed job cannot deploy and cannot request a new review, because `requestReview` treats Passed as terminal and the owner's close and expiry tools revert as settled. Reproduced in a scratch test in both callback orders.\n- **Parser limits are not what the comments promise.** Parsing costs roughly 600 gas per byte. A 128-file, 44 KB snapshot parses at about 26.9 M gas, adding ten source entries trips the 2048-token cap before the documented 128-item bound, and the 64 KiB document cap would need about 40 M gas. Realistic snapshots from the shipped template are 2 to 3 KB and cost about 2 M gas, so this is liveness only.\n\n**Informational, separated by category**\n\n- **Owner trust.** The requester owner alone can set the Intake, the signer, the templates and the binding context, so one key can manufacture the entire chain the deployer trusts. The deployer's immutable requester pointer cannot veto this. Reported as a trust assumption, not a bypass.\n- **Protocol gap.** The approval file is a constant template, and any accepted source entry satisfies the source check, so review truth rests entirely on the oracle question. The contract check is consistency, not review evidence.\n- **Untested live behavior.** The parser rejects any escape or non-ASCII byte anywhere in the snapshot, including fields the gate never reads. The pinned real sample is clean, but no live four-step completion has been parsed.\n- **Open audit blockers.** Automatic binding is correctly disabled. The fixture's question hash is synthetic, no live Intake callback or snapshot has been exercised, and no founder-free or deployment-ready claim is supportable.\n- **Timeout coupling.** Job plus review timeouts can exceed the default 30-day approval window, making a late but valid approval stale until the owner raises the window.\n\n**Independently verified as correct**\n\n- Pinned PoolManager code hashes for Ethereum, Base, Robinhood Chain and Sepolia match live `eth_getCode` today.\n- Runtime sizes are 24,166 and 16,890 bytes, under the 24,576-byte limit. The published site ABI equals the compiled ABI.\n- Epoch provenance survives rotation and blocks on revocation in both receipt-first and bind-first orders. Epoch zero is rejected.\n- Duplicate, nested and escaped fields, replay by job and by digest, CREATE2 prediction, constructor reentrancy, and runtime, manager and permission readback all behave as documented. The in-scope suites pass, 114 tests.","treeHash":null,"usage":{"cachedInputTokens":1967655,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":50133,"runtime":"claude","turns":42,"wallClockMs":855203}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c2e167266d13dcf4","findings":[{"citation":"resolved","description":"GatedHookDeployer._checkApproval (src/deployment/GatedHookDeployer.sol:276) correctly refuses a Passed verdict whose reviewReceiptSignerEpoch is in revokedSignerEpochs, in both callback orders. But HookJobRequester offers no way out of that state. _answerReview deletes reviewOf[intakeRequestId] (line 761), so closeRevokedReviewReceipt reverts UnknownOrSettled (and for the bind-first order there is no early receipt at all, so it would revert ReviewNotReceived anyway). requestReview (line 386-389) rejects any job whose review is Passed. Result: after an incident revocation, every job that was legitimately Passed under the compromised epoch is permanently undeployable and cannot buy a fresh review from the safe signer, even though the job was paid for and its snapshot/approval evidence is intact. By contrast a NOT yet finalized receipt from the same revoked epoch does get a retry path (closeRevokedReviewReceipt -> Unanswered -> requestReview). The asymmetry is not mentioned in docs/GATED_DEPLOYMENT.md, which only says the deployer rejects such verdicts. Impact: honest requesters lose their paid job and review fees on an owner-declared incident; the owner has no recovery short of asking them to open and pay for a new job. Minimal fix that preserves the historical verdict: add an owner function (or extend closeRevokedReviewReceipt) that, when revokedSignerEpochs[reviewReceiptSignerEpoch(job.reviewRequestId)] != 0 and deployedHookOf is irrelevant to the requester, sets job.review = Unanswered and emits ReviewUnanswered(jobRequestId, ask, 0, \"signer epoch revoked\"), so requestReview becomes callable again; or allow requestReview when review == Passed and that epoch is revoked.","line":388,"path":"src/imd/HookJobRequester.sol","reproduction":"State: job Delivered with a valid snapshot; review Passed (either bind-first via _pass or receipt-first via _passReceiptFirst in test/GatedHookDeployer.t.sol fixture); owner calls setOracleSigner(newSigner) then revokeSignerEpoch(1, keccak256('incident evidence')). Then: (1) deployer.deploy(...) as the requester -> reverts ReviewRevoked(ask) [expected: refused, correct]. (2) owner calls requester.closeRevokedReviewReceipt(ask) -> reverts UnknownOrSettled(ask) [expected: a closure path exists; actual: none]. (3) requester calls requester.requestReview(jobId, 0.5 ether) -> reverts NotReviewable(jobId) [expected: a fresh review from the safe signer can be bought; actual: refused]. getJob(jobId).review stays Passed forever. Runnable: test/scratch/AuditLeads.t.sol tests testBindFirstRevokedEpochLeavesNoRetryPath and testReceiptFirstRevokedEpochLeavesNoRetryPath (both pass, documenting the dead end) and testRetainedReceiptRevokedEpochCanBeClosedAndRetried (shows the retry path that exists only for unfinalized receipts). Run: forge test --match-path test/scratch/AuditLeads.t.sol --match-test RevokedEpoch -vv","severity":"medium","snippet":"                || (job.review != ReviewStatus.None && job.review != ReviewStatus.Unanswered)","title":"[code safety / liveness] A finalized Passed verdict whose signer epoch is later revoked is terminal: the job can never be re-reviewed or deployed"},{"citation":"resolved","description":"Per sdk/request_templates.py and test/helpers/ImdTemplates.sol the hooksStoreApproval file is emitted by the fourth 'research-report' step, i.e. by an ordinary IMD worker, after the 'adversarial-review' step (which writes no files) is accepted. 'Accepted' for a review submission means the reviewer's report was accepted by IMD, not that the hook had no findings. On-chain, AcceptedArtifact.verify only requires that (a) some top-level source[] entry has hash == a.sourceHash with any nonzero submissionHash, regardless of which step/node produced it, and (b) the unique files[] entry named hooksStoreApproval has the right sha256/bytes/mediaType and any nonzero submissionHash. Nothing on-chain relates either submissionHash to the review step, to each other, or to the 11 artifact fields (buildHash, creationCodeHash, constructorArgsHash, runtimeCodeHash, manager code hash, permissions). The only thing that connects 'review passed for THIS bytecode' to the Passed flag is the 7-member panel answering true to the long question in REVIEW_SUFFIX, which asks them to compare SHA-256/keccak values across public records. A panel that answers from the approval file's own \"review\":\"passed\" text, or a worker that is also the creator and writes the approval for an unreviewed build, is therefore the single point of failure; the contract has no second check. This is consistent with the repository's own statements (automaticBindingSupported() == false, docs/GATED_DEPLOYMENT.md 'Publication ... does not grant approval by itself') and is reported as a material audit blocker for any 'deployment-ready' or founder-free claim, not as a permission bypass. Mitigations within the design: require the approval file's submissionHash to equal a value the panel is asked to attest (e.g. include the accepted review submission hash in the HookArtifact struct and in the approval document so the panel question and the on-chain digest name the same review), and have the oracle question enumerate the exact digest rather than 'the file'.","line":83,"path":"src/deployment/AcceptedArtifact.sol","reproduction":"Input: the snapshot produced by _doc() in test/GatedHookDeployer.t.sol but with the approval entry's submissionHash set to 0x00..01 and the source entry carrying node=\"build_contract_project\" (the implement step, not the review step) -- see testRealSnapshotShapeParsesAndEscapeFailsClosed in test/scratch/AuditLeads.t.sol, first half. Expected if the chain bound the file to the accepted review: refusal. Actual: deploy succeeds once job.review == Passed. Nothing in src/ reads or constrains which submission produced the approval file; grep 'submissionHash' src/deployment/AcceptedArtifact.sol shows only the two nonzero checks at lines 57 and 83.","severity":"medium","snippet":"                        || d.hashValue(d.member(cursor, \"submissionHash\")) == bytes32(0)","title":"[protocol gap / oracle trust] The artifact-to-review binding is a worker-authored file plus a boolean panel answer; the contract cannot tie the approval file or the matched source entry to the accepte"},{"citation":"resolved","description":"ResultJson scans every byte through memory-struct field updates (d.pos++ at src/deployment/ResultJson.sol:153 plus per-byte range checks) and tokenises into a Token[] of up to 2048 five-word structs; _uniqueKey additionally re-hashes earlier keys per object. A well-formed snapshot that respects every documented bound (65,536 bytes, 1,920 tokens, <=128 items per container, depth 2) measured 34,691,289 gas for check() and 35,013,218 gas for deploy() in a local run. HookMiner's own comment notes RPC eth_call caps of 25-50M, and the sdk.gated_deploy flow runs check() through eth_call before building the transaction, so such a job is refused by the SDK and may not fit a block on some pinned chains (Robinhood Chain / Arbitrum-style per-tx caps are not verified here). The practical exposure is a job whose /jobs/:id/result is large (many files or long URLs); real snapshots seen so far are ~1 KB, so this is a liveness/limits mismatch rather than an exploit. Fix: lower MAX_RESULT_DOCUMENT_BYTES to a value whose worst case fits the smallest target budget (about 16 KiB gives ~9M), or cache d.pos/d.data in locals and use assembly byte reads in _string/_space so the per-byte cost drops an order of magnitude.","line":59,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"test/scratch/AuditLeads.t.sol testCheckGasAtDocumentCeiling: builds _doc(a) plus a 'pad' array of 15 x 127 numbers and one long ASCII string so the document is exactly 65,536 bytes, delivers it as the job result, passes the review, then measures gasleft() around deployer.check(...) and deployer.deploy(...). Expected: verification well under a typical 30M budget for any document the contract accepts. Actual output: 'check gas at ceiling: 34691289', 'deploy gas at ceiling: 35013218'. Run: forge test --match-path test/scratch/AuditLeads.t.sol --match-test testCheckGasAtDocumentCeiling -vv","severity":"low","snippet":"    uint256 public constant MAX_RESULT_DOCUMENT_BYTES = 65536;","title":"[code safety / gas] Verifying a snapshot at the contract's own 64 KiB ceiling costs about 35M gas, above common eth_call caps and at or above several target chains' block limits"},{"citation":"resolved","description":"The parser rejects any backslash or non-printable/non-ASCII byte in any string, anywhere in the document, including fields the gate never reads (delivery.failure, url, repoUrl, projectId). The snapshot bytes are fixed by resultHash at completion, so a requester cannot re-serialise them: if IMD ever emits an escaped quote, a newline in a failure message, a '\\/' escaped slash, or a non-ASCII filename, the job's approval is unusable and the only remedy is a new paid job. The repository's only real snapshot (docs/checks/approval-binding/sources/previous-job-result.json) parses, but it has files: [] and delivery.failure: null, so no live snapshot containing a hooksStoreApproval entry, a non-null failure, or chain-step 'source' arrays has been observed. Whether the live /intake-results/:id bytes are compact ASCII JSON with lowercase hex and lowercase UUIDs is an assumption, not evidence. This is fail-closed by design and is reported so it is not mistaken for proven live compatibility; the remedy is either a live pilot that records a real snapshot through the gate, or supporting the JSON escape subset (\\\" \\\\ \\/ \\n \\uXXXX) in strings that are not compared.","line":158,"path":"src/deployment/ResultJson.sol","reproduction":"test/scratch/AuditLeads.t.sol testRealSnapshotShapeParsesAndEscapeFailsClosed, second half: a snapshot identical in shape to the real previous-job-result.json but with \"failure\":\"push rejected: \\\"main\\\" protected\" (one escaped quote in an unread field). Job Delivered with keccak256 of those bytes, review Passed, correct artifact, mined salt. Expected: the gate reads jobId/state/complete/source/files only and deploys. Actual: deploy reverts ResultJson.InvalidResultJSON() and the job can never deploy. Run: forge test --match-path test/scratch/AuditLeads.t.sol --match-test testRealSnapshotShapeParsesAndEscapeFailsClosed -vv","severity":"low","snippet":"            if (uint8(c) < 32 || uint8(c) > 126 || c == \"\\\\\") revert InvalidResultJSON();","title":"[untested live behavior] One escaped character anywhere in the control-plane snapshot (e.g. delivery.failure or a URL) makes an otherwise approved job permanently undeployable; the approval path has n"},{"citation":"resolved","description":"Intentional powers, documented in docs/THREAT_MODEL.md ('Malicious requester owner'), recorded here as trust assumptions so the audit output is complete, not as bypasses. (1) HookJobRequester owner: setOracleSigner to a key it controls (new epoch, old approvals untouched), bindReview with any oracleRequestId/questionHash/window it asserts (no on-chain proof of the Intake-to-oracle mapping; automaticBindingSupported() is false), resetReviewBinding, confirmReviewExpiry and discard* which can set aside genuine answers, setIntake which changes who may call onJobResult/onReviewResult, and setJobTemplate/setReviewTemplate which define what the panel is asked. (2) GatedHookDeployer owner: setManager on 31337/84532/46630 to any contract with code, and setMaxJobAge down to 1 day which can retroactively stale every pending approval. (3) GatedHookDeployer.requester is immutable and the deployer owner cannot change it (good). Verified independently today: the four pinned PoolManager code hashes in GatedHookDeployer.sol match keccak256(eth_getCode) on chains 1, 8453, 4663 and 11155111 via public RPCs; HookJobRequester runtime is 24,166 bytes (under 24,576); the 200k callback stipend is unchanged. Preconditions for abuse: a single owner key; mitigation is the multisig/timelock the threat model already recommends plus event monitoring of OracleSignerEpochStarted and ReviewBound.","line":898,"path":"src/imd/HookJobRequester.sol","reproduction":"State: any Delivered job with a valid snapshot. Owner calls setOracleSigner(vm.addr(ownerKey)); creator calls requestReview; owner calls bindReview with self-chosen context; MockIntake delivers an attestation signed by ownerKey with matching context -> job.review == Passed -> deployer.deploy succeeds. Expected per the stated trust model: this is allowed. Actual: allowed. Existing coverage: testSignerRotationTakesEffect in test/HookJobRequester.t.sol and testProductionChainsAcceptOnlyPinnedManager in test/GatedHookDeployer.t.sol.","severity":"info","snippet":"    function setOracleSigner(address to) external onlyOwner {","title":"[owner/oracle trust] The requester owner can mint any Passed verdict (setOracleSigner + bindReview) and the deployer owner controls manager enablement on non-pinned chains and the approval window; the"}],"hash":"d1fa0010a26900d46716adaf30f09328d661b7ea3fc36b702abb5fb214eb82c9","nodeId":"f7e9075e-5f88-4e0b-8ebf-5133392004f1","outcome":"completed","summary":"Findings file is written and all five anchors verify against the tree. Recap:\n\n**Scope covered.** I read GatedHookDeployer, AcceptedArtifact, ResultJson, the signer-epoch change and the full review path in HookJobRequester, the SDK snapshot/deploy modules, templates, deployment drafts, the approval-binding check logs, and the pinned IMD docs/OpenAPI. The full suite passes (358 tests). I verified the four pinned PoolManager code hashes against live public RPCs today, and all match. HookJobRequester runtime stays at 24,166 bytes and the 200k callback stipend is unchanged.\n\n**What I found** (written to `.imd-findings.json`, reproductions in `test/scratch/AuditLeads.t.sol`):\n\n- **Medium, code safety.** A Passed verdict whose signer epoch is later revoked is a dead end in both callback orders. The deployer correctly refuses it, but the requester cannot close it and the requester cannot buy a new review, so the paid job is permanently undeployable.\n- **Medium, protocol gap.** The approval file is written by the final worker step and the contract only requires nonzero submission hashes. The sole link between \"review passed for this exact bytecode\" and the Passed flag is the panel's boolean answer. This is the design the repository already declares, reported as a blocker for any deployment-ready claim rather than a bypass.\n- **Low, gas.** A snapshot at the contract's own 64 KiB cap costs about 35M gas to verify, above common eth_call caps and near block limits on some target chains.\n- **Low, untested live behavior.** One escaped character in an unread field makes an approved job permanently undeployable. No live snapshot containing an approval file has ever been observed; the only real capture has an empty files array.\n- **Info, trust.** Owner powers on both contracts (signer rotation plus manual binding mint verdicts; manager enablement on unpinned chains; maxJobAge) are the roots of the gate.\n\n**What I did not find.** No parser escape, duplicate-key, nesting, source or build substitution, replay, CREATE2, constructor reentrancy, or readback bypass. Duplicate, nested and rejected-marker inputs, forged or unbound reviews, and revocation in both orders are all refused as documented.","treeHash":null,"usage":{"cachedInputTokens":2098036,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":43892,"runtime":"claude","turns":23,"wallClockMs":685778}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"Merged from audit_math, audit_permissions, audit_flow and audit_economics (all four reproduced the same mechanism). GatedHookDeployer._checkApproval (src/deployment/GatedHookDeployer.sol:275-278) correctly refuses a Passed verdict whose reviewReceiptSignerEpoch is in revokedSignerEpochs, in both the bind-first and receipt-first orders. But HookJobRequester offers no way out of that state. _answerReview deletes reviewOf[ask] (line 761), so closeRevokedReviewReceipt (line 514) and confirmReviewExpiry (line 808) revert UnknownOrSettled; in the bind-first order there is no early receipt at all, so closeRevokedReviewReceipt would revert ReviewNotReceived even if reviewOf survived. requestReview (lines 386-389) refuses any job whose review is Passed or NotPassed. The job therefore stays review == Passed forever while deployment is blocked forever. The only recourse for the creator is a brand-new paid job.open plus the full build/test/review chain plus a paid oracle.request, even though the delivered snapshot and artifact are unchanged and the revocation may be unrelated to this verdict (a key leaked after it was issued). By contrast an unfinalized retained receipt from the same revoked epoch does have a retry path (closeRevokedReviewReceipt -> Unanswered -> requestReview). docs/GATED_DEPLOYMENT.md line 205 tells the operator to 'use the existing guarded recovery calls' after a revocation, but none applies to a finalized verdict; the asymmetry is undocumented. Expected: after an owner revocation some owner-only or requester path restores liveness so a fresh verdict under the current epoch can re-establish provenance while the historical verdict stays in events. Actual: none exists. Minimal fix that preserves the design: an owner-only closeRevokedVerdict(jobRequestId) (or an extension of closeRevokedReviewReceipt) that requires jobs[job].reviewRequestId == ask, review in {Passed, NotPassed} and revokedSignerEpochs[reviewReceiptSignerEpoch(ask)] != 0, sets review = Unanswered, emits ReviewReceiptRevoked/ReviewUnanswered, and leaves consumed UUIDs, events and the deployer's replay maps untouched; GatedHookDeployer keys on job.reviewRequestId, so the old ask can never be reused and the new ask carries the new epoch.","line":388,"path":"src/imd/HookJobRequester.sol","reproduction":"test/scratch/Judge.t.sol::testRevokedEpochDeadEndBindFirst and ::testRevokedEpochDeadEndReceiptFirst (built on the repo fixture GatedHookDeployerTest). State: job Delivered with a valid snapshot; review Passed via _pass(jobId,true) (bind-first) or _passReceiptFirst(jobId) (receipt-first) under epoch 1. Owner: setOracleSigner(newSigner); revokeSignerEpoch(1, keccak256('incident')). Then: getJob(jobId).review == Passed; deployer.deploy(...) as the creator reverts ReviewRevoked(ask) [correct]; creator requestReview(jobId, 0.5 ether) reverts NotReviewable(jobId); owner closeRevokedReviewReceipt(ask) reverts UnknownOrSettled(ask); owner confirmReviewExpiry(ask) after the deadline reverts UnknownOrSettled(ask); review is still Passed. Expected: a path to a fresh verdict. Actual: none in either order. Run: forge test --match-path test/scratch/Judge.t.sol --match-test RevokedEpochDeadEnd -vv (both pass, i.e. the dead end is reproduced on this commit).","severity":"medium","snippet":"                || (job.review != ReviewStatus.None && job.review != ReviewStatus.Unanswered)","title":"[code safety / liveness] A finalized Passed verdict whose signer epoch is later revoked is a permanent dead end: no re-review or closure path in either callback order"},{"citation":"resolved","description":"Merged from audit_math and audit_permissions. maxJobAge (default 30 days, owner-settable 1..180 days) is compared against job.openedAt. HookJobRequester accepts jobTimeout and reviewTimeout up to MAX_TIMEOUT = 30 days each, onJobResult explicitly accepts late Intake deliveries after local expiry (status Open or Expired), the shipped review template asks for a 24-hour oracle window with validForSeconds 86400, and re-asks after an Unanswered review add more days. None of that time is subtracted, so the real deployment window is maxJobAge minus (delivery delay + review latency), which is zero or negative with legal parameters: with the allowed setMaxJobAge(1 days) no job can ever deploy because the 24-hour oracle window alone consumes it, and with setTimeouts(30 days, x) a delivery at openedAt + 30 days leaves zero seconds. The shipped drafts (deployment/*.draft.json: jobTimeout 604800, reviewTimeout 86400) leave a practical margin, but the invariant maxJobAge > jobTimeout + review latency + deploy slack is enforced nowhere and documented nowhere (docs/GATED_DEPLOYMENT.md only says the owner 'optionally calls setMaxJobAge'). The owner can rescue a stranded job by raising maxJobAge afterwards, which is an undocumented operational coupling. Expected: the staleness bound measures the age of the approval (time since the verdict, e.g. recorded in _answerReview) or at least guarantees a minimum window after the verdict, optionally with an absolute cap from openedAt. Actual: bound from openedAt only; the gate wastes a paid job and paid review for a timing reason unrelated to the safety of the artifact.","line":280,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"test/scratch/Judge.t.sol::testStaleMeasuredFromOpenNotVerdict: open job at T; vm.warp(T + 30 days - 1 hours); Intake delivers the snapshot; _pass(jobId, true) so getJob(jobId).review == Passed; vm.warp(T + 30 days + 1); deployer.deploy(a, doc, code, args, salt, hook) as the creator reverts ApprovalStale(openedAt, 2592000). Expected: a verdict that is about one hour old is deployable. Actual: refused permanently unless the deployer owner raises maxJobAge. Run: forge test --match-path test/scratch/Judge.t.sol --match-test testStaleMeasuredFromOpenNotVerdict -vv.","severity":"low","snippet":"        if (block.timestamp > uint256(job.openedAt) + maxJobAge) revert ApprovalStale(job.openedAt, maxJobAge);","title":"[code safety / boundary] Approval staleness is measured from job open, not from the verdict, so a late delivery or review can make a genuine approval undeployable the moment it passes"},{"citation":"resolved","description":"Merged from audit_math (and the brittleness half of its untested-live finding). _source calls member(cursor,'hash') and hashValue on EVERY source[] entry, and _approval (lines 76-77) calls member(cursor,'name') and member(cursor,'path') on EVERY files[] entry, before deciding whether the entry is the one being looked for. ResultJson.member reverts InvalidResultJSON on a missing key and hashValue reverts on anything that is not a 64-hex string. The snapshot is produced by the control plane from every accepted step of the four-step chain (implement-contract, write-foundry-tests, adversarial-review which 'writes no files', research-report with two outputs), it is immutable, and its keccak256 is pinned by the Intake callback, so the requester cannot repair it. Hence an entry the gate does not even need makes the approval permanently unusable: a step whose accepted entry has 'hash':null, an extra accepted file (hooksStoreEvidence or a report) whose entry omits name or path, or a source entry whose hash is not 64 lowercase hex. The only real snapshot in the tree (docs/checks/approval-binding/sources/previous-job-result.json) has files: [] and a single source entry, so no live multi-entry shape has ever been parsed by this code. Expected: entries that are neither the approval file nor the target source are skipped; only the matching entries are strictly validated. Actual: whole-document revert (InvalidResultJSON) and GatedHookDeployer.deploy reverts for that job forever. Minimal fix: a non-reverting tryMember in _source/_approval that skips entries whose hash/name/path is absent or not a string, keeping strictness for the matched entries.","line":56,"path":"src/deployment/AcceptedArtifact.sol","reproduction":"test/scratch/Judge.t.sol::testNullHashSourceEntryRevertsWholeSnapshot and ::testFileWithoutNameRevertsWholeSnapshot. Input (a): the fixture snapshot (correct accepted source, unique correct hooksStoreApproval file) plus a second source entry {\"node\":\"adversarial_review\",\"submissionHash\":\"11..11\",\"hash\":null}; AcceptedArtifact.verify(snapshot, digest, sourceHash) reverts InvalidResultJSON. Input (b): the fixture snapshot plus a second accepted file {\"path\":\"artifacts/report.md\",\"hash\":\"11..11\",\"bytes\":1,\"mediaType\":\"text/markdown\",\"submissionHash\":\"11..11\"} (no name); verify reverts InvalidResultJSON. testBaselineDocVerifies shows the same document without the extra entry returns true. Expected in both: true. Run: forge test --match-path test/scratch/Judge.t.sol --match-test RevertsWholeSnapshot -vv.","severity":"low","snippet":"            if (d.hashValue(d.member(cursor, \"hash\")) == sourceHash) {","title":"[code safety / liveness] One unrelated accepted entry in the immutable snapshot (a source entry with null hash, a file without name/path) reverts the whole gate for an otherwise valid approval"},{"citation":"resolved","description":"Merged from audit_permissions, audit_flow and audit_economics. _string rejects every string containing a backslash, a control byte or any byte > 126, at any depth, including fields the gate never reads (delivery.failure, delivery.repoUrl, launch, source[].url, files[].url, projectId). The snapshot bytes are fixed by the Intake's resultHash and cannot be normalized, so a single escaped quote in a GitHub delivery failure message, a '\\/'-escaped slash from a different serializer, a \\uXXXX escape or a non-ASCII filename makes the job's approval unusable forever. The sdk/result_snapshot.py mirror applies the same rule, so the SDK cannot rescue it either. The one real snapshot in the tree (sources/previous-job-result.json, 827 bytes) is compact ASCII and parses, but it has files: [] and failure: null; no completed snapshot with a non-empty files array, a delivery failure or chained source entries has been observed, and docs/checks/approval-binding/README.md itself lists 'actual completion encoding' as an unresolved gate. The documented IMD schema does not promise ASCII-only, escape-free serialization. This is fail-closed by design and is not an authorization weakness; it is reported so that it is not mistaken for proven live compatibility and because the cost of a mismatch is a wasted paid job and review. Fix options: tolerate the JSON escape subset (skip \\x pairs and bytes >= 0x80) in strings that are never compared while keeping exact-match semantics for compared keys/values, or obtain and record a first-party serializer guarantee and a live parsed sample before any pilot.","line":158,"path":"src/deployment/ResultJson.sol","reproduction":"test/scratch/Judge.t.sol::testEscapedUnreadFieldBlocksApprovedJob: the fixture snapshot with an added unread member ,\"delivery\":{\"requested\":true,\"failure\":\"push rejected: \\\"main\\\" is protected\"} delivered by the Intake as the resultHash preimage; review Passed; correct artifact and mined salt. Expected: the gate reads only jobId/state/complete/source/files and deploys. Actual: deployer.deploy reverts ResultJson.InvalidResultJSON() and no alternative encoding can match job.resultHash. Existing coverage: test/AcceptedArtifact.t.sol testDuplicateKeysAnywhereAndEscapedAliasesFailClosed shows the \\n case. Run: forge test --match-path test/scratch/Judge.t.sol --match-test testEscapedUnreadFieldBlocksApprovedJob -vv.","severity":"low","snippet":"            if (uint8(c) < 32 || uint8(c) > 126 || c == \"\\\\\") revert InvalidResultJSON();","title":"[untested live behavior] Any JSON escape or non-ASCII byte anywhere in the live completion snapshot, even in fields the gate never reads, permanently blocks an approved artifact; live snapshot encodin"},{"citation":"resolved","description":"Merged from audit_math, audit_permissions, audit_flow and audit_economics (measurements agree within a few percent). ResultJson scans every byte through memory-struct field updates (d.pos++ per byte in _string, per-byte range checks), tokenises into a Token[] of up to 2048 five-word structs, and _uniqueKey re-hashes every earlier key of an object for each new key (quadratic per object, up to 128 keys). Measured on this commit with the project's compiler settings: a 541-byte fixture-shaped snapshot verifies in 579,468 gas; a well-formed 65,536-byte, 1,953-token snapshot that passes every gate costs 33,065,165 gas in check() and 34,530,363 gas in deploy(). HookMiner's own comment notes RPC eth_call caps of 25-50M, and sdk/gated_deploy.py runs check() through eth_call before building any transaction, so such a job is refused by the SDK; several pinned chains have per-block or per-transaction limits at or below that figure. Wide objects are worse: six 128-key objects (1,615 tokens, 8.7 KB) parse in ~35.6M gas, so the 2048-token cap can be reached far below 64 KiB and the documented 128-item array bound cannot actually be used with 7-field file entries (128 files x 7 fields plus 10 sources exceeds 2048 tokens and reverts with the generic InvalidResultJSON). Only the control plane controls snapshot size, so this is not an attacker lever; realistic snapshots are 1-3 KB and cost 0.6-2M gas. It is a mismatch between the constant and the NatSpec bounds on one side and what is deployable on the other. Fix: lower MAX_RESULT_DOCUMENT_BYTES to a value whose worst case was measured against the smallest target budget (about 16 KiB gives ~9M), cache d.pos/d.data in locals or scan strings word-wise in assembly, make the token-cap failure a distinct error, and document the real bounds.","line":59,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"test/scratch/Judge.t.sol::testGasAtDocumentCeiling: builds the fixture snapshot plus a 'pad' member of 15 arrays of 127 zeros and a filler string so the document is exactly 65,536 bytes, delivers keccak256 of it as the job result, passes the review, then measures gas around deployer.check(...) and deployer.deploy(...). Logged on this commit: check gas 33,065,165; deploy gas 34,530,363; token count 1,953; fixture-size verify 579,468 gas for 541 bytes. Expected: a document the contract accepts verifies within a typical 25-30M eth_call budget. Actual: ~33-35M. Run: forge test --match-path test/scratch/Judge.t.sol --match-test testGasAtDocumentCeiling -vv.","severity":"low","snippet":"    uint256 public constant MAX_RESULT_DOCUMENT_BYTES = 65536;","title":"[code safety / gas] Verifying a snapshot at the contract's own 64 KiB ceiling costs about 33-35M gas, above common eth_call caps and near or above several target chains' block limits"},{"citation":"resolved","description":"Merged from audit_permissions and audit_economics. Reported as a declared-design gap and material blocker for any founder-free or deployment-ready claim, not as a permission bypass. The hooksStoreApproval file is emitted by the fourth research-report step, an ordinary IMD worker that also reads the creator's untrusted brief embedded in the job objective; approvalDocument(digest) always contains \"review\":\"passed\" and carries no reviewer identity, review submission hash or verdict beyond the digest. AcceptedArtifact.verify requires only (a) some top-level source[] entry with hash == a.sourceHash and any nonzero submissionHash, regardless of which node produced it (line 56), and (b) the unique files[] entry named hooksStoreApproval with the right sha256/bytes/mediaType and any nonzero submissionHash (line 83). Nothing relates either submissionHash to the adversarial-review step, to each other, or to the artifact's buildHash/creationCodeHash/runtimeCodeHash. A chained job yields one accepted bundle per step, so an approval may name the pre-test bundle while the review covered a later one, and 'accepted' for a review submission means IMD accepted the report, not that the hook had no findings. The only thing connecting 'independent review passed for THIS bytecode' to the Passed flag is the 7-member panel answering true to the question in sdk/request_templates.py, which is set by the same requester owner. This matches the repository's own statements (automaticBindingSupported() == false; docs/GATED_DEPLOYMENT.md). Hardening that keeps the design: include the accepted review submission hash in the HookArtifact struct and the approval document so the panel question and the on-chain digest name the same review, and require the hooksStoreEvidence file's hash in the artifact.","line":83,"path":"src/deployment/AcceptedArtifact.sol","reproduction":"test/scratch/Judge.t.sol::testAnyAcceptedSourceEntryQualifies: snapshot with source[] = [{node write_foundry_tests, hash H_reviewed, submissionHash S1}, {node implement_contract, hash H_pretest, submissionHash S2}] and a unique accepted hooksStoreApproval whose digest commits sourceHash = H_pretest, submissionHash 0x..01. Expected if the chain bound the approval to the reviewed bundle: refusal. Actual: AcceptedArtifact.verify returns true (test passes on this commit), and GatedHookDeployer.deploy accepts it once job.review == Passed. grep submissionHash src/deployment/AcceptedArtifact.sol shows only the two nonzero checks at lines 57 and 83.","severity":"low","snippet":"                        || d.hashValue(d.member(cursor, \"submissionHash\")) == bytes32(0)","title":"[protocol gap / oracle trust] On chain, the approval file is a worker-authored constant and any accepted source entry qualifies; nothing ties the approval or the matched source to the adversarial-revi"},{"citation":"resolved","description":"Merged from all four specialists; recorded as the design's declared trust assumption (docs/THREAT_MODEL.md, docs/GATED_DEPLOYMENT.md), not a bypass. Every input the deployer trusts is set by one role with no timelock or second party: setIntake (line 877) chooses who may call onJobResult/onReviewResult, so a self-deployed IIntake can deliver any resultHash for a job the owner opened; setOracleSigner (line 898) starts a new epoch under a key the owner holds; bindReview supplies the oracle UUID/questionHash/window the callback is matched against with no on-chain proof; setJobTemplate/setReviewTemplate define the question itself; resetReviewBinding, confirmReviewExpiry and discard* can set aside genuine answers. GatedHookDeployer only reads getJob, revokedReviewReceipts, reviewReceiptSignerEpoch and revokedSignerEpochs from the immutable requester and adds no independent authenticity check; its own owner controls setManager (any contract with code on 31337/84532/46630) and setMaxJobAge (down to 1 day, which retroactively stales every pending approval) and can therefore stall or veto any creator's deployment, reversibly and with events. The two-step StoreAdmin ownership is the only protection. Verified during this review: the four pinned PoolManager code hashes in GatedHookDeployer.sol are the ones the specialists re-read live on 2026-10-17 for chains 1, 8453, 4663 and 11155111; the requester is immutable in the deployer; runtime sizes HookJobRequester 24,166 B and GatedHookDeployer 16,890 B are under 24,576 (forge build --sizes); the 200k forwarded-gas stipend is unchanged. Preconditions for abuse: a single owner key. Mitigation that preserves the design: multisig/timelock for both owners, monitoring of IntakeSet/OracleSignerEpochStarted/ReviewBound/MaxJobAgeSet, and optionally a deployer-side minimum delay between ReviewAnswered and deploy so observers can revoke.","line":898,"path":"src/imd/HookJobRequester.sol","reproduction":"test/scratch/Judge.t.sol::testRequesterOwnerAloneMintsPassingChain: with the fixture's owner-chosen Intake (MockIntake), the owner calls setOracleSigner(vm.addr(ownerKey)); the creator calls requestReview; the owner binds a self-chosen context; the Intake delivers an attestation signed by ownerKey; deployer.deploy succeeds with no independent review having happened. Expected per the stated threat model: allowed. Actual: allowed (test passes). Veto: as deployer owner setMaxJobAge(1 days) then any job with openedAt < now - 1 day reverts ApprovalStale (covered by the existing testStaleApprovalRefused shape); setManager(manager, 0, false) reverts ManagerNotEnabled for every artifact (existing testDisabledManagerRefused).","severity":"info","snippet":"    function setOracleSigner(address to) external onlyOwner {","title":"[owner/oracle trust] The HookJobRequester owner alone can manufacture a complete passing authorization chain; the GatedHookDeployer owner can veto every deployment"},{"citation":"resolved","description":"Reproduced from audit_flow. HookJobRequester.openJob assembles the body as prefix ++ 0x<specHash> ++ middle ++ brief ++ suffix (src/imd/HookJobRequester.sol:309). The Intake request id is only returned by intake.requestWithFailure after the body is fixed, so it can never be in the body; the deployer address, chain id, PoolManager, its code hash and the HookSpec itself are not in the body either (only the spec hash is), and the template declares no inputs. The research-report step is told to compute HookArtifact 'per the supplied GatedHookDeployer ABI' with 'jobRequestId is the originating Intake job id', yet nothing supplies them: the worker must discover the Intake id from chain logs (JobOpened's requestId matched by bodyHash) or from the unauthenticated HTTP /intake/:chainId/:txHash record, and must be told the deployer address out of band. If any parameter is wrong the approval names a digest the deployer will never accept; if the worker follows the 'emit no passed approval' rule, no approval is ever produced. docs/GATED_DEPLOYMENT.md acknowledges supplying these via actual job inputs as future work. Not a code defect; a blocker for the end-to-end path resultHash -> accepted approval file -> artifact, which has never been exercised against a live job.","line":20,"path":"sdk/request_templates.py","reproduction":"State: deploy HookJobRequester with deployment/hook-templates.json fragments; call openJob(specHash, brief, max). Inspect the emitted JobOpened body (test/HookJobRequester.t.sol fixture, or sdk.request_templates.assemble): it contains the spec hash and brief only. Expected (per the research-report objective at sdk/request_templates.py:20-22): the worker can read jobRequestId, deployer, chainId, poolManager and permissions from its inputs. Actual: none appear in the body or in any declared input; docs/checks/approval-binding/README.md:108-113 lists 'actual worker publication of the named files' among unresolved gates.","severity":"info","snippet":"        {'skill': 'research-report',","title":"[protocol gap / untested live behavior] The job body gives the publishing worker no Intake request id, deployer, chain or manager parameters, so the HookArtifact it must commit to cannot be derived fr"},{"citation":"resolved","description":"Merged from audit_math and audit_permissions. Verified locally in this review: both callback orders record and check the verification epoch (ApprovalBindingRegression tests and the scratch tests above); revocation blocks deployment in both orders; nested/duplicate/escaped fields, replay (deployedHookOf, usedArtifact), CREATE2 prediction, constructor reentrancy (nonReentrant plus approval consumed before CREATE2), runtime/manager/permission readback and every deployment entry point (deploy is the only state-changing gated path; check/verifyDeployment/mine/predict are views) behave as documented; the sdk/result_snapshot.py mirror parses the real sample. Still unverified and required before any live claim: (1) the protocol publishes no authenticated Intake-request-to-oracle-UUID/canonical-question mapping, so bindReview context is owner-asserted and automaticBindingSupported() correctly returns false; (2) the fixture questionHash is keccak of a mock string, so no test proves the owner can compute the real canonical hash the oracle signs; (3) no live Intake callback has delivered a real attestation or job result to this contract, so the 200k stipend is validated only against MockIntake (heaviest local path 192,286 of 200,000 forwarded gas per docs/checks/approval-binding/cold-gas.log, leaving a 7,714-gas margin for a heavier real signer, which would fail closed and burn the one-shot delivery); (4) no live /intake-results/:id snapshot of a 4-step chained job with the two named outputs has been fetched and shown byte-identical to resultHash and accepted by AcceptedArtifact.verify; (5) the oracle panel's ability to answer the 24-hour review question reliably is untested. docs/checks/approval-binding/summary.json records liveReadinessClaim=false and fixtures/ is marked synthetic; the free request checker's blockers=[] is a schema check, not delivery evidence. The deployment manifest status already says 'not audited'; these items should be listed explicitly as blockers there and in docs/GATED_DEPLOYMENT.md.","line":188,"path":"src/deployment/GatedHookDeployer.sol","reproduction":"State: docs/checks/approval-binding/summary.json line 46 liveReadinessClaim=false; sources/previous-job-result.json is the only real snapshot and has files: [] (python3 -I -c 'from sdk.result_snapshot import parse_snapshot' on its bytes returns files [] and failure None); fixtures/completion.json is synthetic. Expected for a deployment-ready verdict: a live Passed attestation delivered by the real Intake and a live snapshot verified through check(). Actual: none exists in the tree; a reviewer attempting to certify finds no artifact to point at.","severity":"info","snippet":"    function automaticBindingSupported() external pure returns (bool) {","title":"[untested live behavior / audit blockers] resultHash-to-snapshot byte identity, canonical question hash, live Intake callback gas and automatic binding are unverified; nothing in the tree supports a f"}],"hash":"e8142def83ae60132b12bdb6824bdddf46b1c7bd207f4d83a096d254ced0d79b","nodeId":"133372b5-3256-4fea-9d6f-881eed346415","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds nine substantiated findings, all reproduced on commit cc847d8 with the scratch suite in `test/scratch/Judge.t.sol`. The 23 specialist findings collapsed into these nine after merging duplicates. Nothing was dropped as unreproducible, and none reached critical or high, so no proof files were required.\n\n**Code safety**\n- **Medium.** A finalized Passed verdict whose signer epoch is later revoked is a permanent dead end in both callback orders. The deployer correctly refuses it, but the requester then rejects a fresh review, closure and expiry confirmation, so an honest paid job can never deploy again. The docs point operators at recovery calls that do not apply here.\n- **Low.** Approval staleness is measured from job open rather than from the verdict, so a late delivery or slow review can make a genuine approval stale the moment it passes.\n- **Low.** One unrelated accepted entry in the immutable snapshot, such as a source entry with a null hash or a file without a name, reverts the whole gate for an otherwise valid approval.\n- **Low.** Verifying a snapshot at the contract's own 64 KiB ceiling costs about 33M gas in check and 34.5M in deploy, above common eth_call caps that the SDK depends on.\n\n**Owner and oracle trust, protocol gaps, untested live behavior**\n- **Low.** On chain, the approval file is a worker-authored constant and any accepted source entry qualifies, so the oracle panel alone decides that the review covered this artifact. Recorded as a declared design gap, not a bypass.\n- **Low.** Any JSON escape or non-ASCII byte anywhere in the live snapshot blocks an approved job permanently. Live encoding has never been observed with a non-empty files array.\n- **Info.** The requester owner alone can mint a passing chain and the deployer owner can veto any deployment. These are the declared trust assumptions, verified concretely with the fixture.\n- **Info.** The job body gives the publishing worker no Intake id, deployer, chain or manager values, so the artifact cannot be derived from the job's own data.\n- **Info.** Byte identity of resultHash to the live snapshot, the canonical question hash, live callback gas within the 200k stipend and automatic binding remain unverified. Nothing in the tree supports a founder-free or deployment-ready verdict, consistent with the repository's own liveReadinessClaim of false.\n\nIndependently confirmed during the review: both callback orders record and check the verification epoch, replay and reentrancy guards hold, runtime sizes stay under 24,576 bytes, and the pinned manager hashes match the specialists' live reads. The scratch test file is in test/scratch and is not part of the submission.","treeHash":null,"usage":{"cachedInputTokens":1730374,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":26969,"runtime":"claude","turns":30,"wallClockMs":425545}}],"verification":[]}