DeepScan reads your whole repository and finds the places where the code does something different from what it promises: in its docs, docstrings, types, tests, or a twin implementation that does the same job. It takes about 15-20 minutes and runs in the background.
How to run it
In Claude Code, type:
/intent:deepscan
To scan one folder:
/intent:deepscan --scope src/api
In Codex, ask:
a deepscan of this repo
The report link comes back right away. The results arrive in your agent at your next message, and the page at the link shows progress until then.
How it works
Maps the repo and the promises it makesDocs, docstrings, types, tests and twin implementations all say what the code should do.
Turns them into checkable propertiesEach promise becomes a statement the code either keeps or breaks.
Hunts for violations in parallelMany hunters read the code at once, each looking for a place where a property fails.
Checks every finding before you see itA citation check against your code and a skeptic review decide what makes the report.
Our server reads the code and never runs it. Each finding comes with a repro sketch and a prompt your own agent uses to reproduce it on your machine, with your yes.
What gets sent
DeepScan uploads the tracked source files, after the same secret filtering and redaction as checks, capped at 3,000 files and 15 MB. --scope narrows it to a folder.
Sample report
Sample: a scan of a demo job-queue library
Findings
HIGHFlagged, not reproduced
Concurrent first checks can exceed a key’s burst capacity
What should hold
Each key has its own bucket, and the asynchronous resolve function is called once per key to determine that key's capacity and refill rate.
The promise
A key never gets more than capacity requests admitted
in a burst, no matter how many calls are in flight.
Call check("k") several times concurrently on a new key with a resolver returning { capacity: 1, ratePerSec: 1 }. Each call sees no stored bucket before awaiting resolve; after the await, each creates its own full bucket and admits one request before storing it. Thus multiple requests are admitted in the burst and resolve runs multiple times for the same key. The README promises one resolver call per key and a burst capped at capacity (README.md:69-72).
How to trigger it
Using the public KeyedRateLimiter export, create a limiter whose async resolve increments a counter and returns { capacity: 1, ratePerSec: 1 }. Run await Promise.all([limiter.check("k"), limiter.check("k"), limiter.check("k")]) immediately. All three decisions can be allowed and the counter becomes 3, instead of one allowed decision and one resolver call as promised.
Behaves correctly here
Sequential checks share the stored bucket: the test's sequential lookup case confirms one resolver call for two checks, then another only after reset (test/ratelimit.test.ts:57-70).
Fixes that don't work
Checking buckets again after resolving but still creating a bucket before the check is insufficient; callers must use the already-installed bucket to share its tokens.
Caching only the resolved limits would satisfy the lookup count but not the burst cap if each concurrent caller still creates and consumes a separate TokenBucket.
A maintainer's best answer, and why it doesn't hold
There is no reasonable one-line justification: the README explicitly promises the burst cap even when calls are in flight, and concurrent initialization defeats it.
Novelty not checked.
Reproduce this on your machine
Show the prompt
Reproduce this on my machine: write a failing test for "Concurrent first checks can exceed a key’s burst capacity", then run it. Don't fix anything yet.
Where: src/ratelimit/keyed-limiter.ts:50, src/ratelimit/keyed-limiter.ts:52, src/ratelimit/keyed-limiter.ts:53, src/ratelimit/keyed-limiter.ts:54.
The promise it breaks (README.md:71): "**A key never gets more than `capacity` requests admitted
in a burst**, no matter how many calls are in flight."
What happens: Call `check("k")` several times concurrently on a new key with a resolver returning `{ capacity: 1, ratePerSec: 1 }`. Each call sees no stored bucket before awaiting `resolve`; after the await, each creates its own full bucket and admits one request before storing it. Thus multiple requests are admitted in the burst and `resolve` runs multiple times for the same key. The README promises one resolver call per key and a burst capped at capacity (README.md:69-72).
How to trigger it: Using the public `KeyedRateLimiter` export, create a limiter whose async `resolve` increments a counter and returns `{ capacity: 1, ratePerSec: 1 }`. Run `await Promise.all([limiter.check("k"), limiter.check("k"), limiter.check("k")])` immediately. All three decisions can be allowed and the counter becomes 3, instead of one allowed decision and one resolver call as promised.
For comparison, this behaves correctly: Sequential checks share the stored bucket: the test's sequential lookup case confirms one resolver call for two checks, then another only after `reset` (test/ratelimit.test.ts:57-70).
Report whether the test fails for the reason described above.
MODERATEFlagged, not reproduced
A failing job can execute maxAttempts + 1 times
What should hold
A job with maxAttempts set to N is executed no more than N times in total, counting its first execution.
The promise
- Attempts.maxAttempts is the total number of executions, including the
first one. A job with maxAttempts: 3 runs at most three times.
Enqueue a job with maxAttempts: 3 and register a handler that always throws. After the first failure, attempts is 1 and the <= 3 check requeues it; the same happens after failures two and three. The third failure therefore still makes it eligible for another execution, and the fourth invocation finally increments attempts to 4 and marks the job failed. The handler runs four times, despite the README promise of at most three.
How to trigger it
Using the public API, create new JobQueue({ backoff: { baseDelayMs: 0, maxDelayMs: 0, factor: 2 } }), register a handler that throws, and enqueue with { maxAttempts: 3 }. Call processNext() four times; zero backoff makes the job immediately ready after each of its first three failures. The handler is called four times, while the README says it should run at most three.
Behaves correctly here
A handler that succeeds on its first invocation completes without being requeued: the success path increments attempts and marks the job done (src/queue/queue.ts, lines 172–175).
Fixes that don't work
Catching and retrying every error from the whole try block continues to treat persistence errors as handler failures; isolate handler execution errors from completion persistence errors.
A maintainer's best answer, and why it doesn't hold
The <= condition could be intended to allow a retry after the last counted failure, but that makes maxAttempts a retry count rather than the documented total execution limit.
Novelty not checked.
Reproduce this on your machine
Show the prompt
Reproduce this on my machine: write a failing test for "A failing job can execute maxAttempts + 1 times", then run it. Don't fix anything yet.
Where: src/queue/queue.ts:178, src/queue/queue.ts:180.
The promise it breaks (README.md:36): "- **Attempts.** `maxAttempts` is the total number of executions, including the first one. A job with `maxAttempts: 3` runs at most three times."
What happens: Enqueue a job with `maxAttempts: 3` and register a handler that always throws. After the first failure, `attempts` is 1 and the `<= 3` check requeues it; the same happens after failures two and three. The third failure therefore still makes it eligible for another execution, and the fourth invocation finally increments attempts to 4 and marks the job failed. The handler runs four times, despite the README promise of at most three.
How to trigger it: Using the public API, create `new JobQueue({ backoff: { baseDelayMs: 0, maxDelayMs: 0, factor: 2 } })`, register a handler that throws, and enqueue with `{ maxAttempts: 3 }`. Call `processNext()` four times; zero backoff makes the job immediately ready after each of its first three failures. The handler is called four times, while the README says it should run at most three.
For comparison, this behaves correctly: A handler that succeeds on its first invocation completes without being requeued: the success path increments attempts and marks the job done (src/queue/queue.ts, lines 172–175).
Report whether the test fails for the reason described above.
MODERATEFlagged, not reproduced
Cursor pagination skips tied items after a page boundary
What should hold
For items with unique IDs, cursor pagination orders items by sort key and then ID; following successive nextCursor values visits every item exactly once, including ties in the sort key.
The promise
Items are ordered by the sort key and then by id. Following nextCursor
never skips or repeats an item, including when several items share the same
sort key.
Call paginate with items {id:"a", k:1}, {id:"b", k:1}, {id:"c", k:2} and {limit:1}, using item => item.k as sortKey. Sorting puts a before b, and the first page returns a with a cursor containing its key and ID. On the next call, the filter keeps only items whose key is greater than 1, so it drops b and returns c; following cursors therefore never visits b.
How to trigger it
Using the public paginate API, page through [{id:"a",k:1},{id:"b",k:1},{id:"c",k:2}] with limit 1 and sort key item => item.k, repeatedly passing each nextCursor. The collected IDs are ["a","c"], not all three IDs exactly once as promised.
Behaves correctly here
With unique sort keys, e.g. keys 1, 2, and 3 for IDs a, b, and c, the same limit-1 cursor walk returns all items in order.
Fixes that don't work
Filtering only on after.id would skip or misorder items with different sort keys; resumption must compare the (sortKey, id) pair.
A maintainer's best answer, and why it doesn't hold
This is fine because callers can use unique sort keys, but the documented API explicitly promises correct pagination when sort keys tie and does not require them to be unique.
Novelty not checked.
Reproduce this on your machine
Show the prompt
Reproduce this on my machine: write a failing test for "Cursor pagination skips tied items after a page boundary", then run it. Don't fix anything yet.
Where: src/paginate/cursor.ts:45, src/paginate/cursor.ts:51, src/paginate/cursor.ts:59.
The promise it breaks (README.md:104): "Items are ordered by the sort key and then by `id`. **Following `nextCursor` never skips or repeats an item**, including when several items share the same sort key."
What happens: Call `paginate` with items `{id:"a", k:1}`, `{id:"b", k:1}`, `{id:"c", k:2}` and `{limit:1}`, using `item => item.k` as `sortKey`. Sorting puts `a` before `b`, and the first page returns `a` with a cursor containing its key and ID. On the next call, the filter keeps only items whose key is greater than 1, so it drops `b` and returns `c`; following cursors therefore never visits `b`.
How to trigger it: Using the public `paginate` API, page through `[{id:"a",k:1},{id:"b",k:1},{id:"c",k:2}]` with limit 1 and sort key `item => item.k`, repeatedly passing each `nextCursor`. The collected IDs are `["a","c"]`, not all three IDs exactly once as promised.
For comparison, this behaves correctly: With unique sort keys, e.g. keys 1, 2, and 3 for IDs a, b, and c, the same limit-1 cursor walk returns all items in order.
Report whether the test fails for the reason described above.
Coverage
28 files read, 75 KB. Nothing was left out.
Properties checked, no violation found (2)
For valid enqueue options, JobQueue.enqueue checks findByKey before creating a job (queue.ts:95-98), and both stores retain key lookup across status updates (stores.ts:20-22, 32-35; SnapshotStore scans all jobs at 86-87), so queued, running, done, and failed jobs are returned rather than duplicated.
TtlCache.getOrLoad checks and returns the in-flight promise, stores a new load before yielding, and deletes that key on either settlement (src/cache/ttl-cache.ts:113-124: “const pending = this.inflight.get(key)” through “this.inflight.set(key, promise)”; src/cache/ttl-cache.ts:120-122: “.finally(() => {” / “this.inflight.delete(key);”). It caches only fulfillment, while memoizeAsync delegates each argument-derived key to getOrLoad (src/cache/ttl-cache.ts:115-123, src/cache/ttl-cache.ts:187-188).
Properties not hunted (10)
ConcurrencyLimiter never runs more tasks at once than its configured limit, and releases each slot after the task resolves or rejects.
GET /jobs returns a cursor page ordered by job creation time, and following its cursor does not skip or repeat jobs.
Event listener exceptions are isolated: throwing from a subscribed listener does not prevent queue processing or affect delivery to other listeners.
When inserting an entry would exceed maxEntries, the least recently used entry is evicted.
For a valid page number starting at 1, offset pagination returns that page's slice and reports the total item count and total page count.
When concurrency slots become available, waiting tasks begin in their arrival order.
A request exceeding the per-x-api-key limit receives status 429 and a retry-after header expressed in whole seconds.
Duration parsing accepts ms, s, m, and h suffixes, interprets a bare number as milliseconds, and supports fractional seconds such as 1.5s.
The CLI list command prints the next cursor on its own line when another page is available.
When sleep is given an abort signal, it resolves immediately on abort; in either normal completion or abort, its timer and abort listener are cleaned up.
Notes
1 citation(s) were re-anchored to the right line before checking.
1 candidate finding(s) dropped by the skeptic review (no promise broken, or a maintainer's one-line answer holds).
Novelty not checked: no GitHub token is configured.