doug
DashboardScoreboardQueueDocsGitHubAbout
Menu
DashboardScoreboardQueueDocsGitHubAbout
Sign in

live api

Review queue

168 open. 71 need you.

threshold0.300.500.620.80

0.58

#84Restore the WorkOS sign-in front door

○ human drewjst · +768 −12 · 12 files · unapproved

  • high If NEXT_PUBLIC_WORKOS_REDIRECT_URI is unset, malformed, or has a path other than exactly '/auth/callback', configuredWorkOSRedirectUri() returns null and every request matched by the proxy returns a constant 503. This turns a config drift into a total outage of all proxied routes rather than degrading only sign-in.
  • medium requestHostMatches compares only against the single configured redirect host. Cloud Run serves both the deterministic and hash *.run.app URLs, and custom domains/health checks may present other hosts; any mismatch yields a 307 to the canonical origin, which can loop or break internal probes if the forwarded host never equals the configured host (e.g., host header rewritten by the LB).
  • medium The proxy builds `new NextRequest(url, { method, headers })` without forwarding the body or duplex stream. If handleAuthkitProxy uses authRequest to continue/rewrite the request, POST/PUT payloads (server actions, form submissions) can be lost or fail.
  • low Non-GET requests hitting a non-canonical host receive a 307 redirect; some clients/server-action flows will not replay the request correctly, and the same applies to the /sign-in route redirect logic.
  • medium Depends on `authkit` and `handleAuthkitProxy` exports from @workos-inc/authkit-nextjs (stubbed in the test loader). If these are not stable/public exports for the pinned version, the build or runtime behavior may differ from what tests exercise, since tests run against a fabricated stub.
  • medium The new post-deploy smoke asserts exact 307 codes and that the WorkOS location contains the exact encoded redirect_uri; any change in AuthKit's redirect status code or param encoding will fail production deploys even when the app is healthy.
needs you

0.58

#81Front door Phase 1a: WorkOS sessions, durable GitHub connect, and scoped dashboard

○ human drewjst · +13306 −1028 · 46 files · unapproved

  • high Migration version 9 is inserted into the list after version 10 already exists. If the migration runner tracks a single current schema version (max applied), any database already at version >=10 will skip 9, so workos_org_id / installed_by_github_user_id and the unique index are never created, and every session resolution/bind fails at runtime.
  • medium recordProviderEntitlements aborts after 2s, but the API's derivation performs GET /user/installations plus one paginated repositories call per matching installation (each with a 10s ceiling). Real accounts will routinely exceed 2s, so entitlements are never stored, leaving first-time users permanently unscoped (dashboard 403/empty) with only a console.error.
  • low finishSetupAction wraps the entire body in `catch {}` and rethrows a generic SETUP_ERROR, swallowing distinct failures (session missing, API 409 already-bound, transport error) so users and logs cannot distinguish a real conflict from an outage; also switchToOrganization is called outside the try, so its failure surfaces raw.
  • medium When no flow cookie is present, /install/callback fabricates a fresh flow with subject null and accepts the installation_id straight from the query string, so binding relies entirely on the API's installed_by_github_user_id check; any weakness there becomes an installation-hijack path since the unique org index makes the binding permanent.
  • medium deploy binds doug-workos-cookie-password / doug-workos-redirect-uri / doug-workos-client-id secrets that must be created manually; setup only WARNs when missing, so a deploy can succeed with web unable to seal sessions (every login fails) or api 503-ing all sessions.
  • low Session verification accepts a hard-coded set of WorkOS issuers with and without trailing slash and only pins client_id; a custom AuthKit domain rollout silently fails closed for all sessions unless WORKOS_ISSUER is added at the same time as noted, an easily missed coupled change.

0.55

#11Link the queue to the pull requests, and stop printing +0.00

○ human drewjst · +375 −13 · 7 files · unapproved

  • high `Reason(..., severity=f["severity"])` uses direct indexing on stored findings JSON. Rows written before `severity` existed (the 654 backfilled/legacy verdicts the PR itself calls out) lack the key, so building the QueueResponse raises KeyError and /v1/queue returns 500. Should be `f.get("severity")`.
  • low `href={pr.url ?? "#"}` renders an anchor that navigates nowhere (with target=_blank) when url is missing; harmless but the fallback silently produces a dead link rather than plain text.
  • low `_with_url` synthesizes a GitHub URL from repo/pr_number without validating that `repo` is an owner/name pair or that the source host is github.com; malformed or non-GitHub repo strings yield incorrect outbound links.
needs you

0.55

#273The reader's transport moves to Vertex, without the bar, by direction

○ human drewjst · +1227 −85 · 11 files · unapproved

  • high The Vertex request shape (effort/output_config with json_schema, and the bare `claude-opus-5` model id) was never validated against the live API. If Vertex rejects it, every read fails soft into the deterministic score with no alert; the empty-body preflight pins 400 as healthy so it cannot distinguish an unsupported body from a healthy route (acknowledged as #275).
  • medium Client construction failure (missing region/ADC, missing IAM grant on the runtime identity, wrong project resolution) surfaces only as the contracted 'reader unavailable' fallback. The deploy preflight runs as the operator credential, not doug-api-sa, so a missing roles/aiplatform.user grant or ADC project resolution issue would ship undetected.
  • medium `CLOUD_ML_REGION=$VERTEX_REGION` is added to --set-env-vars unconditionally, including on `READER_TRANSPORT=anthropic` deploys where VERTEX_REGION may be empty, producing an empty-valued env var (and a gcloud flag-parsing edge case) on the rollback/hotfix path that is meant to be the safe escape hatch.
  • low test_intent.py now asserts the incorrect selection result (['ADR-0029'] for a footer typo) as expected behaviour. This is brittle to any future ADR addition and will break or mislead once #264 is fixed; it also removes the negative-case guard for relevance scoring.
  • low The mapping-layer guard parses `_build_client` source by splitting on triple quotes and index 2, and forbids the substrings 'MODEL' and 'claude-'; harmless docstring or refactor changes can flip the assertion for reasons unrelated to model mapping.
needs you

0.55

#184feat(reader): switch grounding on for dogfood, move the mechanical tier to Sonnet 5

○ human drewjst · +912 −44 · 11 files · unapproved

  • high MECHANICAL_MODEL = "claude-sonnet-5" and MECHANICAL_EFFORT are sent to the Anthropic API but were never smoke-tested (no credential in build env, per the PR's own findings log). If the model id or effort value is rejected, verify_finding/attribute_findings raise ReaderError on every call for the now-enabled installation.
  • medium DOUG_VERIFY_INSTALLATIONS=150424894 turns grounding on in the same PR that changes the gate function and the model, so a deploy exercises three untested-in-prod changes at once; check-run output and per-review spend change for the live install.
  • low verify_enabled() was removed in favor of verify_enabled_for(installation_id); any other caller or test still referencing the old name (or the DOUG_VERIFY env var) will break silently or at import time.
  • medium Worst-case bound of 240s assumes backoff is negligible and that verify calls do not stack with the risk read inside the same synchronous request; score_one can issue up to MAX_VERIFY_READS_PER_REVIEW additional calls after the risk read, so cumulative time can still exceed Cloud Run's 300s timeout even though the per-read arithmetic test passes.
  • low Documented but unfixed: ground_findings increments its spend counter before the call, so transport failures exhaust the per-review verify budget without grounding anything — now reachable in production because grounding is enabled.
needs you

0.52

#148feat: one sticky PR comment that mirrors the check run

○ human drewjst · +3625 −105 · 31 files · unapproved

  • medium process_job now retires (supersedes) any job whose PR's base.repo.id != job['github_repo_id']. If stored github_repo_id is ever stale/incorrect (e.g. repo transfer, id recorded from a different source, or duplicate registrations), reviews for that repo silently stop with only a stderr line — this affects all installations, not just the PR-comment allowlist.
  • medium _oneline now injects zero-width spaces and drops backticks in rule slugs for ALL check-run summaries, not just PR comments. Any downstream consumer, test, or capture that string-matches rule names/labels (example packs, receipts, dashboards) will see altered bytes, and summary length grows toward SUMMARY_LIMIT.
  • medium upsert falls through to create_comment when the claim row exists but comment_id is NULL (crashed/racing drainer). With two drainers configured, this can produce duplicate comments and duplicate reviewer notifications; the 404-then-forget path can also re-notify.
  • medium Web validators were tightened to require pr_comment and pr_comment_denied_at exactly (exact key set, extra keys rejected). If the API rollback occurs or any deploy ordering slips, every dashboard load and settings save fails outright rather than degrading.
  • low ALTER TABLE installation_repos ADD COLUMN pr_comment BOOLEAN NOT NULL DEFAULT TRUE takes an ACCESS EXCLUSIVE lock; on PG<11 or under concurrent load this can rewrite/block the table. Also the new column defaults every existing repo to opted-in, relying solely on the env allowlist to prevent comments everywhere.
  • low DOUG_WEB_URL is set from $(web_url) inline in a comma-separated --set-env-vars string; an unexpected value containing a comma would corrupt the whole env var list for the deploy.

0.48

#88feat: add hosted Example Pack adjudication workbench

○ human drewjst · +7085 −69 · 43 files · unapproved

  • low _example_pack_locks is a process-global dict keyed by advisory-lock int that is never pruned; on sqlite/local paths it grows without bound per distinct (cohort, pack, finding) triple.
  • medium api.py now imports google.cloud.storage transitively at module scope (example_pack_gcs). If google-cloud-storage is missing or fails to import in the deployed image, the entire API fails to start, not just the Example Pack routes — a deploy-time all-or-nothing risk from a new dependency.
  • medium _example_pack_call maps any bare ValueError to HTTP 409 'cohort evidence is invalid'. Pydantic ValidationError and unrelated internal ValueErrors will be silently reported as data-invalid 409s, masking real bugs and confusing operators.
  • medium StorageBudget starts counting at GcsObjectStore construction and a fresh service/store is built per request; if a long-lived store is ever reused (e.g., a cached service or a worker capture spanning multiple calls) all subsequent operations raise CaptureBudgetExceeded rather than timing out per call.
  • medium record_attempt now interleaves hosted vs local branches with several early returns keyed off os.environ['DOUG_EXAMPLE_PACK_BUCKET'] and context-var state; the hosted config can be re-derived mid-call and diverge from the config captured in capture_scope_if_enabled, making capture silently drop attempts (e.g., MissingCaptureScope / attempt_kind != risk) in ways hard to diagnose.
  • low ExamplePackStore Protocol return type widened from Path to Path | str for put_pack/put_adjudication; any existing caller doing Path operations on the result (e.g., os.fspath, .name) will break when a hosted store returns a key string.
  • hmac.compare_digest on raw header strings raises TypeError for non-ASCII header values, surfacing as an unhandled 500 rather than a 403.

0.48

#30Fence claim-holder queue terminals against post-reclaim double-spend

○ human drewjst · +725 −173 · 16 files · unapproved

  • medium In the replay path, ingest.complete() is now called before check_run.post(). If the GitHub check-run post fails/raises, the job is already 'done' and any error handler's ingest.fail() will be fenced out as a no-op, so the PR silently never receives a check run and will not be retried.
  • medium release/complete/supersede/fail now require a keyword-only claim_generation and return bool. Any caller not updated in this diff (e.g. the adjudicator/drain paths or other modules) will raise TypeError at runtime; callers that ignore the new False return will treat a rejected terminal as success.
  • medium Migration 4 adds review_jobs.claim_generation as INTEGER NOT NULL DEFAULT 0 via ALTER TABLE. Safe on modern Postgres, but on older versions this rewrites/locks the table; also code reads job['claim_generation'] unconditionally, so any deployment where migrations lag behind code will KeyError in claim()/worker.
  • low _db_now uses clock_timestamp() on Postgres but the caller's datetime.now(UTC) on sqlite, so lease/cooloff comparisons mix clocks across dialects; reclaim_stalled also now re-pends running rows with NULL started_at, which could re-queue a row in a narrow window if any future path sets running without started_at.
  • low find_review now matches head_sha column OR pr_meta['head_sha']; the newly written head_sha column on the verdicts insert must exist in the schema/migrations or the save_review call with head_sha= will fail, and broadened matching could replay a verdict for rows previously excluded.
  • · Partial read: 38% of the diff (30,000 of 78,775 chars). Cut inside api/doug/worker.py. Never sent: api/tests/test_api.py, api/tests/test_deviations.py, api/tests/test_ingest.py (+5 more). Findings below cover only what was sent; a clear is not evidence about the rest.

0.48

#50Tenant API keys: repo-selection model (closes MT1/MT2/MT5; MT4 + lifecycle landing next)

○ human drewjst · +5322 −263 · 18 files · unapproved

  • low Mint rate limiting is explicitly fail-open (None count allows), so a transient DB error on the count path removes the per-installation daily mint cap.
  • medium Migration 006 issues `ALTER TABLE installations DROP COLUMN token_hash`. SQLite before 3.35 does not support DROP COLUMN and raises a syntax error that is not in _SATISFIED, which would crash-loop startup on older runtimes; Postgres path also relies on string-matching driver messages to stay idempotent.
  • medium Adding "no such column" to _SATISFIED and the heuristic `"does not exist" in msg and "column" in msg` broadens the set of DDL errors silently ignored; a future ALTER referencing a genuinely missing column/table variant could be swallowed, leaving schema drift undetected.
  • medium For selection='all' tenant keys, `effective` is computed from currently live repos; if that set is empty the code passes an empty frozenset to store.latest_reviews. If the store treats a falsy repo_ids as 'no filter', the tenant filter degrades to installation-only scoping instead of denying access — a fragile empty-set-vs-None contract.
  • low In revoke_token's repos path, store.installation_token_repo_ids(token_id) is read before verifying the token belongs to the proven installation; correctness depends entirely on GitHub repo ids being globally unique and on revoke_installation_token's installation check. Any future non-global id or missing check yields cross-tenant revocation.
  • medium TokenResponse changed shape (added token_id/selection/repos/last4/expires_at, `repo` removed) and dispense_token now requires selection semantics; existing clients relying on the old `repo` field in the response will break even though the legacy request body is accepted.

0.48

#25M2 hardening: deep-read spend cap primitive, coverage integrity, live review state, ADR-0002 cross-pin

○ human drewjst · +795 −17 · 10 files · unapproved

  • medium The new except handler in `fetch_pr` calls `print(..., file=sys.stderr)` but no `import sys` appears in the review.py diff; if the module doesn't already import sys, the fallback path raises NameError and defeats the very purpose of the guard (failing the whole PR fetch on any review-state error).
  • medium `save_review` now inserts a `prompt_hash` key, but the diff shows no corresponding Column added to the verdicts table nor a migration. If the column/DDL is absent, every persist raises and is swallowed by the caller's broad except, silently losing ledger writes.
  • medium New `deep_read_counters` table is defined in metadata with a unique constraint but no accompanying migration is visible; on an existing deployed database `record_deep_read` will fail (or the insert/update will error) unless create_all runs against production.
  • low `Coverage.complete` now returns False whenever files_dropped is non-empty, and `_dropped_files` flags any patch-less file with nonzero additions/deletions. GitHub sometimes omits patches for renames/large-but-unchanged entries, so ordinary PRs may now be reported incomplete with truncation notices appended to verdicts.
  • low `record_deep_read` returns True when storage is disabled, so the spend cap is silently unenforced in any environment without a configured ledger — a cap that fails open on misconfiguration.
  • low `fetch_open_prs` now loops list_files up to 30 pages per PR across up to `limit` PRs, multiplying GitHub API calls and increasing rate-limit/latency exposure on repos with large PRs.
  • · Partial read: 64% of the diff (30,000 of 46,558 chars). Cut inside api/tests/test_review.py. Never sent: api/tests/test_store.py, docs/REVIEWING.md. Findings below cover only what was sent; a clear is not evidence about the rest.

0.47

#281Doug is a Coldworks product, and says so in the footer

○ human drewjst · +417 −11 · 6 files · unapproved

  • high Moving `$(web_url)` from an argument into `web_base=$(web_url)` makes web_url()'s exit status propagate under `set -euo pipefail`. On a bootstrap deploy where doug-web does not exist, the fallback `gcloud run services describe` exits non-zero, so the assignment now aborts the whole api deploy instead of yielding an empty DOUG_WEB_URL — the exact tolerated case the surrounding comments and the updated test docstring still claim is supported.
  • medium The footer now links to https://coldworks.dev, which the code comment states had no DNS records at write time and is mapped by a script in a different repository. Merging before the apex is mapped puts a dead link on the two most-read public pages, a likely hotfix.
  • medium The domain lookup uses `--filter="spec.routeName=$WEB_SERVICE"` with `2>/dev/null || true`. Any filter/field mismatch, disabled API, missing beta component, or permission error is indistinguishable from "no mapping", so the new custom-domain preference silently degrades back to the run.app hostname and quietly reverts DOUG_WEB_URL on the next api deploy — the failure mode the change was written to prevent.
  • medium The EXIT trap in `cutover` is installed before the HTTPS and WorkOS pre-checks, so an early `exit 1` prints "STOPPED after: nothing" together with instructions to destroy a redirect-URI secret version that was never created, inviting an operator to roll back a live secret.
  • low The ambiguity refusal in web_url() is only effective at the one call site converted to an assignment; any other/inline `$(web_url)` use (and the `$0 $*` in the error text, which resolves to the function's empty arg list rather than the script's) still swallows the non-zero status.
needs you

0.45

#18Step-2 Tasks 1-2: App credentials + migration runner with the outcome-loop schema

○ human drewjst · +1186 −26 · 10 files · unapproved

  • medium apply() runs at engine construction on every process start; concurrent instances run ALTER TABLE on production Postgres without an advisory lock. Only the ledger insert race is handled — concurrent DDL on the same table can raise a non-'already exists' error (e.g. lock timeout or 'tuple concurrently updated') and crash startup.
  • medium _SATISFIED matches on substrings of DB error text ('duplicate column name', 'already exists'). Postgres emits 'column "x" of relation "y" already exists', which matches, but a genuinely unrelated 'already exists' (e.g. index/constraint) would also be silently swallowed; conversely locale/driver wording changes break idempotency.
  • medium If a statement mid-list fails with a non-satisfied error, earlier statements in the same version already committed (one transaction per statement) and are not rolled back; retry relies entirely on the substring matching to be idempotent. Non-atomic version application.
  • low Migration DDL and Table definitions must be manually kept identical (verdicts/outcomes new columns). Types differ subtly in intent (BIGINT vs BigInteger okay), but any future edit to only one side silently diverges fresh vs prod databases; only a test guards this.
  • low upsert_installation does read-then-insert-then-update across separate transactions; on IntegrityError it falls through to UPDATE, but concurrent writers can still interleave updates and last-writer-wins may record a stale state (e.g. 'active' overwriting 'deleted').
  • low outcome_jobs.due_at/status and review_jobs unique columns rely on create_all-generated indexes that will not exist on prod tables created by migrations; the comment acknowledges intentionally omitting indexes, which can lead to poor drain-query performance and index divergence.

0.45

#43Migration 005: unique App-path verdict identity

○ human drewjst · +902 −220 · 12 files · unapproved

  • medium Migration 005 permanently DELETEs duplicate verdicts rows plus their findings/reads/deviations before creating the unique index. If the keeper selection (MIN(id)) is not the most complete/correct row, scored data is lost with no down-path or backup step; also any FK from a table not in the 'closed set' (only asserted by a test) would fail or orphan.
  • low CREATE UNIQUE INDEX (non-concurrently) on verdicts blocks writes for the duration on Postgres; on a large production ledger this can stall webhook ingestion during deploy/cold start.
  • medium _is_app_identity_collision matches on lowercased driver message substrings ('uq_verdicts_app_identity', 'unique constraint failed: verdicts.installation_id'). A driver/version wording change, or a Postgres message that omits the index name, causes the race path to re-raise and fail a paid job instead of resolving to the peer row.
  • low The partial unique index is deliberately not declared on the SQLAlchemy verdicts table, so create_all()-built databases (tests, fresh installs) lack the constraint that production enforces — the race-floor behavior is untested against real create_all schemas unless migrations are also run.
  • low Index predicate `tier <> 'external'` evaluates to NULL for rows with NULL tier, silently excluding them from uniqueness enforcement (and from the dedupe pre-pass), so duplicate App-path verdicts with NULL tier remain possible.
  • low On the race-loser path the worker discards the locally paid intent read and deviations without persisting them; if the peer's row lacks deviations (e.g. peer crashed before save_deviations), the published check run replays an incomplete verdict.
  • Partial read: 47% of the diff (30,000 of 63,719 chars). Cut inside api/tests/test_api.py. Never sent: api/tests/test_intent.py, api/tests/test_migrations.py, api/tests/test_store.py (+4 more). Findings below cover only what was sent; a clear is not evidence about the rest.

0.45

#32Step-2 Task 10 (code): give doug-api its own identity and the App's credentials

○ human drewjst · +69 −16 · 1 file · unapproved

  • medium The new doug-api-sa is granted only roles/cloudsql.client, whereas the default compute SA carried roles/editor. Capabilities that previously worked implicitly (roles/logging.logWriter, monitoring metric write, any other GCP API the service calls) are not granted, so logs/metrics or other calls may silently fail at runtime after cutover.
  • medium deploy() now references --service-account doug-api-sa@... and --set-secrets GITHUB_APP_PRIVATE_KEY=doug-github-app-key:latest, but setup() only WARNs and continues if doug-github-app-key does not exist. A deploy run before setup/secret creation fails at gcloud run deploy time, potentially mid-release.
  • low The SA create is `|| echo` suppressed; a create failing for reasons other than 'already exists' is only caught by the subsequent describe, which can also pass/fail spuriously due to IAM propagation delays, producing flaky setup runs.
  • low --no-cpu-throttling changes Cloud Run billing/instance lifecycle (always-allocated CPU) and is required for the post-response BackgroundTasks; combined with max-instances 2 this alters cost and concurrency behavior in a way not otherwise validated.
needs you

0.45

#27Step-2 Task 6: ingest webhook deliveries, start the outcome clock, and grade third-party reviews

○ human drewjst · +2241 −36 · 6 files · unapproved

  • medium New lifespan raises RuntimeError when GITHUB_WEBHOOK_SECRET is unset, converting a previously degraded-but-serving service into a crash loop; any environment/revision (local, staging, alternate deploy path) lacking the secret will fail to boot entirely, taking down unrelated endpoints.
  • medium save_external_review deduplicates via SELECT-then-INSERT with no unique constraint, so concurrent redeliveries of the same review can both insert, double-counting a reviewer's stance in agreement metrics with no downstream repair.
  • medium worker.drain and _reconcile_then_drain are scheduled via Starlette BackgroundTasks; on request-scoped CPU / scale-to-zero these can be throttled or killed, and no periodic backstop (reconcile_all) is wired yet, so queued jobs can sit stranded until an unrelated delivery arrives.
  • medium Every accepted pull_request delivery kicks worker.drain concurrently in background tasks; without documented locking/claiming in drain, simultaneous deliveries could process the same job twice, causing duplicate paid model reads or duplicate check runs.
  • low Enqueue gate treats any non-False draft value (absent/null) as draft, so a payload variant lacking the draft field silently skips review entirely with only stderr logging and a 202.
  • · Partial read: 27% of the diff (30,000 of 111,826 chars). Cut inside api/doug/store.py. Never sent: api/doug/worker.py, api/tests/test_api.py, api/tests/test_store.py (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.45

#23Step-2 Task 5: run claimed jobs through the review pipeline

○ human drewjst · +920 −0 · 6 files · unapproved

  • medium On the replay path, intent_read is only reconstructed when both intent_alignment and coverage are non-null. For a deterministic-tier verdict (no reads row => coverage None) with recorded deviations, the replayed check run silently omits the deviation section, producing a different check run than the original post for the same commit.
  • medium Verdict reconstruction from stored rows drops fields not persisted/selected (e.g. reason severity is read but Reason(**r) must accept it; rv/reader verdict is not reconstructed), so render() on replay may differ from the original render input.
  • low find_verdict_by_identity returns rows without validating band/threshold; Band(existing['band']) will raise ValueError if a legacy/unexpected band string is stored, failing the job permanently instead of replaying.
  • medium Idempotency read is a plain SELECT with no unique constraint on verdicts (acknowledged in the docstring); two concurrent workers claiming the same job after a reclaim can both miss the row and write duplicate verdicts.
  • low gh.rest.pulls.get for head-freshness uses parsed_data.head.sha with no handling for deleted/inaccessible PRs; failure propagates into the generic retry path, burning attempts on permanently gone PRs.
  • · Partial read: 72% of the diff (30,000 of 41,907 chars). Cut inside api/tests/test_worker.py. Never sent: docs/REVIEWING.md, docs/design/outcome-loop/ROADMAP.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.45

#76Unify npm workspaces and add console search/paging

○ human drewjst · +7409 −10596 · 30 files · unapproved

  • medium web/console deploys switch from `gcloud run deploy --source` to Cloud Build + `--image`. First run depends on the Artifact Registry repo `doug` existing (created only in setup()) and on the deployer having cloudbuild/artifactregistry permissions; any gap fails the post-merge web deploy mid-merge, and nothing in CI exercises `gcloud builds submit`.
  • low .dockerignore/.gcloudignore exclude `**/*.md`, `docs`, and `api` at the repo root. If either Next app ever reads markdown/content or shared files outside its own directory, the Cloud Build upload and docker context silently omit them, producing a build that differs from local `next build`.
  • medium Standalone output now nests under web/ or console/; the final stage relies on COPY of `/app/<app>/public` and CMD `node <app>/server.js`. If an app has no public/ directory (console/public is .gitkeep-only) or tracing places server.js elsewhere, the image builds/starts incorrectly only at runtime.
  • medium The builder shells out to `node -p require('./web/package.json').optionalDependencies[...]` and then `npm install --no-save` for musl bindings. If those optionalDependencies keys are ever renamed/removed the substitution yields `undefined` and `npm install pkg@undefined` fails the image build; the arch mapping also assumes only arm64/x64 exist.
  • low Search/paging state is written with raw `window.history.pushState` rather than router navigation. This depends on Next's pushState/useSearchParams sync behavior; on a version bump or in the two independent jobs tables it can leave the rendered page out of sync with the URL after Prev/Next or blur.
  • low Root package.json/package-lock.json changes now trigger a web deploy, but the console service is deliberately not deployed by CI. A root lockfile change can therefore ship a web image built from a dependency graph the deployed console image never gets, requiring a manual rebuild that is easy to forget.

0.42

#12Record how much of the diff the reader actually saw

○ human drewjst · +628 −33 · 11 files · unapproved

  • medium `_q(cov.files_unseen)` passes a Python list to a quoting helper presumably written for strings; unless _q JSON-encodes lists, this emits Python repr (single quotes, possibly embedded quotes from file paths) into a JSON column, producing invalid or unparseable SQL for the backfill.
  • medium score_one's return arity changed from 3 to 4; callers outside the diff (backtest/replay, other scripts, CLI) that still unpack three values will fail at runtime.
  • low _FILE_HEADER regex with re.M can match lines inside a patch body (e.g. a diff of a file that itself contains a '### path (modified, +1/-0)' line, such as this very HANDOFF/test corpus), inflating file counts and mislabeling files_unseen/file_cut.
  • low The emitted `INSERT INTO reads ... SELECT id FROM verdicts WHERE repo/pr_number/model ORDER BY id DESC LIMIT 1` relies on an assumed uniqueness that is not enforced by a constraint; a partial prior backfill or concurrent insert attaches coverage to the wrong verdict row.
  • low save_read() is retained but no longer called (coverage folded into save_review), leaving a second write path that can create duplicate reads rows if reused.
needs you

0.42

#251A transferred repository keeps its outcomes, and its history

○ human drewjst · +1430 −22 · 12 files · unapproved

  • medium After a transfer, `_repository_identity` now returns the SUCCESSOR's `full_name` instead of the old junction row's name. Outcomes/verdict rows written afterwards will carry the new repo string while historical verdicts keep the old one; downstream joins that key on `outcomes.repo`/`verdicts.repo` (e.g. the outcome_14/outcome_60 join in `run_history`, which builds keys from verdict rows) can silently fail to match, so a transferred repo's runs may show no outcome even after the repair.
  • medium `_serialise` stringifies every datetime column of the outcomes row, but `rollback` only converts `observed_at` back to a datetime. Any other timestamp column in `outcomes` (present on Postgres but not exercised by the sqlite tests) would be re-inserted as a raw string, risking an insert failure or wrong value during an incident rollback.
  • medium `_repository_evidence` now mints a GitHub token for `reader_installation_id` (the successor installation) to fetch git history for jobs owned by a different installation. If the successor's app grant lacks access (private repo, restricted permissions), previously-terminal jobs will now retry up to MAX_ATTEMPTS and consume the successor's rate limit; the outcome is still written under the old installation, which no other reader can scope to except through the new lineage.
  • low `_tenant_ids` raises ValueError when both `installation_id` and `installation_ids` are supplied. Any existing or future caller of `latest_reviews`/`run_history`/`run_detail` that forwards both (e.g. through kwargs defaults) turns a scoping mistake into an uncaught 500 rather than a safe narrow filter.
  • medium Read visibility now spans every installation that ever registered a repo the caller currently holds. Safety relies entirely on the `repo_ids` pairing being enforced at every call site; `queue` passes lineage whenever `repo_ids is not None`, so any future path that passes an unproven or over-broad `repo_ids` would expose another tenant's rows.

0.42

#64Build the M3 adjudicator job and scheduler

○ human drewjst · +2142 −146 · 20 files · unapproved

  • medium deploy() now unconditionally calls adjudicator(), which requires doug-adjudicator-sa and the secrets bindings created only by the manual adjudicator-setup step. If that operator step hasn't run (or runs in a different project), every API deploy will fail after the API revision has already been promoted, leaving CI red and pipeline state half-applied.
  • medium prereg_hash is computed from a hardcoded relative path '../docs/design/outcome-loop/publication-preregistration.md', so gcp.sh only works when invoked from api/. Invoking from the repo root (or any other cwd) aborts the deploy or, if error handling differs, ships an empty DOUG_PREREG_HASH that makes the job fail at runtime.
  • medium clone_treeless's refresh fetch changed from check=False to check=True. Any existing caller relying on tolerant refresh (backtest CLI with a stale/offline cache) will now raise CalledProcessError instead of proceeding with cached history.
  • low reclaim_stalled() resets status to pending without incrementing claim_generation, and runs at the start of drain in the same execution; a still-live holder from a slow prior execution whose lease expired could have its rows reclaimed and re-claimed while it is mid-clone. Fencing still relies on status/generation checks at settle time, but the reclaim window depends purely on the 2h lease being longer than real work.
  • low claim_repository raises LostClaim if the update rowcount differs from the locked row count; this escapes drain and fails the entire Cloud Run execution rather than skipping the repository, so a single concurrent mutation aborts the whole daily run.
  • · Partial read: 87% of the diff (100,000 of 115,345 chars). Cut inside docs/superpowers/plans/2026-08-06-m3-adjudicator-job-scheduler.md. Findings below cover only what was sent; a clear is not evidence about the rest.

0.42

#202fix(worker): retry a sticky PR comment that never landed

○ human drewjst · +1614 −27 · 8 files · unapproved

  • medium The 15-minute settle window is only a heuristic for worker liveness (acknowledged in-code/ADR). A first worker that is paused or in long GitHub backoff can still be inside the gap between ingest.complete and the outcome write when the sweep claims the row; both then call pr_comment.upsert and, if the listing cannot yet see the peer's create, a second comment (and notification storm) lands on a live PR — the exact harm the complete-before-post ordering exists to prevent. record_pr_comment_outcome is also unfenced, so the slow original writer can overwrite the sweeper's recorded outcome and re-arm or hide a repair.
  • medium Migration 16 runs a plain CREATE INDEX (no CONCURRENTLY, inside _run's transaction) on review_jobs, which on Postgres takes a SHARE lock for the whole build while boot holds it, blocking webhook enqueue. review_jobs is described as the table where 'virtually every row is done', so build time scales with all history; only tracked as issue #204, not mitigated here.
  • low _replay_recorded passes owes_comment=True unconditionally. Any completion path that returns before actually invoking _post_pr_comment (e.g. a comment-feature short circuit above it, an exception between complete and the post) leaves the 'owed' marker standing, so the sweep later mints an installation client and attempts a comment for a job/tenant that may not want one, spending a repair budget to record skipped:off.
  • low retry_unposted_comments passes raw review_jobs rows to _render_recorded/_instrument/_post_pr_comment, which previously received ingest.claim's enriched dict. Any field those helpers read that claim derives rather than selects (beyond claim_generation) raises at runtime; the raise is swallowed and only surfaces as failed:internal plus a spent retry, so the repair path can be silently dead in production while unit tests pass.
  • low drain now runs the sweep on every delivery, adding an unindexed-window SELECT plus per-job installation client mints and pulls.get calls on the shared installation token's rate limit; issue #203 notes the per-job client minting is unfixed, so a batch of 20 repairs per drain can compete with review traffic for rate limit.

0.42

#100fix(api): report a stale session scope as reauthorize_required instead of dropping it

○ human drewjst · +139 −7 · 3 files · unapproved

  • medium New `status` value `reauthorize_required` is emitted where clients previously only saw `ready|setup_required`. Per the repo's own findings log, deployed web validators treat unknown statuses as an outage, so merging/deploying this before the web-side tolerance change breaks the dashboard for any user with a stale scope.
  • low The stale branch reports `reauthorize_required` unconditionally, ignoring `organization_id`. An unbound installation (previously `setup_required`) whose scope has expired is now labelled as needing reauthorization, which may direct the user to a re-auth flow that cannot resolve a missing org binding.
needs you

0.42

#14Close the two live auth holes

○ human drewjst · +114 −26 · 6 files · unapproved

  • medium deploy() now references secret doug-webhook-secret via --set-secrets, but setup() only adds IAM bindings (with errors swallowed by `|| true`); no creation step is shown. If the secret doesn't exist, the Cloud Run deploy fails or the service starts without GITHUB_WEBHOOK_SECRET, making all webhooks 503.
  • medium /v1/queue now requires X-Doug-Token; any existing consumer (CLI, scripts, other clients) not updated will get 401. Only the Next.js server component was updated.
  • medium getQueue swallows 401/503 and falls back to bundled fixture data, so an unset DOUG_API_TOKEN in the web deploy silently shows stale/fake queue data rather than erroring.
  • low The webhook now returns 503 when GITHUB_WEBHOOK_SECRET is unset instead of accepting; existing environments without the secret will start rejecting all deliveries, causing GitHub delivery failures until the secret is provisioned.
needs you

0.42

#164Walked Out v1: hunk-evidence convergence — no resolved state, published silence count

○ human drewjst · +2065 −120 · 22 files · unapproved

  • medium convergence.classify/compare gained a required positional `prior_read` parameter. Any caller not updated in this commit (or added later) will raise TypeError at runtime on the paid-read path; only store.py and the eval script were updated, enforced by a structural test rather than the type system.
  • medium convergence_for() issues 4+ extra queries (verdict lookup, prior-verdict scan, two finding fetches, two read fetches) after every reader-tier check run render, including the replay path. The prior-verdict lookup filters on (installation_id, github_repo_id, pr_number, tier) with a lexicographic (scored_at,id) predicate; without a supporting index this can degrade on large verdicts tables.
  • low attribute_findings performs a charged model call inside score_one on the reader path; _charge(scope) happens before the try body's client creation but the whole block is wrapped in a broad except that returns 0 — a persistent failure will silently burn the attribution spend cap with no attributions, only stderr noise.
  • low _sent_file_patches re-derives per-file patch geometry from the diff and self-checks against cov.hunks; any divergence (e.g. chunk separator/format change, empty-patch edge with m.end()+1 > content_end) silently disables attribution for the whole read rather than surfacing the drift.
  • low hash_hunk includes any line starting with '+' or '-' from the hunk body, which will also catch '---'/'+++' file-header lines if a caller passes text where they appear after a '@@' line; also 'pair_delta' compares whole indexes by dict equality so pure hunk reordering reports 'changed-elsewhere'.
  • low The Since section is rendered on every reader check run and contains substantial generated prose with count/plural logic; a miscount (e.g. persisted rows with basis None and code_changed None excluded from the denominator) yields misleading customer-visible copy.

0.42

#104feat(api): reconcile the outcome lane against missed merge webhooks

○ human drewjst · +1839 −8 · 10 files · unapproved

  • medium reconcile_all_outcomes issues one pulls.get per merged PR inside a 14-day window, per repo, per installation, and runs on every cold start (scale-to-zero => frequently) plus every 6h Job plus every installation.created. The installation token's rate limit is shared with the user-visible review lane; a busy tenant can burn it and cause review job failures that then wait out FAILED_REVIVE_COOLOFF_SECONDS. The ordering-after-drain mitigation only helps within a single startup pass.
  • medium _reconcile_then_drain calls worker.reconcile_outcomes(installation_id) with no try/except, unlike reconcile_all_outcomes which isolates per-tenant failures. A raise from app_auth.installation_client or store during the installation.created path will propagate out of this helper; behavior depends on the caller's wrapping, which is not shown as protected.
  • medium Reconciliation can now create outcome_jobs rows for merges that occurred before the App was installed (within the 14-day lookback), invalidating publication-preregistration.md 2-less merges cannot exist. The code acknowledges no metric reads this yet, but the ledger now contains rows the adjudicator will process and verdict on merges Doug never reviewed.
  • low _MAX_CLOSED_PRS_PER_REPO=300 with updated-desc sort means a repo that exceeds the cap will permanently never reconcile its excluded tail on any pass (self-documented). Only a stderr log surfaces this; there is no metric or alert.
  • low The Job uses --task-timeout 3600s with --max-retries 0 and store.active_installations() has no ORDER BY / cursor, so a timeout means later-sorting tenants are never reconciled and the failure looks like an ordinary timeout rather than a coverage gap.
  • · Partial read: 94% of the diff (100,000 of 106,448 chars). Cut inside docs/superpowers/plans/2026-08-12-outcome-lane-reconciliation.md. Findings below cover only what was sent; a clear is not evidence about the rest.

0.42

#187feat(reader): route committed docs data as prose, raise EFFORT to high

○ human drewjst · +472 −14 · 8 files · unapproved

  • medium EFFORT raised from "medium" to "high" with no pre-registered run; raises thinking tokens and latency per read against a 120s per-attempt timeout and 300s request budget. A latency regression would surface as increased reader-unavailable fallbacks in production.
  • medium _is_prose now demotes any docs/*.{json,jsonl,csv,yaml,yml} to prose tier, changing which files reach the model. Non-contract but genuinely load-bearing data under docs/ (e.g. docs/config/*.yaml, generated fixtures, CI configs stored under docs/) will be deprioritized or cut; the _CONTRACT_STEMS allowlist only matches the first dot-segment, so names like docs/api-openapi.json or docs/v2-schema.yaml are not excepted.
  • low test_the_preregistered_coverage_bar_still_holds_at_the_shipped_budget invokes read_budget_gate.main() in CI and asserts an exact string "all code sent whole on 30/30 (100%)" over the 30 first-parent commits ending at a pinned SHA; this couples the test suite to git history/fixture availability and will break as soon as the tiering or sample drifts.
  • low test_the_paths_that_inherit_the_raised_effort_are_enumerated regex-scans reader source for '"effort": EFFORT' and asserts a count of 2 while the docstring claims three consumers; the third is matched only by the separate 'effort=EFFORT' substring check, so any harmless formatting change (line wrapping, keyword ordering) breaks the test.
needs you

0.40

#102feat(web): Lane 1 Phase B, PR 2 — rebuild the dashboard on the console's design grammar

○ human drewjst · +1637 −463 · 15 files · unapproved

  • medium --rule-soft/--dim/--row-hover are now declared only on .dashboard-surface. Any component rendered outside that wrapper (portal, dialog, extracted subcomponent) silently loses row dividers/hover tint. Guarded only by a repo-walking test, which won't catch runtime portals.
  • medium dashboardFilters replaces the closed-vocabulary band/tier normalization with parseFacetSelection, so previously ignored values (?band=foo, ?tier=bogus) now act as real constraints and yield an empty table. Behavior change for existing shared links, mitigated but not eliminated by zero-count ghost pills.
  • low Per-PR history disclosure is a sr-only checkbox plus :has(); it announces as a checkbox rather than a disclosure, has no aria-expanded, loses state on navigation, and depends on :has() support with a fail-open fallback that hides the caret entirely.
  • medium A ~190-line CSS module is deleted and replaced wholesale with inline Tailwind arbitrary-value utilities (column widths, sticky header, responsive max-[900px] variants, dot-grid surface). Layout/theme regressions here are not detectable by the unit tests added.
  • low Facet pills set aria-current="true" on filter <Link>s that are not the current page/step, and sortable headers rely on aria-sort on <th> containing a link; both can mis-announce state to assistive tech.
  • · Partial read: 68% of the diff (100,000 of 146,517 chars). Cut inside web/app/dashboard/page.tsx. Never sent: web/lib/facets.test.mjs, web/lib/runs-time.test.mjs, web/lib/console-lockstep.test.mjs (+5 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.38

#277The manifest names the mechanical tier, so a swap cannot pool two eras

○ human drewjst · +210 −0 · 9 files · unapproved

  • medium `mechanical_parameters` is added as a required field on the persisted `WholeInstrumentManifestV0` with no default and no version bump, so any previously written manifest JSON (packs, hosted corpus entries) will fail Pydantic validation when re-read. The comment explicitly rejects a default, but that trades silent mislabeling for a hard read failure on historical data with no migration path shown in the diff.
  • medium `record_attempt` gains a required keyword-only parameter. Only reader.py's `_record_attempt` is updated here; any other production caller (e.g. hosted/intent capture paths) will raise TypeError at capture time. Worth verifying all call sites were covered, since only tests were otherwise touched.
  • medium Adding a component to the instrument hash changes `instrument_id()` for all reads, so existing corpus partitions split at deploy time even though the mechanical tier did not change. Downstream consumers of example_pack_eval partitioning may see empty/duplicated populations until re-capture.
  • low `mechanical_parameters()` reports static module constants rather than the request actually sent; if verify/attribute call sites ever accept per-call model/effort overrides, the manifest will confidently describe a tier that never ran. Only a test guards this invariant.
needs you

0.38

#103feat(web): bounded shadcn table, a threshold view lens, a larger type scale, and a space picker that navigates

○ human drewjst · +3446 −946 · 22 files · unapproved

  • medium AutoSubmitSelect commits (requestSubmit → space switch, full navigation) on blur whenever a keyboard-originated change is pending. A user who arrows the closed select and then clicks anywhere else on the page will silently switch organizations; Escape is the only escape hatch and depends on entryValue being captured on focus (which won't happen if focus was set programmatically without a focus event fired through React).
  • medium applyLens rewrites run.band at the boundary, so search (runMatchesQuery) and facet counts now reflect the lensed band rather than the recorded verdict. Intentional and documented, but it makes shared URLs with ?threshold=… report band-based query matches that contradict the run detail pane's recorded verdict — a likely user-reported inconsistency.
  • low serializeThresholdLens uses String(lens) while ThresholdGear submits draft.toFixed(2). A lens applied via the gear ("0.30") and one produced by thresholdChanges from a parsed float could render different param strings for the same view, and the Clear <Link> path relies on parse/serialize symmetry that isn't enforced against the gear's formatting.
  • low The contract test now matches an exact CSS selector sequence ':root,\n.dashboard-surface,\n.paper-tokens' in globals.css; any formatter or selector reorder breaks CI without any real regression.
  • low Swapping hand-rolled td/th for shadcn TableCell introduces whitespace-nowrap plus a sticky header inside a max-h-[55vh] scroll container with border-separate. Sticky headers with separated borders and per-cell borders are fragile across browsers; column overflow/truncation behavior for API-supplied strings changed in several cells.
  • · Partial read: 39% of the diff (100,000 of 255,338 chars). Cut inside web/lib/dashboard-contract.test.mjs. Never sent: docs/superpowers/specs/2026-08-12-dashboard-ux-design.md, HANDOFF.md, docs/superpowers/plans/2026-08-12-dashboard-ux.md. Findings below cover only what was sent; a clear is not evidence about the rest.

0.38

#114feat(web): rebuild the dashboard as a rail/ledger/dock shell, census the ledger, and make Repositories real

○ human drewjst · +2459 −335 · 7 files · unapproved

  • · Partial read: 57% of the diff (100,000 of 175,570 chars). Cut inside web/app/dashboard/page.tsx. Never sent: web/lib/dashboard-contract.test.mjs, web/lib/ledger-census.test.mjs, HANDOFF.md (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
  • low repositoryTable maps over the raw `connected` array; if the connections list contains a repo twice (or differs in case), the first row gets the rollup and the second renders zeroed counts with the same key, producing a duplicate React key and a misleading 0-run row.
  • medium bandCensus counts any band that is not exactly "flagged" as `cleared`, so a new or unknown band value would silently be reported as cleared — the opposite of the allowlist-refusing rule applied to outcomes elsewhere in the same file.
  • low Many hardcoded column widths were shrunk (score 78→58, tier 88→64, job 118→76) and the table min-width lowered to 940px based on manual measurement; a longer tier/job/outcome string or a different font metric will truncate or overflow in production.
  • low The 1620px dock breakpoint is duplicated literally across five class strings; any future change requires editing all sites and a missed one silently yields a broken two-column/one-column mix (the failure mode is invisible at build time, as the comment itself notes).
  • medium severityCensus and repoRollup dereference run.finding_counts.total without a null guard; if the API omits finding_counts for any row (older records or partial serialization) the whole dashboard render throws server-side.
needs you

0.38

#279The reader holds no API key: Workload Identity Federation on the first party

○ human drewjst · +742 −21 · 10 files · unapproved

  • medium The federation exchange (metadata-server ID token fetch, audience match, Anthropic rule validation) can only be verified on Cloud Run; a misconfigured rule, wrong audience, or archived service account makes every first-party read fail soft into the deterministic score rather than erroring loudly.
  • medium ADR-0028/0029's one-command transport rollback (DOUG_READER_TRANSPORT=anthropic on the running service) now silently depends on federation working, since the key mount is gone. If federation is broken, the transport rollback no longer restores service and requires a second, also-untested --update-secrets step during an incident.
  • low --set-secrets is declarative, so any subsequent deploy silently drops an emergency ANTHROPIC_API_KEY mount and returns the service to federation. This is documented as intentional in OPERATIONS.md but remains an operational foot-gun during a prolonged incident.
  • low Relies on anthropic.WorkloadIdentityCredentials and the credentials= constructor kwarg existing in the resolved SDK (floor >=0.120.2); a floating upper bound means a future SDK release renaming/removing these breaks client construction, though a contract test now guards the installed version.
  • low _google_identity_token() hits the GCP metadata server on every exchange with no caching or error wrapping; a transient metadata-server failure surfaces as an unhandled exception inside client credential resolution and falls through to soft fallback.
needs you

0.38

#213feat(web): separate the items, and let the console go dark

○ human drewjst · +988 −291 · 16 files · unapproved

  • medium Every palette token in both light and dark blocks is replaced at once (background, card, border, input, sheen, charts, atmosphere, radius), and the console is newly allowed to render dark. All verification is source-text assertions — no rendering — so any component that implicitly assumed the old warm/light values (chips, hover states, chart series, dividers) can regress unnoticed and need a hotfix.
  • low Surface-scoped tokens (--rule-soft, --dim, --row-hover, --surface-dot) are declared only on .dashboard-surface / .dark .dashboard-surface, not on .surface-tokens. Content Radix portals out of the wrapper (the threshold gear popover) now resolves the palette correctly but any use of these tokens inside it silently resolves to nothing, and the new dark path widens the blast radius of that gap.
  • low The 'never read' hatching moves to Tailwind arbitrary values embedding var() inside repeating-linear-gradient (bg-[repeating-linear-gradient(135deg,var(--cov-unread)_0_1.5px,...)]). This depends on Tailwind's arbitrary-value parsing/underscore-to-space handling; a mis-generated class fails silently (no hatch, no border) rather than erroring, and it must stay byte-identical to console's file under the lockstep test.
  • low New tests assert exact hexes, exact multi-selector shapes, and scan all app/ and components/ .tsx files for any 6-digit hex with only comment stripping and a hand-maintained exemption list. Any legitimate literal colour (SVG assets, og-image, meta theme-color) or a whitespace/format change in globals.css will break CI, inviting follow-up fix commits.
  • low ThemeMenuItem renders the light-mode wording until after hydration (useSyncExternalStore mount guard). The justification is that the row sits in a collapsed <details>, but a server-rendered open details, prefetch/restored scroll state, or slow hydration would briefly show 'Switch to dark' to a user already in dark mode and click-toggle to the wrong target.

0.38

#38Show App and CI review runs side by side

○ human drewjst · +2609 −26 · 15 files · unapproved

  • medium `find_review` now requires installation_id and github_repo_id to be NULL. Any caller path where an App-originated verdict previously suppressed a rescore (e.g. webhook redelivery routed through /v1/review, or a future/edge caller) will now re-score, doubling LLM cost and inserting duplicate verdict rows.
  • medium comparison_reviews raises ComparisonResultTooLarge (HTTP 413) when a PR group set exceeds 500 rows. A single busy repo with many duplicate App writes will make the dashboard return an error with no partial data and no client-side fallback documented, effectively breaking /compare until the user narrows the query.
  • low The comparison query joins verdicts against a grouped subquery on (repo, pr_number) plus a max(reads.id) group-by subquery over the whole reads table with no time bound; on a large ledger this can be a slow full scan per request.
  • low _comparison_run indexes row["head_sha"], row["score"], row["band"], etc. directly and only guards pr_meta/coverage; a legacy row missing an expected key or a coverage dict lacking one of the five keys would raise KeyError rather than degrade.
  • · Partial read: 27% of the diff (30,000 of 112,179 chars). Cut inside docs/REVIEWING.md. Never sent: docs/superpowers/plans/2026-08-01-dual-run-comparison-dashboard.md, docs/superpowers/specs/2026-08-01-dual-run-comparison-dashboard-design.md, web/app/compare/page.tsx (+6 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.38

#56feat: route read budget by file tier and raise it to 100k

○ human drewjst · +2479 −54 · 25 files · unapproved

  • medium read_order() reorders the assembled diff (code, then tests, then prose; smallest patch first), changing the model's input ordering versus the validated probe configuration. Combined with the 100k budget, prior AUC/threshold calibration (DEFAULT_READER_THRESHOLD=30) no longer applies and reader verdicts may shift in production.
  • medium DIFF_BUDGET raised from 30k to 100k chars while DEFAULT_READ_TIMEOUT_S stays at 120s. Large PRs now send up to ~3.3x more prompt content, increasing the chance of read timeouts (silent fallback to deterministic score) and raising per-read cost for the tail of large PRs.
  • low review.py depends on the private helpers features._is_prose and features._is_test for routing. Any future change to these scoring-oriented helpers (e.g. adding suffixes or manifest names) will silently alter read-budget selection, coupling scoring vocabulary to reader routing.
  • low _is_prose treats any .md/.txt/.rst plus LOCKFILES as prose, with only a narrow allow-list (MANIFESTS, CMakeLists.txt, requirements/constraints*.txt). Other code-bearing text files (e.g. *.cmake, setup.cfg-style .txt entry points, SQL-in-.txt fixtures) will be demoted to the last tier and can be cut from the read without being obviously wrong to reviewers.
  • · Partial read: 20% of the diff (30,000 of 147,807 chars). Cut inside api/doug/settle.py. Never sent: api/scripts/backfill_ledger.py, api/scripts/read_budget_gate.py, api/tests/test_coverage.py (+14 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.38

#198feat: per-repo deep read, one rail on every dashboard route, and switches that read as switches

○ human drewjst · +1685 −537 · 25 files · unapproved

  • medium `repository()`/`isRepositorySettings()` were tightened from optional to `exact([... "deep_read"])`, so any connections/settings body lacking `deep_read` (rolled-back API revision, canary, stale instance, or any other API code path that projects repositories without the new key) now fails validation and degrades the entire dashboard to an unreachable-ledger error rather than one missing toggle. Deploy order protects promotion but not rollback.
  • medium The account menu, MENU_ITEM, ScopePicker and connectionLabel moved into components/dashboard-rail.tsx, but `signOutAction` (and possibly other now-unused imports such as AutoSubmitSelect/NoJsSubmit/DougLogo removals' companions) still appears imported in dashboard/page.tsx; with strict lint/TS unused checks this fails the build.
  • low The exported `deepRead()` helper was deleted; any remaining importer outside the files touched here (components rendering repository rows) would break compilation.
  • low deep_read=false now also suppresses the intent read and drops scoring to the deterministic tier, which on repos with no explicit flag line silently moves the band from DOUG_READER_THRESHOLD to DOUG_THRESHOLD. Intended and documented, but it changes verdict output for any repo toggled off and could surprise users as fewer 'needs you' flags.
  • low `ALTER TABLE installation_repos ADD COLUMN deep_read BOOLEAN NOT NULL DEFAULT TRUE` is cheap on PG11+/SQLite but will rewrite/lock the table on older Postgres; also, the app code reads the new column immediately, so a partially applied migration during deploy would raise on `repo_deep_read` selects.
  • · Partial read: 58% of the diff (100,000 of 173,342 chars). Cut inside api/tests/test_review.py. Never sent: api/tests/test_store.py, api/tests/test_api.py, web/lib/session-api.test.mjs (+5 more). Findings below cover only what was sent; a clear is not evidence about the rest.

0.38

#15Close the reliability gaps the multi-agent review surfaced

○ human drewjst · +890 −49 · 20 files · unapproved

  • medium The replay path constructs ReviewResponse manually with only score/band/threshold/reasons/deviations/intent_* while the fresh path goes through _score_and_persist; if ReviewResponse (or Verdict) carries other required fields (e.g. tier, pr_url), a redelivered webhook returns a pydantic validation error instead of the recorded verdict. Replayed responses also silently omit `tier`, which find_review does return.
  • low _inflight_review pops the per-(repo,pr,sha) lock after `with lock:` without try/finally, so any exception during scoring leaves the key permanently in _inflight_locks — unbounded dict growth in a long-lived process.
  • medium find_review filters on verdicts.c.pr_meta['head_sha'].as_string(), which depends on JSON-path support in the configured backend (works on Postgres/JSONB, can fail or never match on SQLite/older setups). The API wraps this in a bare except so failures degrade to duplicate paid reads rather than an error, meaning a broken query silently defeats the entire idempotency feature.
  • low _get_engine disposes the previous engine when DATABASE_URL changes; connections checked out by other threads from the old pool can be invalidated mid-request. Rare but a hard failure when it happens.
  • medium Staged deploy relies on unquoted $traffic_flags word-splitting, a python3 parse of gcloud JSON to find the candidate-tagged URL, and `service_exists && traffic_flags=...` under set -e semantics. If the candidate tag URL isn't yet present in status.traffic when described, promote_if_healthy aborts and the deploy job fails even though the revision is fine.
  • low Deduplication is in-process only, so with max-instances 2 concurrent webhook redeliveries hitting different instances still double-charge and insert duplicate ledger rows; downstream consumers must tolerate duplicates as documented but any that don't will double-count.

0.38

#87fix: resolve Dependabot alerts for brace-expansion and hono

○ human drewjst · +42 −109 · 1 file · unapproved

  • medium Lock entries for workspace deps changed from "^16.3.0" to "16.3.0" but no package.json changes are included in the diff; if package.json still declares ^16.3.0 the lockfile is out of sync and `npm ci` will fail.
  • medium Removal of `libc: [glibc]/[musl]` metadata across many optional platform packages indicates the lockfile was regenerated by an older npm than the one that produced it, which can result in incorrect optional native binaries being installed on musl/glibc images.
  • low hono moved from web/node_modules 4.12.32 to hoisted root 4.13.1; hoisting plus a minor bump can change resolution for other workspaces that previously relied on a different version.
needs you

0.35

#74fix(web): add security headers, retire /compare link, and setup npm workspaces

○ human drewjst · +198 −4 · 6 files · unapproved

  • medium docs/REVIEWING.md ends with a stray '>>>>>>> origin/main' conflict marker, indicating an unresolved merge was committed; likely also means other conflicted hunks were resolved by accident (e.g. the findings-log line deletion).
  • low The 'backtest-*.json' ignore rule was replaced by '.worktrees/' rather than added alongside it, so backtest artifacts can now be accidentally committed.
  • low A findings-log.jsonl entry (pr 56 reader:temporary-global-mutation) is deleted with no stated reason, in an otherwise append-only log — consistent with a bad merge resolution.
  • medium Title claims npm workspaces setup, but no root package.json is included; the findings log records that root workspaces previously broke isolated 'npm ci' in web/console Dockerfiles and CI, so any re-introduction is build-breaking.
needs you

0.35

#157feat(check-run): one needs-you alert, and the caveats move below the findings

○ human drewjst · +371 −43 · 3 files · unapproved

  • medium GitHub alert callouts ([!IMPORTANT]/[!WARNING]) are used in check-run summary markdown, but the handoff states this rendering was never verified in a check run (only in PR comments). If unsupported, the block degrades to a quote showing literal '[!WARNING]', likely triggering a hotfix.
  • low The summary table cells are guarded against pipes, but the alert body splices `_oneline(partial.label)` which contains file paths; _oneline must guarantee no newlines or the alert marker silently demotes to a plain blockquote. This invariant is asserted only for one fixture, not enforced in _alert_block.
  • low _finding_counts reads r.severity on every Reason including deterministic-tier reasons where severity is `str | None` and unvalidated; a non-Reason-shaped object or missing attribute would raise during rendering of the check run.
  • medium pr_comment.py mirrors the check-run summary verbatim; the layout/table/alert changes were made here without any corresponding change to the mirror, which may break frame length assumptions (FRAME_MAX noted as still open in HANDOFF).
needs you

0.34

#48Per-installation tokens and tenant-scoped queue reads (M2's last item)

○ human drewjst · +2499 −37 · 11 files · unapproved

  • medium tenancy.mint/resolve depend on store.installations.c.token_hash, but no schema/migration change appears in this diff. If the column or its migration is not already deployed, every mint/resolve raises at runtime and both /v1/installations/token and /v1/queue tenant auth break.
  • medium /v1/installations/token is intentionally public with no rate limiting; each request performs a GitHub API call with the caller-supplied PAT and can repeatedly rotate an installation's token, silently invalidating a tenant's live credential (last-writer-wins with no confirmation step).
  • low _operator_only changes the response for a resolvable tenant token on /v1/patterns, /v1/comparisons and /v1/score/read from 401 to 404. Existing clients or monitoring that key on 401 for auth failures may misclassify these, and the extra tenancy.resolve() DB lookup now runs on every failed auth attempt of these hot endpoints.
  • medium Tenant-scoped latest_reviews requires installation_id equality, so CI-written rows with NULL installation_id for a tenant's own repos are entirely invisible in the tenant queue (not just superseded). Tenants using the CI path will see an unexpectedly empty or partial queue.
  • · Partial read: 24% of the diff (30,000 of 127,535 chars). Cut inside api/tests/test_tenancy.py. Never sent: docs/REVIEWING.md, docs/design/outcome-loop/ROADMAP.md, docs/findings-log.jsonl (+2 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.34

#80Implement Doug Example Pack v0

○ human drewjst · +4762 −34 · 19 files · unapproved

  • medium In read_diff and read_with_decisions the model text extraction (`next((b.text for b in response.content ...), "")`) now runs before the `stop_reason != "end_turn"` check so raw bytes can be captured. If a non-end_turn response has `content` as None or blocks lacking `.text`, an AttributeError/TypeError escapes instead of the previous ReaderError, bypassing the module's fail-loud-but-typed contract.
  • low Every reader exit path now invokes _record_attempt, which builds coverage (re-running coverage() over the diff) and canonicalizes structures. Although capture is guarded by env checks, the added call sites multiply the surface where a capture bug could raise inside the review path; only the outer generic except protects it.
  • low score_packs classifies both missing adjudications and 'unknown'/'disproved' dispositions into `unsupported`, so unadjudicated findings inflate the burden metric identically to disproved ones; gate outcomes from evaluate_control_gates may therefore be misleading.
  • medium verify_pr78_fixture hard-codes expected production enqueue caller counts (api.py:1, worker.py:2) and exact doc line ranges/hashes; any unrelated refactor or doc edit makes the verifier raise, requiring a follow-up fix.
  • · Partial read: 49% of the diff (100,000 of 202,429 chars). Cut inside api/tests/test_worker.py. Never sent: api/tests/test_example_pack_eval.py, api/tests/test_example_pack.py, api/tests/test_reader.py (+3 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.34

#45Enforce findings-log rates and settle false missing-imports

○ human drewjst · +771 −6 · 10 files · unapproved

  • medium looks_like_missing_import_finding treats any description containing 'import' plus 'missing'/'without'/'not imported' as a missing-import claim. Findings like "imports `foo` but the module foo is missing" or "`Y` imported from x is missing" satisfy the gate, and since `foo`/`Y` does appear in an Import/ImportFrom node, is_disproved_by_file will drop a genuinely real finding — exactly the residual-real cases the docstring says must not be settled.
  • low take_import adds both the alias and the top-level module name for `import a.b as c`, so a claim about `b`/`c` can be considered satisfied by unrelated bindings; likewise `from x import Y` records Y even if module x doesn't exist, so silent over-settlement is possible.
  • low resolve_file issues a repos.get_content call per candidate finding on every reviewed PR (worker path), adding latency and GitHub rate-limit pressure with no caching or per-run limit; failures are only logged to stderr.
  • low worker resolve closure indexes job["head_sha"] directly; if that key is ever absent/None for a job shape, the closure raises inside scoring rather than degrading to no settlement (unlike api.py which guards on meta.head_sha).
  • · Partial read: 99% of the diff (30,000 of 30,275 chars). Cut inside docs/REVIEWING.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.34

#44Give doug-web its own service account

○ human drewjst · +100 −21 · 3 files · unapproved

  • medium web() now hard-requires doug-web-sa to exist; any web deploy performed before the updated setup() is re-run (or before SA propagation completes) will fail at deploy time. There is no create/guard in web() itself.
  • medium doug-web-sa is granted only secretmanager.secretAccessor on doug-api-token, whereas the previous identity (default compute SA) held roles/editor. If the dashboard's server component touches anything else (queue repo access, logging/monitoring writes, other secrets), it will now fail at runtime rather than at deploy.
  • low If doug-api-token does not yet exist, setup only emits a WARN and continues; a subsequent web deploy will then start a service whose --set-secrets binding is unauthorized, surfacing as a runtime 5xx instead of a setup failure.
  • low _function_body relies on exact formatting ('name() {' at column 0, '() {' suffix) and contains dead/no-op branches; harmless shell reformatting will cause these pins to pass vacuously or fail spuriously.
needs you

0.34

#284The intent tier reads a change against the records it names, and the Vertex gate probes the hosts Google can grant

○ human drewjst · +475 −116 · 10 files · unapproved

  • medium `_bears_on` makes candidacy conditional on the change naming a word of the record's *title* (PR title word or file stem). A binding record whose title uses different vocabulary than the diff is now excluded entirely regardless of body relevance — the authors' own measurement drops 44 of 171 selections across 60 PRs. Failure mode is silent (no finding surfaced) rather than loud.
  • low
  • low Dropping directory segments and extensions means changes whose subject only appears in the path (e.g. `.github/workflows/*.yml`, `api/uv.lock`, or files with short stems filtered by len>2 such as `ci`, `db`) can reach no record at all unless the PR title happens to name one. Hyphenated stems also leak generic tokens back in (`package-lock.json` still yields `lock`), so the exclusion is inconsistent with its stated rationale.
  • low New/changed tests assert against the live decision-record corpus (`test_unrelated_changes_select_nothing_across_the_real_record_set`, `set(lema[:2]) == {...}`) and against the installed Anthropic SDK's internal host table. Adding or editing an ADR, or bumping the SDK, will turn CI red for reasons unrelated to the code under change.
  • low `vertex_host` falls through to `<name>-aiplatform.googleapis.com` for any unrecognised value, so a manually set single-region VERTEX_REGION (e.g. us-central1) still builds a resolvable URL and only fails at the 429 quota branch; gcp.sh does not validate the location set that the workflow test enforces, leaving the two gates inconsistent.
needs you

0.34

#185feat: the dashboard toggle is the only thing that turns the PR comment off

○ human drewjst · +272 −109 · 14 files · unapproved

  • medium Deleting DOUG_PR_COMMENT_INSTALLATIONS makes the PR comment write live for every installation with an active installation_repos row on the next deploy; several repos start posting user-visible comments simultaneously, and the only remaining kill switch is per-repo toggles or a Cloud Run revision rollback.
  • low The worker's skip token changed from 'skipped' to 'skipped:off'/'skipped:no-active-row'/'skipped:no-ledger'; any external log-based dashboards, alerts, or grep-based monitoring keyed on the exact word will silently stop matching.
  • low A missing engine now yields PR_COMMENT_NO_LEDGER, which the worker treats as a normal skip rather than an error/retry; a transient DB/engine outage will silently suppress comments with only a log line, indistinguishable in behavior from an intentional opt-out.
  • medium Newly enabled installations may not have re-accepted 'Pull requests: Read and write', so the first writes can fail with 403; the failure is swallowed and only surfaces as a dashboard banner, so a broad enablement could produce a burst of denied writes with no comments posted.
needs you

0.34

#99fix(web): render an expired session scope honestly, and survive cold-start derivation

○ human drewjst · +731 −83 · 10 files · unapproved

  • medium `SetupConnectionLike.status` and `ConnectionLike.status` were widened to include "reauthorize_required", but the setup/pending logic (PendingConnections, setup selection) still receives all connections. If those code paths classify anything not "ready" as pending setup, an expired connection will also render as a "finish setup" row, contradicting the new reauthorize state.
  • medium frontDoor adds `Boolean(connection.organization_id)` to the selection predicate. Previously a personal install with organization_id === null and organizationId === null would have matched (null === null); now it never does, silently pushing such users to the choose state. This relies on an undocumented-in-code API invariant (ready implies non-null org).
  • low The entitlement derivation budget grew from 2s to a total 8s (plus 400ms backoff), awaited inside the auth callback. A dead or hung API now delays every sign-in redirect by up to ~8.4s.
  • low CookieWriter requires both `set` and `delete`; clearScopeUnconfirmed calls `delete` on whatever cookies() returns. Any injected/older cookie store lacking `delete` (as in one test stub) would throw, though it is swallowed by the try/catch.
needs you

0.34

#108feat(web,api): give the receipt its first consumer, and the 60-day window a column

○ human drewjst · +3775 −117 · 34 files · unapproved

  • medium `runSummary` now requires `nullableString(value.outcome_60)` and RUN_SUMMARY_KEYS includes it. If the web app ships before/without the API change, every run list payload fails validation and the dashboard throws SessionApiError instead of degrading.
  • low The receipt page maps only 404/503/401; 500/502 and validator rejections all collapse into `unreachable`, so a genuine server error is reported as an unreadable answer. Low impact but can mask real API faults during rollout.
  • low run_history now fetches outcomes for window_days IN (14,60), roughly doubling rows scanned/returned per page; keyed reduction is correct but per-request cost and memory grow with page size.
  • low Adding a ninth column to both run tables (fixed widths, horizontal scroll below 980px) can push the age/job columns off-screen on narrower viewports; no width adjustment elsewhere in the diff.
  • · Partial read: 50% of the diff (100,000 of 199,193 chars). Cut inside web/lib/receipt-merge-view.test.mjs. Never sent: web/lib/receipt-fixture.test.mjs, web/lib/receipt-page-contract.test.mjs, HANDOFF.md (+2 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.34

#69Backfill and permanently start 60-day outcome clocks

○ human drewjst · +4675 −134 · 20 files · unapproved

  • medium enqueue_outcome_jobs changes the merge path from one row to two rows per merge (14 and 60 day). Any downstream count/dashboard/queue-load assumption based on one outcome_jobs row per merge (e.g. pending counters, batch sizing, published denominator arithmetic) will now see 2x rows, and legacy rows only have the 14-day identity, so mixed cohorts exist.
  • medium The bulk insert relies on on_conflict_do_nothing(...).values(rows).returning(...) and raises RuntimeError for any dialect other than postgresql/sqlite. Multi-row upsert with RETURNING requires recent SQLite (3.35+) and depends on the uq_outcome_job unique index exactly matching index_elements; a mismatch surfaces as a runtime ON CONFLICT error on the hot webhook path instead of the previous IntegrityError-tolerant path.
  • low The SQLite backfill due_at is computed as date(merged_at,'+60 days') || substr(merged_at, 11), which assumes merged_at is always stored as a fixed-width ISO text of at least 10 chars with the remainder being time/offset; any alternate storage format (e.g. no fractional seconds, 'T' separator variants, offset suffix) yields a malformed or wrong due_at.
  • medium preregistration_preflight hard-fails deploy on a literal grep for '^\*\*Status:\*\* LOCKED '; any reformatting of the pre-registration header (or running deploy from a non-repo image/tarball where docs/ is absent) blocks all API and adjudicator deploys, including emergency rollouts.
  • low The one-time backfill applies inserts and writes the manifest inside the same transaction, but the manifest fsync/rename ordering means a crash between manifest write and commit leaves a manifest naming rows that were rolled back; the CLI verify/rollback path would then fail on counts and require manual reconciliation.
  • Partial read: 47% of the diff (100,000 of 210,733 chars). Cut inside docs/design/outcome-loop/design-lock.md. Never sent: docs/design/outcome-loop/publication-preregistration.md, docs/superpowers/specs/2026-08-07-m3-60-day-backfill-design.md, docs/design/outcome-loop/60-day-backfill-runbook.md (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.

0.34

#120feat: per-repository needs-you flag line

○ human drewjst · +2938 −195 · 31 files · unapproved

  • medium The connections validator was tightened from tolerant (`exactWithOptional`) to `exact`, so web now hard-requires `default_needs_you_threshold` and per-repo `needs_you_threshold`. Any deploy/rollback where the web build is live against an older API revision fails every dashboard load rather than degrading.
  • medium A custom RequestValidationError handler is registered app-wide to work around NaN serialization on one PATCH field. It now owns the 422 body shape for every route and will silently diverge from FastAPI's stock handler on future upgrades (e.g. added top-level fields).
  • low `_banding_threshold` still returns the modal threshold for `summary.threshold`; with per-repo lines, installations with mixed lines will display a line that matches none of the rows (acknowledged as deferred in the docstring).
  • low Flag-line settings are keyed to ledger rollup rows by `full_name`; a repository renamed on GitHub (where the ledger rows carry the old name) will render the '—' no-control cell or attach the setting to the wrong row until names resync.
  • · Partial read: 53% of the diff (100,000 of 187,194 chars). Cut inside HANDOFF.md. Never sent: docs/superpowers/specs/2026-08-18-per-repo-needs-you-threshold-design.md, docs/superpowers/plans/2026-08-18-per-repo-needs-you-threshold.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.34

#68Console: open run forensics under the Runs table

○ human drewjst · +1160 −277 · 10 files · unapproved

  • medium SelectRunLink computes href={runHref(id)} at render time; runHref falls back to empty URLSearchParams on the server (typeof window === 'undefined'), so SSR emits `/?run=<id>` while the client renders `/?tenant=..&repo=..&run=<id>`, producing a React hydration attribute mismatch on every run row.
  • low clearHref is built server-side from tenant/repo only, so clearing selection from the forensics crumb navigates and drops client-side facet/sort params that were applied via pushState, silently resetting the operator's filtered view.
  • medium loadRunDetail(id).then(...) has no .catch; an unexpected server-action rejection (network/deploy skew) leaves detail null and the panel stuck on "Loading run N…" indefinitely with an unhandled promise rejection.
  • low Every <tr> gets tabIndex=0 and Space/Enter handlers with aria-selected but no role=row/grid semantics; Space is preventDefault'ed inside a scrollable region, breaking keyboard page/scroll behavior for the 500-row table.
  • low The page now awaits getRunDetail sequentially after getRuns on every request carrying ?run=, serializing two API calls and increasing TTFB for deep links instead of fetching in parallel.
needs you

0.34

#71Show failing jobs in the console: health strip and /jobs (Phase 2a)

○ human drewjst · +4601 −119 · 22 files · unapproved

  • medium outcome_queue.py deletes its private _as_utc/_db_now and only imports _db_now from store (not _as_utc), while also dropping the UTC import. Any remaining call to _as_utc or use of UTC in that module would raise NameError at runtime; the diff does not show the rest of the file.
  • medium Shell became async and awaits getHealth() on every page load, so all console pages (including Runs) now depend on /v1/health latency; the comment acknowledges the fetch is repeated per load and not memoized (AbortSignal opts out), adding up to 8s of sequential latency per page when health is slow.
  • medium job_rows calls _as_utc(r["due_at"]) unconditionally for outcome rows; unlike started_at there is no None guard, so any outcome_jobs row with NULL due_at would raise a TypeError/AttributeError and 500 the /v1/jobs endpoint.
  • low job_health returns raw MIN() timestamps without passing them through _as_utc, so on sqlite (and any naive column) the API emits zoneless timestamps while as_of is aware; correctness depends entirely on the client using parseUtc, and any other consumer will mis-render ages.
  • low job_rows' unhealthy_only clause for the review lane includes every pending job with attempts==0, so ordinary freshly enqueued jobs appear in the 'unhealthy' list; the health strip applies a 15-minute threshold before calling pending degraded, so the table and strip will disagree about what counts as unhealthy.
  • · Partial read: 48% of the diff (100,000 of 206,664 chars). Cut inside HANDOFF.md. Never sent: docs/superpowers/specs/2026-08-07-console-health-failure-surface-design.md, docs/superpowers/plans/2026-08-07-console-health-failure-surface.md. Findings below cover only what was sent; a clear is not evidence about the rest.

0.34

#28Step-2 Task 7b: heal the queue at startup, and stop the grader lane failing silently

○ human drewjst · +392 −31 · 4 files · unapproved

  • medium lifespan spawns a daemon thread that runs reconcile_all() then drain() on every cold start with no rate limiting or leader election; on scale-to-zero or rapid revision churn this can repeatedly walk all open PRs of all installations and, given the acknowledged non-unique verdict identity, race concurrent workers into duplicate paid model reads.
  • medium Duplicate-work protection relies on an advisory find_verdict_by_identity pre-read (no unique index until migration 003); multiple instances booting concurrently can each claim and process the same reclaimed job, causing duplicate spend/check runs.
  • medium The new code uses threading.Thread and sys.stderr in api.py; the diff does not show these imports being added, so verify `threading` (and `sys`) are imported or startup will raise NameError (swallowed only inside _startup_reconcile, not for the Thread construction in lifespan).
  • low drain() runs inside the same try/except as reconcile_all; a failure mid-drain is only printed, so partially processed jobs are left to lease expiry with no metric or alert beyond a stderr line.
needs you

0.34

#63console: group runs by PR, facet pills, column sorting

○ human drewjst · +1617 −134 · 13 files · unapproved

  • medium grouping.ts/sorting.ts use explicit `./runs.ts` runtime imports enabled via `allowImportingTsExtensions`. That flag requires `noEmit`/`emitDeclarationOnly` in tsconfig, and Next's webpack/turbopack resolution of explicit `.ts` specifiers is not guaranteed across versions — a build-time failure risk that only shows up in production build, not in `node --test`.
  • medium View state is written with `window.history.pushState` and read back via `useSearchParams`. This sync is a Next-version-dependent behavior; if it doesn't propagate, pill/sort clicks update the URL but the table won't re-render (selection/sort memos keyed on searchParams). No fallback to router.replace.
  • low PAGE_LIMIT raised to 500 and all grouping/sorting/faceting moved client-side; the table renders up to 500 rows plus expanded children on every sort/filter click, which may noticeably degrade interaction on large scopes.
  • low Row expansion state is keyed on `repo#pr` and kept in local state, but is not reset when facets/sort change; a group filtered out and later re-added stays expanded, and expanded children reflect the filtered subset rather than the PR's full run history (badge title mitigates but the row content can still mislead).
needs you

0.32

#106feat: make the outcome instrument visible (Approach A)

○ human drewjst · +1602 −82 · 24 files · unapproved

  • medium New bot-author gate in the webhook enqueue path and worker._skip_reason means any same-repo PR whose author login ends in '[bot]' or whose type is 'Bot' is silently never reviewed. Repos where an app/bot opens legitimate change PRs (e.g. release automation) lose all Doug coverage with no operator override.
  • low _skip_reason now returns a third sentinel "bot"; any caller that switches on the previous {"draft","fork"} values (metrics labels, log parsing, downstream branching) may not handle it.
  • medium The promote/smoke gate now requires /v1/showcase/scoreboard to return 200. In an environment where DOUG_SHOWCASE_REPO is unset or the ledger is unavailable the endpoint 404s by design, which will now fail deploys that previously passed.
  • low _instrument runs two additional SELECTs per processed job (aggregate over outcome_jobs plus meter lookup) on every check-run render, including the replay path; errors are swallowed but latency/DB load per job increases.
  • low instrument_snapshot_for_repo picks among multiple (installation, repo) candidates by preferring any with outcome_jobs rows, but the with_jobs query filters only on installation_id, so a pair could be selected based on jobs belonging to a different repo id under that installation.
needs you

0.32

#246ci: a merge deploys again, without waiting for approval

○ human drewjst · +282 −1709 · 8 files · unapproved

  • medium Both deploy jobs no longer name a GitHub environment, so any merge — or a direct push to main bypassing PR review — deploys to production with only the workflow's test re-run and gcp.sh's staged rollout in front of it. Intended and documented, but it materially increases the blast radius of a bad merge.
  • medium With the environment gate removed, the WIF provider's attribute condition (repository + ref pin) is the only barrier between a branch and the deployer credential. The script's exists-arm converges the live provider onto CONDITION, so any future weakening of that string silently loosens production; drift between the script and the applied provider is still unguarded (deferred to issue #247).
  • low test_setup_cicd_pins_both_the_repository_and_the_ref parses the script by splitting on 'providers create-oidc'/'update-oidc' and the next blank line, and requires the literal '--attribute-condition="$CONDITION"' in both arms. Any reflow of the shell script (or a missing update-oidc arm) will break CI for cosmetic reasons; the list-comprehension unpack of CONDITION= also throws an unhelpful ValueError if the line is renamed.
  • low Deleting the production environment removes any environment-scoped secrets/vars and changes the OIDC token's sub claim back to ref form. The ADR asserts nothing depends on either (repo-level vars only, no sub-based IAM binding), but this was verified manually rather than enforced, so a future environment-scoped value would fail at deploy time.
  • · Partial read: 100% of the diff (23,084 of 23,084 chars). Never fetched: HANDOFF.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.32

#90fix(web): dashboard honesty — run cap, coverage semantics, outcome tone, typed PR metadata

○ human drewjst · +314 −28 · 9 files · unapproved

  • low filterRuns' lowCoverage branch now only matches result.kind === 'known' && low, so runs whose changed_files is null/0 (unknown denominator) become invisible under the 'coverage < 50%' filter even though they were previously shown/hidden by a chars ratio. Acknowledged in the findings log but still a UX regression tail.
  • medium outcomeTone now flags every unrecognized kind, and the previously-handled 'clear' value plus any non-outcome strings (e.g. job statuses like 'scheduled'/'pending') would render in the miss/flag colour. Correctness depends entirely on the DB column never carrying such values.
  • low getSessionRuns now requests limit=500 (up from the route default of 100) on every dashboard render, a 5x increase in payload and validation work per page load; the exact-key validator also rejects the entire list on any single malformed row.
  • medium pr metadata validation moved from a loose record check to an exact-key, field-typed check. Any additive field in api/doug/models.py PRMetadata deployed before web will cause the whole run detail request to throw SessionApiError, breaking the detail pane.
  • low coverageLabel returns '100%' for pct >= 100 but the previous branch ordering means a value slightly above 100 (impossible today due to Math.min(1,...) but reachable if the cap is ever removed) would claim a complete read.
needs you

0.32

#229The findings list names its files and triages down its left edge

○ human drewjst · +1031 −19 · 8 files · unapproved

  • medium The summary now emits `<details>/<summary>` HTML into a check run's `output.summary`, which is not verified to render collapsed on GitHub's Checks API surface. If it renders literally, the summary shows raw HTML tags on every PR (self-acknowledged; revert is the two `_fold` calls).
  • medium `HOW_TO_READ_HEADING` is deleted and replaced by `HOW_TO_READ_SUMMARY`. Only the two test files are shown as updated; any other module or script referencing the old attribute would raise AttributeError at runtime (attribute access is not caught by ruff F821).
  • low `_severity_chip` truncates after `_oneline` neutralisation (`_oneline(raw)[:24]`). Slicing sanitised output can cut inside an inserted zero-width guard or a multi-character neutralised token, weakening the invariant that model text cannot form markdown/link syntax; the safer order is truncate-then-sanitise.
  • low `_UNLINKABLE_PATH` blocks backtick and `]` but not `[`. An unbalanced `[` in a model-supplied path is placed inside the link text; while a code span normally shields it, this relies on renderer precedence and could degrade the link to literal text exposing the raw URL.
  • low `_verdict_bundle` now includes `file` in each reason dict. Safety depends entirely on every consumer wrapping it in a `Reason` (where `exclude=True` drops it); any future direct serialisation of this dict would leak the field and break the exact-key client validator.
needs you

0.32

#227The org move lands in the ledger: rename backfill, installation 153075663, M4 deferral on record

○ human drewjst · +236 −23 · 14 files · unapproved

  • medium Migration 17 is a one-shot literal UPDATE of repo/repo_full_name. Any review or outcome row enqueued or written under 'drewjst/doug' after the migration runs (in-flight webhook jobs, retries, replays holding the old payload name) will re-introduce old-name rows and re-fork the (repo, pr_number) join the migration exists to fix. There is no normalization at write time to make the rename durable.
  • low installation_repos.full_name, JSON blobs (pr_meta, raw), and historical receipt links keep the old slug by design. This leaves an active-name mapping and dashboard/receipt links that decay once the old junction row leaves 'active' (acknowledged as issue #228), i.e. a known user-visible breakage deferred rather than handled.
  • medium Switching DOUG_INTENT_INSTALLATIONS and DOUG_VERIFY_INSTALLATIONS from 150424894 to 153075663 widens grounding/intent (the larger paid read) from one repo to every repo covered by the org installation, including coldworks. Intended per ADR amendment, but it is a cost/behavior expansion driven by an installation-scoped allowlist with no per-repo gate.
  • low Data migration statements are subject to the _satisfied 'no such column' swallow, so a schema lacking repo_full_name/repo would record version 17 without renaming anything. Mitigated by the argument that create_all always provides the columns, but the safety relies on an invariant not enforced in code.
  • low Deployment correctness depends on out-of-band GCP/GitHub state (WIF provider condition repointed, stale drewjst principalSet binding still present, App ownership transfer pending). If any piece was not actually applied, the merge's deploy fails or authorizes an unintended principal.
needs you

0.32

#72Serve GET /v1/prs/{n}/receipt, and make instrument identity checkable

○ human drewjst · +4094 −31 · 19 files · unapproved

  • medium Migration 8 backfills verdicts.prompt_hash with a hardcoded sha256 literal for all reader rows where prompt_hash IS NULL. Correctness depends entirely on the out-of-band claim that SYSTEM+SCHEMA never changed; if any historical prompt era existed, rows are permanently mislabeled with a hash they never produced. The UPDATE is also unbounded in SQL terms (row-locking a large verdicts table on Postgres at deploy).
  • low Existing dispensed keys were minted with scopes=["queue:read"] only; mint_key now adds "receipt:read" but there is no migration/backfill for previously minted keys, so all pre-existing tenant tokens receive 401 on the new endpoint until re-minted.
  • low _obj_or_none calls json.loads unconditionally on outcomes.detail (declared Text). Any malformed or non-JSON legacy value raises an uncaught JSONDecodeError, turning a receipt read into a 500; also breaks if the column type is ever migrated to JSON (driver returns dict, json.loads fails on dict).
  • low repo_id_for resolves duplicate active installation_repos rows with a newest-wins ordering rather than failing, so an operator receipt can be served scoped to a drifted/stale installation row; only a stderr log signals the ambiguity.
  • low _select_governing_verdict uses one_or_none(); if the PARTITION BY/WHERE invariant is ever weakened, this raises MultipleResultsFound and 500s a customer-facing receipt endpoint instead of degrading.
  • · Partial read: 48% of the diff (100,000 of 209,205 chars). Cut inside api/tests/test_receipts.py. Never sent: docs/REVIEWING.md, api/README.md, docs/design/outcome-loop/ROADMAP.md (+2 more). Findings below cover only what was sent; a clear is not evidence about the rest.

0.32

#78feat: persist review job base SHA

○ human drewjst · +608 −98 · 12 files · unapproved

  • medium ingest.enqueue now has a required keyword-only base_sha and raises ValueError on empty/None; any caller not updated in this diff (scripts, console, backfill jobs) will raise at runtime instead of enqueueing.
  • medium Webhook admission and reconcile now skip PRs entirely when base.sha is missing or fails _text validation (e.g., length >64 truncation semantics). Previously these were reviewed; a payload variation would silently stop reviews with only a stderr log.
  • low process_job raises RuntimeError when the fetched PR lacks head.sha or base.sha on the stale-head path; repeated failures burn attempts and can drive the job to 'failed' rather than superseding, if GitHub responses are consistently incomplete.
  • medium Migration version 10 is added while 9 is reserved on another branch; a merge introducing 9 after 10 has already been applied in production could leave version 9 unapplied depending on the applier's ordering/tracking logic, and the test asserting apply()==[5,6,7,8,10] will break.
needs you

0.32

#252The landing page shows the check run, and argues for the product

○ human drewjst · +607 −93 · 6 files · unapproved

  • medium The landing page now awaits getScoreboard alongside getQueue in a Promise.all; if the scoreboard endpoint errors (and getScoreboard lacks the same sample-data fallback getQueue has), the entire home page render fails rather than degrading like the queue does.
  • medium CheckRunCard dereferences scoreboard.adjudicated/pending/as_of/repo/deep_read_cap unconditionally and only null-checks deep_reads with `!== null`; an undefined or absent field (e.g. deep_reads undefined, deep_read_cap null) would render 'undefined' or throw on day() slicing a non-string.
  • low Findings list uses `key={r.rule}`; multiple reasons can share the same rule id (e.g. same rule at different severities/files), producing duplicate keys and possible mis-rendering.
  • medium SiteHeader is now called with a new `maxWidthClassName` prop; if that prop was not added to the component, this is a type/compile break or a silently ignored prop causing header/main width mismatch.
  • low Adding axes: ['opsz','wdth'] to next/font Bricolage_Grotesque will fail the build if the variable-font axes are not exactly as named or the font version served lacks them.
  • low cleared% guards summary.open === 0, but if summary.cleared/open are ever undefined the value renders as NaN%; also the hero picks byRisk[0] which assumes verdict.score is always numeric.
needs you

0.32

#24Step-2 Task 7 (Steps 1-3): startup reconcile for missed deliveries and stranded claims

○ human drewjst · +921 −20 · 7 files · unapproved

  • medium FAILED_REVIVE_COOLOFF_SECONDS is enforced inside the shared _revive path used by ingest.enqueue, not just by startup reconcile. Any live webhook event (reopen/synchronize at the same head SHA) arriving within an hour of a failed job now returns None and the PR is silently not reviewed, which is a user-visible regression distinct from the poison-PR problem being solved.
  • medium The cooloff compares review_jobs.finished_at against a tz-aware `now - timedelta(...)`. If fail() persists a naive UTC timestamp (or the backend stores strings without offset), the SQL comparison can be wrong/inconsistent between SQLite and Postgres, either never reviving or reviving immediately. Tests write tz-aware values so they wouldn't catch a naive-writer mismatch.
  • low reconcile_all runs reclaim_stalled plus a full open-PR sweep per active installation at every startup; on frequent cold starts this issues one paginated pulls.list per repo per boot and can re-arm revived jobs (max_attempts paid reads) once per cooloff window, with no global rate limiting or lock against concurrent instances reconciling simultaneously.
  • · Partial read: 60% of the diff (30,000 of 50,192 chars). Cut inside api/tests/test_worker.py. Never sent: docs/superpowers/plans/2026-08-01-step-2-amendments.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.32

#57doug-console Phase 1: an operator console for the run, split from the public site

○ human drewjst · +13079 −13 · 41 files · unapproved

  • medium save_review now persists `r.severity` instead of always None for reason rows. Any downstream consumer (pattern joins, comparisons, dashboards) that assumed reason.severity is NULL for non-reader tiers may now see values, changing aggregation or rendering.
  • low outcome_by_pr filters with `repo IN (...) AND pr_number IN (...)` rather than tuple pairs, so it can fetch unrelated (repo, pr) combinations; harmless for lookups but can pull large result sets on wide pages and is a latent perf/correctness smell.
  • low job_by_verdict and outcome_by_pr rely on dict last-write-wins over an ORDER BY id; if multiple review_jobs reference one verdict_id (not prevented by the unique constraint), the displayed job may be arbitrary from a user's perspective.
  • low The new `console` deploy target skips staged traffic and smoke testing, and is not exercised by CI deploy; a broken revision goes straight to 100% traffic with only Docker build coverage.
  • low run_detail's outcome_jobs query filters on v['github_repo_id'], which can be NULL for CLI/backfilled rows, silently returning no outcome jobs; similarly RunDetail assumes several columns exist on every verdicts row.
  • · Partial read: 16% of the diff (30,000 of 182,290 chars). Cut inside api/tests/test_api.py. Never sent: api/tests/test_deploy_gcp.py, api/tests/test_store.py, console/.gcloudignore (+28 more). Never fetched: console/package-lock.json, docs/superpowers/plans/2026-08-06-doug-console-phase-1.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.32

#37Charge every paid read to a scope before the model call

○ human drewjst · +820 −108 · 14 files · unapproved

  • medium worker.process_job now does `reader.installation_scope(job["installation_id"])`; if any enqueued job (legacy rows already in the queue, or a payload path that omits it) lacks `installation_id`, this raises KeyError and fails the job before the review runs, where previously the read proceeded un-scoped.
  • medium `read_diff` and `read_with_decisions` gained a required keyword-only `scope`; any remaining caller or monkeypatched stub not updated in this diff (scripts, other modules, notebooks) will raise TypeError at runtime rather than degrading gracefully.
  • low SpendCapExceeded subclasses ReaderError; only api.score_pr_read, review.score_one and review.read_intent order the except clauses correctly. Any other `except reader.ReaderError` site (e.g. decision_intent_probe, other endpoints) will report an exhausted budget as a broken reader, defeating the intended distinct signaling.
  • low _charge debits a unit before the call and never refunds on failure, so a transport/refusal retry loop can burn the monthly cap and silently downgrade a tenant to the deterministic tier without any paid read succeeding.
  • medium CI review path, /v1/score/read probe, the CLI and the offline research probe all share SENTINEL_SCOPE with a 1000/month ceiling; during the described dual-run soak this ceiling can be exhausted and silently downgrade production CI reviews to the deterministic tier.
  • · Partial read: 43% of the diff (30,000 of 69,843 chars). Cut inside api/tests/test_deviations.py. Never sent: api/tests/test_reader.py, api/tests/test_review.py, api/tests/test_store.py (+2 more). Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.32

#195feat: let a repository turn its deep read off, and say what that costs

○ human drewjst · +1095 −270 · 22 files · unapproved

  • medium `repository()`/`isRepositorySettings()` were tightened from optional to required `deep_read` via `exact()`. Any connections/PATCH body from an API instance predating the column (rollback, canary, stale instance, or web deploying before api) is rejected whole, degrading every dashboard to LedgerUnreachable rather than hiding one toggle. Accepted in ADR-0019 but is the most likely hotfix trigger here.
  • medium The web build depends on the API already emitting `deep_read`; ADR-0019 notes merge order is enforced by the PR stack but deploy completion order is not, so the API can lag the web build and break the dashboard during the window.
  • low Turning deep_read off changes both the scorer and, for repos with an unset flag line, the effective banding threshold (DOUG_READER_THRESHOLD -> DOUG_THRESHOLD), so Doug flags noticeably fewer PRs. Only the UI copy and a `deep-read-off` reason surface this; users could perceive it as Doug going silent and demand a rollback.
  • low `repo_deep_read` returns True when the row is missing or the engine is unavailable, so a reconciliation fault or DB outage can cause paid LLM reads on repositories that explicitly opted out (the write is durable, but a lost/removed row silently re-enables spend).
  • · Partial read: 93% of the diff (100,000 of 106,987 chars). Cut inside HANDOFF.md. Findings below cover only what was sent; a clear is not evidence about the rest.
needs you

0.32

#146Drop the scoring-tier column from the runs table

○ human drewjst · +70 −31 · 2 files · unapproved

  • medium The runs table drops from nine to eight columns, but no colSpan update (e.g. for empty-state or child/expanded rows) appears in the diff. If any row uses colSpan={9}, the table layout will be off by one column.
  • low run.tier is no longer rendered in RunCells; if the field is now unused elsewhere, sorting/filtering or type usage may become dead or inconsistent, though the detail pane reportedly still shows it.
needs you

0.30

#65Fix the M3 adjudicator deployment

○ human drewjst · +378 −13 · 4 files · unapproved

  • medium `wait_for_service_account "$ADJUDICATOR_SA"` and `wait_for_service_account "$SCHEDULER_SA"` replace explicit `exit 1` / `return 1` branches but their exit status is not checked. Unless gcp.sh runs with `set -e`, a permanently missing service account now only prints an error to stderr and setup continues to IAM binding steps, contradicting the stated "fail loudly" requirement.
  • low Bounded retry is 10 attempts with 1s sleep (~9s total). IAM propagation can occasionally exceed this, so the false-failure mode may recur; consider exponential backoff or a longer window.
needs you

0.30

#75Front door Phase 0: take the operator credential off doug-web

○ human drewjst · +1906 −186 · 16 files · unapproved

  • low _showcase_cache is a module-level global mutated without a lock; concurrent misses can recompute and the cached QueueResponse object is shared across requests. Benign today (response is only serialized), but any future in-place mutation of the cached items list (e.g. the in-place items.sort in _queue_response applied to cached data) would leak across requests.
  • medium The api deploy now fails and blocks promotion if /v1/showcase/queue does not return 200. It 404s when DOUG_SHOWCASE_REPO is unset OR when store.enabled() is false, so any environment without a ledger (or with a stale env var) will hard-fail deploys of an otherwise healthy API.
  • low web now depends on api with `!cancelled() && !failure()`. If the api job is skipped due to a path filter but the changes job outputs stale/incorrect values, or a future job is added to the needs list, web may deploy against an API revision that does not serve /v1/showcase/queue, silently falling back to the bundled fixture on both public pages.
  • low pretest asserts test files EXIST via `find`, while the test command relies on node's own 'lib/**/*.test.mjs' glob expansion (Node >=21). The two matchers differ, so a guard passing does not guarantee tests actually ran; on a runner with an older Node the test step could report zero tests as a pass.
  • medium The new unauthenticated route filters on the display-only verdicts.repo string rather than github_repo_id, and drops the token gate entirely; a repo-name collision or renamed repo could expose unintended rows on a public surface.
  • · Partial read: 95% of the diff (100,000 of 104,855 chars). Cut inside docs/superpowers/plans/2026-08-08-front-door-phase-0.md. Findings below cover only what was sent; a clear is not evidence about the rest.

0.30

#257The lineage pairing is checked, not documented

○ human drewjst · +360 −91 · 6 files · unapproved

  • medium _tenant_ids now raises when installation_ids is supplied without repo_ids. Any caller (including future kwargs forwarding or an API branch where repo_ids resolves to None while lineage is populated) will now 500 on a read path that previously returned data. Verification of exhaustiveness is by grep, not by types, per the project's own findings log.
  • medium Rollback now parses each temporal column with that column's own type (date.fromisoformat for Date, time.fromisoformat for Time). If a manifest holds a full ISO datetime string in a Date/Time column (older manifest format, or a column whose declared type changed), fromisoformat raises and the whole rollback aborts mid-incident where the previous code path would have restored the row.
  • low Temporal column set is derived from store.outcomes.columns at call time; a column whose SQLAlchemy python_type does not match datetime/time/date but which _serialise stringified via a duck-typed .isoformat() (e.g. a custom type wrapping a temporal value) would still be re-inserted as a raw string, so the two mechanisms are mirrored only by assumption.
needs you

0.30

#85Fix WorkOS application-scoped session issuer

○ human drewjst · +25 −7 · 2 files · unapproved

  • medium _issuers() now invokes _client_id() when WORKOS_ISSUER is unset; if the client id env var is missing/blank, _client_id() likely raises (or yields an empty/garbage issuer), turning a previously working default path into an auth failure or exception.
  • low The accepted issuer set is expanded beyond the previously pinned defaults; any mismatch between the configured client id and the one embedded in real tokens (e.g. trailing whitespace, different environment client) will silently accept or reject tokens.
needs you

0.30

#271The alerting is in the repo, and one bar grades every surface (#121)

○ human drewjst · +807 −72 · 11 files · unapproved

  • medium `liveness_bar_seconds` is added as a required (non-defaulted) field on ReviewLaneHealth/OutcomeLaneHealth. Only the /v1/health route injects it into the store dicts; any other code path or future caller that builds HealthResponse from `job_health` output will raise a validation error at request time (500). A default of 0/None or a computed default would be safer.
  • low The review-lane bar used by the console strip and row wording moves from 15 minutes to 30 minutes (REVIEW_BAR_FALLBACK_SECONDS = 30*60). This intentionally reduces sensitivity: pending work between 15 and 30 minutes will now read `clear`/`pending` instead of `degraded`/`not drained`, so a previously visible early warning disappears from the console.
  • low The console test parses `api/doug/api.py` with a regex and multiplies literal factors to pin the fallback constants. Any refactor of those constants (moved to a config module, computed, or written with a comment/expression the parser cannot handle) fails the test for reasons unrelated to actual drift.
  • low The new script runs without `set -e` and relies on manual status checks; `PROJECT="$PROJECT" audit` (env prefix on a shell function) and the `apply` path that intentionally exits 1 after creating the uptime check make partial-run semantics easy to misread in CI, where a non-zero exit from 'apply' will look like failure rather than 'run again'.
needs you

0.28

#73Act on Doug's review of #71: timezone contract and /jobs row labels

○ human drewjst · +208 −47 · 6 files · unapproved

  • medium reason() now falls through to pendingReason(job.enqueued_at, asOf) for any status === "pending" row that is not overdue/retrying/stalled. Outcome-lane clocks that are pending with a future due_at can have been enqueued days ago, so they would be labelled "not drained 20d" even though nothing is late — exactly the overstatement the PR is trying to prevent, in the other lane. A lane guard (e.g. only apply pendingReason to review rows or when due_at is null) would be safer.
  • low Thresholds use strict `>` (ms > PENDING_THRESHOLD_MINUTES * 60_000 and ms > ADJUDICATOR_GRACE_HOURS * 3_600_000) while the health strip's classify() may use >= for the same thresholds; an exact-boundary row could be graded degraded by the strip but read neutral in the table, reintroducing the disagreement this change removes.
cleared

0.28

#266The merge commit sha comes from the event when the field is gone

○ human drewjst · +643 −18 · 5 files · unapproved

  • low The new module import `merge_sha` shares a name with what was previously a common local variable (`merge_sha`) in both api.py and worker.py. The two touched sites were renamed, but any other function in these large files that still binds a local `merge_sha` would silently shadow the module (and break if it also calls merge_sha.resolve in the same scope). Worth grepping the whole files.
  • medium from_merge_commit assumes the client exposes `graphql(query, variables=...)` and returns either an unwrapped payload or a `data` envelope. If githubkit surfaces GraphQL errors differently (e.g. partial data with `errors`, or a response object rather than dict), every lookup silently returns None and the outcome lane stays dark behind a single stderr line — only fakes exercise this path in tests.
  • low When merge_commit_sha is absent, the row write is deferred to a Starlette background task that mints an installation token and issues a GraphQL call per merge. Task loss on restart/deploy, or thread-pool pressure during a burst of merges, means outcome rows are only recovered by the 6-hourly reconciler; the 202 has already been returned so GitHub will not redeliver.
  • low Correctness of the retry depends entirely on enqueue_outcome_jobs deduping (in-request attempt vs. background attempt vs. reconciler). If the dedup is keyed on (repo, number, window) but not tolerant of a differing merge_commit_sha resolved later, duplicate or conflicting outcome rows could land and skew the denominator.
  • low _record_merge changed from returning None to returning bool with a 'queue the retry' meaning. Any call site other than the webhook branch (e.g. replay/backfill paths) will ignore the True signal and silently drop merges whose payload lacks the sha.
cleared

0.28

#233The summary cut names the findings it drops (#181)

○ human drewjst · +910 −16 · 4 files · unapproved

  • medium `reserved += body.count("<details>") * len(_DETAILS_CLOSE)` counts every literal `<details>` in the body, including ones inside model-authored `reason.label` text (which `_oneline` does not neutralise). A diff producing many such labels drives `reserved` past SUMMARY_LIMIT, forcing `cut = 0` so the summary degrades to just the notice and footer ("400 of 400 findings are missing"). Reserve inflation from untrusted content is new behaviour introduced by this PR, and it is deferred to #234 rather than bounded here.
  • low `_shown_findings` requires each surviving bullet to occupy an exact whole line equal to `_bullet(...)`. If `_fold` (or any future section) ever indents, wraps, or prefixes bullets, every folded finding will be counted as missing and the notice will overstate the shortfall \u2014 the same class of implicit coupling the PR removed from `_trim_empty_fold`, left in place here.
  • low When `reserved + len(footer) > SUMMARY_LIMIT`, `cut` clamps to 0 and the emitted summary contains no body at all (only the notice/footer). This is reachable via the label-driven reserve inflation above, so the clamp silently trades all content for the notice rather than degrading gracefully.
  • low `_trim_empty_fold` only inspects the last `\n<details>` occurrence; if the cut ever leaves more than one opener with the last one containing body text, an earlier emptied disclosure would be closed rather than dropped, rendering an empty labelled triangle above the "findings missing" notice \u2014 the contradiction the helper exists to prevent.
cleared

0.28

#95feat(web): Lane 1 Phase B, PR 1 — port the console's design system foundations

○ human drewjst · +2719 −395 · 26 files · unapproved

  • medium Five shared UI primitives (Button/buttonVariants, Card family, Badge/badgeVariants, Table family, Slider) are deleted. Verified only by grep of web/**/*.{ts,tsx}; any dynamic import, MDX/story, or concurrent branch referencing them will fail to compile after merge.
  • low Ported components hardcode light-paper hex colors (#3d403c, #c9c6bd) and globals.css .cov-track/.cov-fill use raw hex instead of tokens; if the dashboard ever renders under the dark/forced-dark surfaces already present in this file (.glass, /queue), these become unreadable.
  • low parsePage accepts any integer including extremely large values ('1e9' parses as an integer via Number) and returns it unvalidated; correctness relies entirely on pageSlice clamping, so any other caller of parsePage can receive an out-of-range page.
  • low design-system tests assert on prose comments and exact regex shapes (e.g. /flagged \? "…" : "…"/, ΔE 6.1 sentence). Harmless refactors or prettier reformatting of the ported files will fail CI and require hotfixes.
  • · Partial read: 67% of the diff (100,000 of 149,376 chars). Cut inside web/lib/sorting.test.mjs. Never sent: web/lib/console-lockstep.test.mjs, docs/superpowers/plans/2026-08-11-lane1-phase-b.md. Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.28

#278The reader's fallback gets a log line, and the alerting learns to watch it

○ human drewjst · +396 −19 · 7 files · unapproved

  • low fetch() sets the global FETCH_PAGES so the EXIT trap can clean up its mktemp file, but every call site invokes fetch inside a command substitution ($(fetch ...)), so the assignment happens in a subshell and never reaches the parent's trap. The documented interrupt-safety guarantee does not hold; a mid-loop interrupt still strands the temp file.
  • low The new policy fires on a single fallback log line in a 5-minute window (threshold 0, trigger count 1, duration 0s). Any transient reader/transport blip will page; this is deliberate per the findings log but is the most likely thing to require a follow-up retune.
  • low The alert depends on exact byte-for-byte agreement between reader.FALLBACK_LOG_TOKEN, the shell FALLBACK_TOKEN, and the deployed log-metric filter. The pin test covers the source files but not already-provisioned metrics, so re-wording the token silently zeroes the existing metric until apply is re-run (the audit would then report the metric missing, so exposure is bounded).
  • low Fallback diagnostics are emitted with print(..., file=sys.stderr) inside a request handler rather than the application logger; if the service later adopts structured logging the textPayload half of the metric filter is what keeps it working, and the raw SDK error string (possibly containing request/auth detail) lands in Cloud Logging.
cleared

0.28

#82Bump postcss and next

◆ agent dependabot[bot] · +281 −236 · 3 files · unapproved

  • medium package-lock.json workspace entries list `"next": "^16.3.0"` while console/package.json and web/package.json pin the exact `16.3.0`. npm ci validates these specs and may error with an out-of-sync lockfile, breaking CI/deploy installs.
  • low sharp is bumped from 0.34.5 to 0.35.3 (major) as Next's optional dependency, including new platform packages and libc constraints; image optimization in containers (esp. musl/alpine or older Node) could fail to resolve the correct native binary at runtime.
cleared

0.27

#54Retire the CI-token review path (roadmap Task 9): dual runs stop

○ human drewjst · +70 −891 · 12 files · unapproved

  • medium The /v1/review endpoint is deleted outright. Any consumer still deploying the old doug-review.yml workflow (copies live in downstream repos, not just this one) will now get 404 instead of a verdict; the removed workflow's non-hardened variant would also fail to write any summary. Mitigated by continue-on-error, but it silently stops producing reviews.
  • medium Deleting the /v1/review handler removes the only obvious use of `review`, `hmac`, and `threading` in api.py (the inflight lock helpers were also removed). If any remaining code path still references `review.*` or the `_inflight_*` helpers, this raises NameError at request time; if not, lint/CI may fail on unused imports.
  • low worker.py now stamps prompt_hash on reader-tier verdicts, changing the population of stamped rows. Pre-existing App-path rows remain NULL, so any receipt/precision query that assumes prompt_hash presence or groups by it will see a mixed-history dataset.
  • · Partial read: 58% of the diff (30,000 of 51,510 chars). Cut inside api/tests/test_store.py. Never sent: api/tests/test_worker.py, api/tests/test_workflow_summary.py, docs/decisions/ADR-0008-doug-reviews-doug.md (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.27

#269Refuse a mint when the daily cap cannot be counted

○ human drewjst · +56 −14 · 5 files · unapproved

  • medium count_installation_tokens_minted_since returns None for ANY failure (storage off, transient SELECT error, connection blip). The caller now raises 503 for all of these, so a momentary DB hiccup turns every mint request into a hard failure instead of a logged allow. If the ledger isn't strictly required earlier in the handler, this can break previously working deployments that ran with storage disabled.
  • low The 503 detail is hardcoded to "no ledger configured", but None also covers transient query/connection failures with a ledger fully configured, which will misdirect operators during incident triage.
  • low Endpoint behavior changes from returning 200 (mint allowed) to 503 when the count cannot be obtained; clients that previously succeeded during counting failures must now handle 503, and prior specs documenting fail-open remain in the repo, creating contract ambiguity.
cleared

0.27

#223ci: deploys wait for Andrew's approval, and the deployer credential is main-only

○ human drewjst · +135 −8 · 5 files · unapproved

  • medium Adding `assertion.ref=='refs/heads/main'` to the provider condition narrows token exchange. Any run whose OIDC `ref` claim is not exactly refs/heads/main (workflow_dispatch on a tag or branch, re-runs from a non-main ref, future reusable/called workflows) will now fail at the token exchange with an opaque IAM error rather than a clear config message. Documented as accepted, but it removes the previously working emergency rollback-from-branch/tag path.
  • medium The workflow relies on the `production` environment existing with a required reviewer. If the environment is absent or its protection rule is removed/misconfigured, `environment: production` silently no-ops and deploys ship unapproved — the intended gate fails open with no guard step (deferred to issue #225).
  • low The exists-arm now unconditionally runs `providers update-oidc` with the computed condition, overwriting whatever condition the live provider has. If the deployed provider was intentionally broadened (e.g. temporarily, or shared with another workflow/branch), re-running setup will revoke that access; the script also swallows the create error (2>/dev/null) so a permissions failure only surfaces from the update.
cleared

0.26

#117fix(api): a censored outcome is a non-observation, not a defect

○ human drewjst · +196 −29 · 3 files · unapproved

  • medium Row values from the store are plain strings (e.g. "censored"), compared directly to OutcomeKind members. This silently evaluates False unless OutcomeKind is a str-based enum (StrEnum or str,Enum with proper __str__), which would reintroduce the exact bug being fixed (censored counted as defect) or break the CLEAN check. Tests use str(kind), which can also mask/mismatch depending on the enum base.
  • low /v1/patterns denominator and base_rate change (18/2 -> 16/0), altering precision/lift/clears_base values and possibly producing division-by-zero/NaN or empty pattern output for consumers expecting non-zero base rates.
  • medium Only the 'prs' loop skips censored rows; the 'hits' loop is unchanged in the diff. If it does not filter keys absent from is_defect, carriers can include PRs excluded from the denominator (the new test asserts otherwise).
cleared

0.26

#155fix: guard every PR-comment write with seq, not just the discovery path

○ human drewjst · +423 −30 · 10 files · unapproved

  • medium claim_pr_comment_seq advances last_seq before the GitHub PATCH and never rolls it back on failure, so any job whose seq falls between the last successfully landed write and a failed reservation will be silently refused ('skipped-stale'), leaving a stale comment until the next push. Deliberate and documented, but it converts transient GitHub errors into suppressed updates.
  • low Rows predating migration 13 have comment_id set with last_seq NULL, and NULL never blocks, so each pre-existing PR gets exactly one unguarded write after deploy — accepted but a real (bounded) correctness hole.
  • low The conditional UPDATE + same-transaction re-read relies on Postgres row-lock/EvalPlanQual re-evaluation under READ COMMITTED, but tests only exercise SQLite; a divergence in concurrent behavior would be invisible until production.
  • low claim_pr_comment_seq returns True when no row is found (including a row concurrently removed by forget_pr_comment), allowing an unguarded write in that narrow window; the caller then depends on a 404 to recover.
  • low set_pr_comment_id gained a required keyword-only `seq` argument; any caller outside this diff (scripts, other services) would break at runtime with a TypeError.
cleared

0.25

#39Scope the experimental intent tier to an installation allowlist

○ human drewjst · +386 −27 · 9 files · unapproved

  • medium read_intent now returns None for any non-canonical or sentinel scope, so the CI/untenanted path silently loses intent deviations and the previously surfaced 'intent-unavailable' reason. Intentional, but any consumer relying on deviations from /v1/review will see empty results with no signal.
  • low reader.intent_enabled() was deleted based on a claim of no callers; if any external script, probe, or test imports it, this is an ImportError/AttributeError at runtime.
  • low Production still runs DOUG_INTENT=1 until gcp.sh is redeployed; after deploy, if the allowlist env var is mistyped or dropped, the tier silently turns off with no alert (fail-closed but unobservable).
  • low The gcp.sh test greps for lines containing --set-env-vars; a reflowed/multiline env var block or differently-formatted deploy line would make the assertion vacuous or fail spuriously.
  • · Partial read: 90% of the diff (30,000 of 33,294 chars). Cut inside docs/design/outcome-loop/ROADMAP.md. Never sent: docs/design/outcome-loop/design-lock.md. Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.25

#83Bump sharp and next

◆ agent dependabot[bot] · +281 −236 · 3 files · unapproved

  • low package.json pins exact version "next": "16.3.0" but the lockfile root/web entries record "^16.3.0", indicating a range mismatch that could allow unintended minor upgrades on future installs.
  • medium sharp bumped 0.34.5 → 0.35.3 (major libvips 1.2.4 → 1.3.2, engine now >=20.9.0, new libc constraints). Image optimization builds could break on Node 18 runners or musl/glibc-mismatched containers.
cleared

0.25

#86Fix WorkOS session issuer validation structurally, and log rejected issuers

○ human drewjst · +129 −24 · 2 files · unapproved

  • medium Issuer is now accepted for any WorkOS application path (any client_ id in the path). Security now rests entirely on the JWKS signature being client/environment-scoped plus the `client_id` claim. If the JWKS endpoint is shared across applications/environments (or later changed to a generic WorkOS JWKS), tokens minted for other applications with a spoofed/absent client_id mismatch could pass, since the issuer no longer constrains provenance.
  • low The rejected issuer value is interpolated into an error message that is written to stderr. Since `iss` is attacker-controlled, arbitrary text (including newlines) can be injected into logs; consider sanitizing/truncating.
cleared

0.24

#19Step-2 Task 3: durable review_jobs queue, with a lease for crash-stranded claims

○ human drewjst · +837 −3 · 4 files · unapproved

  • medium Dedupe collision is detected by substring-matching driver error text ('uq_review_job', 'unique constraint failed: review_jobs.'). A constraint rename, driver/version change in message formatting, or a different Postgres wording would cause the unique violation to propagate as a 500 from the webhook path instead of being treated as a duplicate.
  • low claim() only takes a row lock on Postgres; on other dialects two concurrent claimers read the same pending row and the loser fails with a lock error rather than skipping. Documented as test-only, but an operational change of backend would silently regress.
  • low enqueue inserts then supersedes older pending rows in one transaction, but the revive path runs in a separate transaction after the IntegrityError; a concurrent claim/complete between the failed insert and the revive UPDATE yields None (interpreted as 'already queued') even if the work still needs doing in some interleavings.
  • medium Module depends on store.review_jobs table and its unique index existing, but no schema/migration change appears in this PR; deploying without the corresponding table or index would break enqueue entirely or lose dedupe protection.
  • · Partial read: 73% of the diff (30,000 of 40,920 chars). Cut inside api/tests/test_ingest.py. Never sent: docs/REVIEWING.md, docs/design/outcome-loop/ROADMAP.md. Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.22

#265Sign ADR-0027 and ADR-0028, and declare the transition bar

○ human drewjst · +364 −56 · 8 files · unapproved

  • low The docstring-stripping heuristic only skips lines that *start* with triple quotes, so interior lines of a multi-line docstring in reader._client/_verify_client are still inspected. A future docstring or string literal mentioning 'Vertex(' would fail the suite for a non-policy reason, and conversely 'anthropic.Anthropic(' asserted verbatim breaks on any harmless refactor (aliasing the import, extracting a factory).
  • low Adding the anthropic[vertex] extra pulls google-auth[requests] into the resolved graph. The lockfile shows no new packages, but the extra now pins runtime resolution to google-auth's requests transport; if google-cloud-storage is ever dropped or version-bumped incompatibly the extra becomes the sole constraint and could shift resolution.
  • low The intent test's negative case was moved from api/pyproject.toml to Makefile/.gitignore, narrowing what it proves; the acknowledged general weakness (cosmetic changes selecting three records) is deferred to an issue rather than covered here.
cleared

0.22

#26Charge the failed-revive cooloff to reconcile, not to every caller

○ human drewjst · +189 −50 · 4 files · unapproved

  • medium Live-trigger enqueue now revives 'failed' rows immediately with no cooloff. Any live caller that fires repeatedly (webhook redeliveries, drain stale-head catch-up loops) can re-arm max_attempts paid model reads per event with no rate limit, which is the exact cost the cooloff was added to bound.
  • low trigger defaults to 'live', so any future or overlooked periodic/automated caller silently gets the no-cooloff path; only reconcile_installation opts in. A mistyped or unknown trigger value also falls through to live terms rather than erroring.
cleared

0.22

#260The queues' silence is an HTTP status: /healthz/queues (#121)

○ human drewjst · +266 −0 · 4 files · unapproved

  • medium /healthz/queues is intentionally unauthenticated but executes an aggregate ledger query (store.job_health) on every request, making it a cheap DoS/DB-load vector for anonymous callers; there is no caching or rate limiting.
  • medium Returning 503 from a path under /healthz risks being picked up by a platform/load-balancer probe configured on a /healthz prefix, which would remove healthy instances from service on a queue-backlog condition.
  • low Liveness bars are compile-time constants (30m review, 26h outcome) with no env override; if the drain cadence changes or a deploy/quiet period stretches the gap, the endpoint returns 503 and pages until code is redeployed.
  • low The 'old retry is not a contradiction' behaviour relies entirely on store.job_health's oldest_pending_at excluding rows with attempts > 0; that filtering is not enforced or asserted at this layer, so a change in job_health silently turns designed retry behaviour into paging 503s.
cleared

0.22

#105fix(deploy): serve /docs — Cloud Build was stripping it from the upload

○ human drewjst · +185 −74 · 6 files · unapproved

  • low The new pin shells out to `git init` and `git check-ignore` in a temp dir; it depends on git being installed, on the repo being a git checkout (fails in tarball/sdist CI), and on `core.excludesFile=/dev/null` being valid (not on Windows). It also hardcodes `len(compiled) > 100`, which can break as the tree changes.
  • low Changing bare `docs`/`out`/`reports`/`data` to root-anchored means nested directories with those names (e.g. web/app/docs, any build `out`/`dist` output dirs under web) are now included in the Docker build context and Cloud Build upload, increasing upload size and potentially including stale build artifacts in the image context.
  • low Adding min-w-0 to the docs rail and params-table dt changes wrapping behavior across all docs pages; long code lines now rely on the <pre>'s own overflow-x-auto, which is unverified on desktop breakpoints beyond manual inspection.
cleared

0.22

#107fix(api): carry a finding's file on its Reason instead of rematching by description

○ human drewjst · +207 −16 · 7 files · unapproved

  • medium save_review no longer derives file/severity from reader_verdict, so any caller (existing or future) that hand-builds a Verdict while passing reader_verdict will silently persist NULL file/severity — degrading convergence identity. The invariant is only documented, not enforced in code.
  • low Reason gains a field with exclude=True; exclude is honoured only by model_dump/FastAPI, so any path using dict(reason) or __dict__ into a response body would add a `file` key that web/lib/session-api.ts rejects via exact key-set validation.
cleared

0.22

#111feat(web): add an About page and put it in the header nav

○ human drewjst · +346 −4 · 5 files · unapproved

  • medium PHOTOS references /about/doug/*.jpg files that are not added in this PR (only a README placeholder), so the About page ships broken image icons until the real photos land — likely to require a quick follow-up fix.
  • low Bio section is explicitly marked as a draft ("Draft bio — Andrew, edit or replace freely") with personal biographical claims; shipping unreviewed copy to a public page invites a content hotfix.
  • low next() uses an unbounded while loop over Math.random to avoid repeating the previous fact; safe for 8 items but is a busy-wait pattern that would degrade/hang if FACTS were ever reduced in a way that makes all candidates equal to prev (e.g. duplicate entries filtering later).
  • low Tests assert on raw source text (e.g. `href: "/about"` formatting and index ordering); any formatter change to NAV_LINKS quoting/spacing breaks CI without a real regression.
cleared

0.22

#118Let a finding cite bounded reads at head (dark)

○ human drewjst · +2253 −1 · 20 files · unapproved

  • medium verify_finding calls _report_cost(..., pr=None). If _report_cost dereferences pr (e.g. pr.number/pr.head_sha) for its log line, a verify read would raise AttributeError; it is caught by ground_findings' broad handler only if the exception escapes verify_finding, but it occurs after a successful (paid) model call, so the spend is wasted and the finding silently stays ungrounded.
  • low Each verify read is an extra synchronous model call (up to MAX_VERIFY_READS_PER_REVIEW=2, 60s timeout each) inside worker.drain's sequential 20-job loop on a shared threadpool, adding up to ~2 minutes per review when enabled — a throughput hazard once the flag is turned on.
  • low ground_findings uses a bare `assert` to enforce the additive invariant; under python -O asserts are stripped, and if it does fire it raises AssertionError on the live review path rather than failing soft like every other branch in the function.
  • · Partial read: 64% of the diff (100,000 of 157,101 chars). Cut inside docs/superpowers/plans/2026-08-18-cited-head-reads.md. Never sent: docs/design/competitor-imports/positions.md, docs/design/competitor-imports/decisions.md, docs/design/competitor-imports/design-lock.md (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.22

#29Make ingest's claims true, and pin the fail-open property nothing was holding

○ human drewjst · +456 −76 · 6 files · unapproved

  • low cooloff_hold_remaining is invoked on every enqueue that returns None, which is the common case for nearly every open PR on each sweep (dedupe of pending/running/reviewed rows). This adds one extra connection+SELECT per open PR per sweep, not just for genuinely failed rows; on large installations this materially increases startup DB traffic.
  • medium reconcile_installation now defaults trigger='live' instead of hard-coding 'reconcile'. Any caller other than reconcile_all (e.g. installation.created handler, future/test callers) now revives 'failed' rows immediately, bypassing FAILED_REVIVE_COOLOFF_SECONDS; a repeated installation-webhook redelivery loop could re-arm max_attempts paid model reads per delivery.
  • low cooloff_hold_remaining assumes naive finished_at values are UTC and reattaches UTC. If any row was ever written by a path using local naive time, the reported remaining hold (and thus the log line) would be wrong; advisory-only so impact is limited to misleading operator output.
  • · Partial read: 78% of the diff (30,000 of 38,491 chars). Cut inside api/tests/test_worker.py. Never sent: docs/REVIEWING.md. Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.22

#35Stop anonymous callers billing the account through /v1/score/read

○ human drewjst · +154 −30 · 5 files · unapproved

  • medium /v1/score/read now requires an x-doug-token header; any existing script, backtest harness, or dev environment calling it anonymously breaks (401), and any revision without DOUG_API_TOKEN bound returns 503 for all requests.
  • low Fourth inlined copy of the token gate (acknowledged in the comment); divergence between copies is a plausible future defect source, e.g. inconsistent status codes or missing fail-closed check.
cleared

0.22

#42Landing page: match the GitHub Pages brand

○ human drewjst · +390 −264 · 9 files · unapproved

  • low next-themes ThemeProvider is mounted inside <body> while attribute="class" targets <html>; suppressHydrationWarning is set on <html> only. If any client component reads useTheme during initial render (e.g. ThemeToggle icon), it can produce a hydration mismatch or icon flash unless it guards on mounted state.
  • medium Forced-dark wrappers use a nested .dark div; page-level backgrounds outside max-w wrappers still come from body (light) unless min-h-full/bg-background fully covers. Also the .glass utility remains hardcoded to white-alpha values, so any component using .glass on the now-light landing page or shared skeletons will render nearly invisible.
  • low ::selection changed from --sheen/--background to --accent/--accent-foreground; in dark mode --accent is a dark gray with light foreground, which may reduce selection contrast compared to the previous high-contrast pairing.
  • · Partial read: 67% of the diff (30,000 of 44,937 chars). Cut inside web/app/queue/page.tsx. Never sent: web/components/theme-provider.tsx, web/components/theme-toggle.tsx, web/package-lock.json (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.22

#59M3 item 1: adjudicate.py as a pure function, with the detector amendments it needs

○ human drewjst · +1369 −27 · 8 files · unapproved

  • medium `_earlier_by_instant` calls `commit_instant` during attribution, so a corpus containing an unparseable committer date on a PR with multiple candidate reverts raises out of `parse_revert_targets_evidenced` and aborts the whole repository map — the per-job `try/except ValueError` in `adjudicate` only covers the winner's date and cannot catch this.
  • low TOLERANCE_DAYS moved from scripts into git_labels and is now imported by screen_features/rf_kamei/label_precision_delta; if any script relied on locally tuning it, or on the previous module-level definition being re-exported, label sets and cached backtest results could shift.
  • low doug/adjudicate.py (live path) now depends on doug.backtest.git_labels, coupling production adjudication to backtest tooling; any heavyweight or subprocess-related import there is pulled into the live job.
cleared

0.22

#194feat(web): give the settings a page, and the site a way to reach it

○ human drewjst · +965 −172 · 17 files · unapproved

  • low `exactWithOptional` permits `deep_read` in both the repository and PATCH-response validators before any API emits it, and the tightening back to `exact()` is enforced only by a comment plus an external issue. If PR 2 stalls, the guard stays permanently loose and a mistyped/renamed field could silently pass through unrendered.
  • low A failed `getConnections` on /dashboard/settings redirects to /dashboard with no explanatory context; if /dashboard's own not-ready handling ever changes to redirect elsewhere (or to /dashboard/settings), this becomes a redirect chain, and today the user is bounced with no breadcrumb explaining why the settings page vanished.
  • low `revalidateDashboard()` revalidates only the two literal paths; the settings page is a dynamic authenticated route rendered per user, so revalidation semantics for it should be verified — a mismatch would leave the surface the click happened on showing pre-write state, which is precisely the bug this helper was added to fix.
  • low HANDOFF.md contains a duplicated header line ('Plan — the settings page (decided):' immediately followed by '(decided, not yet built):'), and the state block claims the work is 'not yet built' while also describing it as built and green — harmless but confusing for the next handoff.
cleared

0.22

#16Land the outcome-loop design lock + landing-page positioning section

○ human drewjst · +753 −18 · 11 files · unapproved

  • medium Renames MAGPIE_THRESHOLD/MAGPIE_CORS_ORIGINS/MAGPIE_API_URL to DOUG_* in .env.example without any accompanying code change in the diff. If api/ or web/ still read the MAGPIE_* names, local/dev setups following the example will silently fall back to defaults (threshold, CORS origins, API URL), producing broken onboarding or wrong behavior.
  • low web/app/page.tsx landing-page positioning section is modified but not visible in the truncated diff; unverified copy/markup could break the page build or layout.
  • · Partial read: 36% of the diff (30,000 of 83,428 chars). Cut inside docs/design/outcome-loop/architecture.md. Never sent: docs/design/outcome-loop/build-plan.md, docs/design/outcome-loop/design-lock.md, docs/design/outcome-loop/experience.md (+2 more). Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.22

#49Settle false unmigrated-column/schema-dependency findings

○ human drewjst · +459 −4 · 11 files · unapproved

  • medium columns_of introspects Doug's own DATABASE_URL ledger DB, which is not the reviewed repository's schema; outside self-review this settles (or fails to settle) against the wrong ground truth. Documented but unfixed.
  • low inspect(engine).has_table/get_columns is invoked per claimed table on every scored PR with no caching, adding DB round-trips to the review hot path.
  • low _TABLE_DOT_COLUMN matches any dotted identifier in prose (e.g. 'migrations.py', 'review.score_one'), so unrelated mentions become 'claimed columns'. Currently benign because unresolvable tables return None, but a coincidental collision with one of Doug's own table names could settle a real finding.
  • low looks_like_schema_dependency_finding matches any description containing 'migrat' plus 'missing', which can classify non-column migration findings into the schema settlement path and drop them on an incidental table.column match.
cleared

0.20

#211docs: reconcile every doc surface with what actually shipped

○ human drewjst · +427 −40 · 16 files · unapproved

  • medium The new guard `assert.doesNotMatch(rest, /"adjudicated"[^\n]*<Kw>\s*\d/)` uses an unescaped double-quoted field name, but the page source writes fields as `&quot;adjudicated&quot;`. The pattern can never match, so this assertion silently passes regardless of whether a concrete count is reintroduced — the exact regression it was added to prevent.
  • medium Snapshot numbers are extracted from prose with unanchored regexes (`/([\d,]+) real/`, `/([\d,]+) rows/`, etc.). Any unrelated copy edit that introduces an earlier digit-plus-keyword sequence on the page (e.g. another sentence containing "… 3 real …") will bind the wrong capture and fail or, worse, pass with wrong arithmetic — the same fragile-regex class the PR's own findings log flags.
  • low `assert.equal(snapshot.backfill, counts.backfill)` couples the docs copy to the live `docs/findings-log.jsonl` contents. If any historical row is corrected to `source: "backfill"` (or a row's `source` field is omitted, defaulting into the prospective bucket), unrelated PRs will fail this docs test with a confusing message.
  • low api/README states unset `DOUG_SHOWCASE_REPO` makes `/v1/showcase/*` return 404, while the same file's Files section says the bundled queue fixture is used by "the showcase when the ledger is not configured". These two statements can mislead operators about showcase behavior in an unconfigured deploy.
cleared

0.20

#200fix(web): the refused-authority 403 stops offering a remedy that cannot work

○ human drewjst · +427 −46 · 6 files · unapproved

  • low The extension detection `/\.[a-zA-Z]+$/` treats any trailing dot-word as an extension, so a module whose basename contains a dot (e.g. `@/lib/next.config`, `@/lib/foo.server`) will be resolved without appending `.ts` and fail to load; conversely a directory-style alias (`@/lib/foo/index`) gets `.ts` appended to a directory path. Test-only impact, but the failure mode is a confusing 'file not found' that masks the real error rmeant to avoid.
  • low Deleting the `reauth=github` arm makes the parameter fall through to the normal bind path. The new test relies on the flow cookie supplying the installation id, but the code immediately after does `const queryId = queryInstallationId(request); if (queryId === null) return invalidFlow();` paths with a stale bookmark and no `installation_id` query param may now return a generic 400 instead of a meaningful response; any external bookmark or email link to that path would degrade silently.
  • low The 403 instructs users to sign out of Doug and re-authenticate at GitHub, but the accompanying comment admits the 404 can be caused by a legacy installation record that no re-authorization can repair; readers in that case will perform a disruptive sign-out for no benefit before reaching the fallback sentence.
cleared

0.20

#243The findings log names the instrument that raised each finding

○ human drewjst · +518 −160 · 11 files · unapproved

  • medium `parse_row` now hard-rejects any `rule` not matching `<kebab>:<kebab>`, and `append()` re-parses through the same gate. Any external/automated writer (or a human transcribing a reader `category_slug` verbatim) will now fail at disposition time rather than record the finding; the check was verified only by grep over the current repo.
  • low `rates(rule_prefix=...)` normalizes a missing colon but not an empty or misspelled prefix: `rule_prefix=""` becomes ":" and a typo like "readr" yields n=0, which a caller can misread as a real measurement — the exact failure mode `normalize_prefix` was written to prevent.
  • low The page and llms.txt snapshot (205 rows / 193 prospective / 176 reader rows) is hand-maintained while the log grew by 7 rows in this same PR; the guard only asserts snapshot.total <= actual, so an understated or internally-stale figure ships silently.
  • low 20 committed evidence rows were rewritten in place to add the `reader:` prefix. The claim that all 20 were reader findings is not verifiable from the diff, and mis-attributing any of them silently shifts the published reader share.
cleared

0.19

#220feat(web): a light nav bar, and say what "clean" means

○ human drewjst · +629 −41 · 14 files · unapproved

  • low outcomeMeaning/outcomeWindowHint are copy-pasted verbatim into both web/lib/runs-time.ts and console/lib/runs.ts. The lockstep test guards drift today, but any future edit to one copy without the other will silently diverge the definitions of 'clean'/'censored' between surfaces.
  • low With a null window, the pending sentence reads "Pending: the outcome window after this pull request merged have not finished running yet" (subject/verb disagreement); same phrase composes awkwardly in the censored branch ("Censored: the outcome window ... closed without ...").
  • medium The .site-bar scope hardcodes light-palette hexes and relies on every descendant naming its own token-based ink; any header/theme-toggle child that inherits the page's dark-mode foreground (or uses a token not re-declared, e.g. --card, --secondary, --input) will render near-white on a near-white bar. The added design-system test only pins the listed nine tokens.
  • low The contract tests now parse COLUMNS with a regex requiring the array to terminate on a line exactly '\n];' and pin literal 'hint: outcomeWindowHint(14)'; harmless reformatting (prettier, adding a trailing comment) will fail CI and force a hotfix.
  • low aria-label on the header InfoDot embeds the entire multi-paragraph outcomeWindowHint (five paragraphs joined by \n\n), which screen readers will read as one enormous label on a focusable span with no role.
cleared

0.18

#20Step-2 Task 4: render the verdict as a neutral check run

○ human drewjst · +538 −1 · 3 files · unapproved

  • medium verdict_from_reader now passes severity=f.severity into Reason; this assumes both ReaderFinding.severity always exists and Reason accepts a severity field (rendering code also reads r.severity). If Reason is a strict pydantic model without that field, construction raises at runtime for every reader verdict.
  • low test_no_blocking_conclusion_string_exists_anywhere_in_the_module greps the whole source for words like 'success', 'failure', 'stale'. Any future comment or docstring using those words fails CI unrelated to behavior.
  • low Deduplication of the truncation caveat relies on the literal rule name 'read-truncated' matching what truncation_reason produces; a rename there silently reintroduces the duplicated notice.
  • low Summary truncation slices at an arbitrary byte offset, which can cut inside a markdown list item or code span, producing malformed markdown; only a trailing notice is appended.
cleared

0.18

#174fix(web): two sign-in failures Doug could not see, or described wrongly

○ human drewjst · +498 −3 · 8 files · unapproved

  • low For non-401/503 failures (e.g. 403 from an expired scope, or 500), the page now renders a terminal 'unreachable' screen with no sign-out or recovery control, replacing the previous error boundary's 'Try again'. Users hitting a recoverable 403 have no path forward from this page.
  • low Contract tests assert on exact source text/regexes of page.tsx (e.g. `try { session = { data: await getConnections(accessToken), failure: null }`). Any formatting change by prettier/lint will break CI even though behavior is unchanged.
  • low console.warn now fires on every sign-in that carries no provider token (silent SSO, password, non-GitHub). This is the expected path, so log volume/cost could rise substantially in production.
cleared

0.18

#248🛡️ Sentinel: Add HTTP security headers to operator console

○ human drewjst · +45 −0 · 2 files · unapproved

  • medium The test dynamically imports `../next.config.ts` from a plain node:test .mjs file; Node cannot parse TypeScript without a loader/type-stripping support (only available in newer Node versions or with --experimental-strip-types), so CI may fail on this new test.
  • low X-Frame-Options: DENY on all paths will break any current or future embedding (e.g., iframe-based previews, OAuth/IAP interstitials rendered in frames). Comment asserts no embed surface, but this is an assumption worth verifying.
cleared

0.18

#31Ask for the sweep's terms where installation.created reconciles

○ human drewjst · +157 −37 · 6 files · unapproved

  • low installation.created now inherits FAILED_REVIVE_COOLOFF_SECONDS, so a real (first-time or reinstall) install whose PRs previously failed will not be reviewed until cooloff expires — an operator who fixes credentials and reinstalls sees only a log line, not a review.
  • low The 'live' default of reconcile_installation now has no production caller, so it is only exercised by tests; future regressions in that path won't be caught in real usage.
cleared

0.18

#109fix(web): make this week's public surfaces findable and honest

○ human drewjst · +449 −160 · 19 files · unapproved

  • low /queue was converted from forced-dark/.glass to site-theme .panel and introduces `bg-sheen` and `bg-accent/accent-foreground` utilities; if `bg-sheen` or the accent tokens are not defined for the light theme, the live/fixture dot and threshold hover states render invisibly or transparent.
  • low `only_settled` is computed after removing the `read-truncated` reason, so a truncated read whose remaining reasons are all settlement notices will render SETTLED_NOTE ("Every finding the read produced was disproved") even though the read was incomplete — a slightly dishonest claim in the very surface this PR aims to make honest.
  • low convergence.SETTLEMENT_RULES intentionally duplicates settle.SETTLED_REASON_CODES for purity; drift is only caught by a test, so a new settle notice added without updating both silently disappears from the finding-diff.
  • low New pins assert on exact source text (e.g. exactly two `NAV_LINKS.map` occurrences, absence of the substrings 'glass'/'caught'/'dark'), which will fail on innocuous refactors or unrelated wording and may prompt hotfixes.
cleared

0.16

#276main is undeployed: the workflow never got the variable gcp.sh now requires

○ human drewjst · +94 −1 · 3 files · unapproved

  • medium `deploy_step` is everything in deploy.yml after the first occurrence of `bash deploy/gcp.sh deploy`, so it includes all subsequent steps and comments. The membership check `f"{name}:" in deploy_step` can therefore be satisfied by an unrelated later step, a comment, or a substring, letting a genuinely missing env var pass the very regression this test was added for.
  • low `re.search(r"READER_TRANSPORT:\s*(\S+)", deploy_step)` and the VERTEX_REGION equivalent take the first match anywhere after the split; a comment or another step mentioning these names with a colon would be picked up, and a missing match raises AttributeError instead of a clear failure.
  • low VERTEX_REGION is pre-staged but unused while READER_TRANSPORT is `anthropic`, so its correctness (and the region's actual model availability) is only validated by a hardcoded allow-list in tests that will silently rot as regional availability changes.
cleared

0.16

#101feat(web): floating header with separated sign-in, hosted /docs section

○ human drewjst · +2501 −282 · 30 files · unapproved

  • low --docs-content-offset (7rem) is hand-tuned to SiteHeader's rendered height and used for both the sticky sidebar offset and H2 scroll-margin; any header padding/height change silently breaks anchor positioning and sidebar alignment.
  • low DocsSidebar resets mobileOpen during render via a lastPathname comparison. This is a documented React pattern but is easy to break; if the render-phase setState is ever moved below other hooks/conditions it can cause extra render loops.
  • low ThemeToggle now intentionally renders the light icon until mount, producing a visible icon flash for dark-mode users on every load; acceptable tradeoff but a user-visible regression that could prompt a follow-up fix.
  • · Partial read: 82% of the diff (100,000 of 122,307 chars). Cut inside web/app/page.tsx. Never sent: web/lib/docs-nav.test.mjs, web/AGENTS.md, HANDOFF.md (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.16

#119fix(web): tolerate per-repo flag line fields in the connections body

○ human drewjst · +53 −3 · 2 files · unapproved

  • low `default_needs_you_threshold` is accepted with no shape/type validation (unlike `needs_you_threshold`), so malformed values pass the guard while the value is asserted as ConnectionsResponse; harmless only as long as nothing reads it.
  • low New code depends on `nullableNumber` existing in this module; if it isn't defined the build fails (not visible in the diff).
cleared

0.15

#9Stop handing back a config that is not usable yet

○ human drewjst · +60 −7 · 1 file · unapproved

  • low The binding-visibility loop runs 12 iterations and simply falls through if the binding is never observed, printing no warning and continuing to report success. An actually-missing binding would be indistinguishable from slow propagation.
  • low Distinguishing an acceptable failure by grepping stderr for 'already exists' is locale/message-format dependent; a wording change in gcloud output would turn idempotent re-runs into hard exits.
cleared

0.15

#34Make a successful review observable, and record that the cutover landed

○ human drewjst · +321 −42 · 4 files · unapproved

  • medium New `print(..., file=sys.stderr)` calls rely on `sys` being imported in worker.py; the diff does not add the import. If `sys` isn't already imported at module top, every successful job raises NameError.
  • medium The replay line formats `existing['score']:.2f` and `existing['tier']`/`existing['band']` straight from the stored row. If score is ever NULL/None or band is stored as an enum object, the f-string raises inside the replay branch before ingest.complete, turning an otherwise successful idempotent replay into a job failure.
cleared

0.15

#70Revert "Console: open run forensics under the Runs table (#68)"

○ human drewjst · +277 −1160 · 10 files · unapproved

  • low Revert deletes lib/selection.ts, lib/run-detail-action.ts and components/run-forensics.tsx; if any file outside this diff still imports them (e.g. another page or test), the build will fail. Worth a repo-wide grep for `@/lib/selection`, `run-detail-action`, and `run-forensics`.
  • low Reverting removes in-page forensics and the capped scrollable table; deep links of the form /?run=<id> shipped previously will now silently ignore the param, so any bookmarked/shared selection URLs land on the plain list without detail.
cleared

0.14

#193feat(web): give the dock the width, and make the ledger legible

○ human drewjst · +188 −30 · 5 files · unapproved

  • low Row type sizes were raised (13→13.5px title, 11→11.5px, 10.5→11px job cell) while COLUMNS widths were measured against the old sizes; only the outcome cells were re-measured and reverted. Other columns may truncate or wrap unexpectedly at the narrowest 1620px stop, where the title column is already ~188px.
  • low New dock stops (560 at 1800, 640 at 2100) plus a 4px row height increase were verified only against a static localhost mock, not the real dashboard (no fixture mode/auth). Real chrome differences could push the ledger under its 876px floor and cause horizontal scroll.
  • low --dim changed from #aaa79f to #757269 across the whole .dashboard-surface scope; any element relying on --dim for low-emphasis differentiation (e.g. against --muted-foreground) may now read as near-equal emphasis in surfaces not enumerated in the analysis.
cleared

0.14

#51Executable MT exit gate: second-account isolation proof

○ human drewjst · +247 −0 · 3 files · unapproved

  • low jq results are used in numeric/arithmetic comparisons (e.g. `[ "$b_total" -gt 0 ]`) without validating that jq succeeded; if the response is not JSON (error page, 5xx) jq prints nothing and the test errors out with a misleading message rather than a clean FAIL.
  • low `--argjson id "$B_ID"` / `--argjson inst "$B_INST"` assume the mint response returns numeric ids; if the API returns string ids (or null), argjson receives an unquoted bareword and jq fails, producing a spurious FAIL on tenant-scoping assertions.
  • medium Proof keys are only revoked at the end of the happy path; an early failure (e.g. the explicit `exit 1` after mint, or a mid-script jq abort) leaves live 'isolation-proof' credentials on both accounts since there is no trap-based cleanup.
cleared

0.14

#110fix: make three green checks able to fail, and design MT3

○ human drewjst · +589 −113 · 6 files · unapproved

  • low When the read is partial and all remaining (non-truncation) reasons are settled codes, SETTLED_NOTE is suppressed and those settled reasons are instead listed under "Findings" beneath a Flagged title — the same misreading the note was introduced to avoid (issue #109). Cosmetic/comprehension risk only.
  • low The new `fieldOf` helper assumes records are delimited by the anchor key appearing as `key: "` and that the field appears within the sliced record; a reordering that puts the anchor key not first in each object literal could slice across records or return null, silently weakening assertions (equal(null, 'live') would fail loudly, so risk is mostly false failures).
cleared

0.12

#67Retire /compare and the dual-run comparison stack

○ human drewjst · +686 −1585 · 16 files · unapproved

  • low GET /v1/comparisons is removed entirely; any external/operator tooling or bookmarks pointing at it now 404. Intentional per plan, but a breaking contract removal.
cleared

0.12

#282The evidence log reads with the credential that cloned (#270)

○ human drewjst · +73 −5 · 3 files · unapproved

  • low `env=_git_auth_env(token)` replaces the child process environment; if the helper does not merge `os.environ` (PATH, HOME, GIT_* settings), `git log` could fail to resolve git/HOME differently than before. The clone path already uses the same helper, so this is likely safe, but the log now inherits that behavior too.
cleared

0.12

#94fix(web): give the auth-entry integration test its own dist dir

○ human drewjst · +56 −4 · 8 files · unapproved

  • low `distDir` is now driven by the DOUG_WEB_DIST_DIR env var; if that variable ever leaks into a build or deploy environment, the standalone output/serving paths would silently point at a non-`.next` directory.
cleared

0.12

#138fix(web): tolerate pr_comment fields ahead of the API

○ human drewjst · +44 −3 · 2 files · unapproved

  • low `exactWithOptional` only checks key presence (`k in value`), so a required key explicitly set to `undefined` would pass the presence check; subsequent type predicates mostly cover this, but the guard is weaker than `exact`'s length/sort comparison.
  • low The relaxed guard is intended to be tightened back to `exact` in a follow-up PR; if that cleanup is missed, unexpected API fields silently pass validation.
cleared

0.12

#40Light/dark theme for the landing + docs pages

○ human drewjst · +110 −5 · 4 files · unapproved

  • low Theme is only applied from localStorage; there is no prefers-color-scheme fallback, so users with OS dark mode get the light theme by default. Also color-scheme:dark is only set on the dark selector, so form/scrollbar chrome won't match system preference.
  • low theme.js is a blocking script in <head> that sets data-theme before paint (good), but icon/aria state is only synced on DOMContentLoaded, so the toggle button renders empty briefly; also the CSS <style> comes after so no FOUC of the theme itself, though a render-blocking non-deferred script slightly delays parse.
  • low docs/index.html duplicates the dark palette with literal hex values instead of sharing tokens with index.html; future palette edits in one file will silently drift from the other.
cleared

0.12

#52Bind githubkit clients to locals (prod dispense was GC'd mid-call)

○ human drewjst · +24 −10 · 2 files · unapproved

  • low The proof script now reads failure bodies from /tmp/doug-proof-body, assuming mint() writes the response body to that exact path; if mint() writes elsewhere or not at all, `body` will silently be empty and the diagnostic remains uninformative (also `body` is assigned but not obviously consumed by check()).
cleared

0.12

#145findings-log: add repo, so external reviews stay out of doug's denominator

○ human drewjst · +188 −10 · 4 files · unapproved

  • low `rates()` gained a keyword-only `repo` param and `Rates` gained a required `by_repo` field; any external constructor of `Rates(...)` positionally or by keyword outside this module would break. Appears internal-only, so impact is likely nil.
  • low `repo` slug regex is permissive about semantics (e.g. 'doug' vs 'doug-api' are distinct denominators) and there is no allow-list, so a valid-but-wrong slug still silently splits rates — the exact failure the change aims to prevent.
cleared

0.12

#93fix(console): a censored outcome is a non-observation, not a miss

○ human drewjst · +257 −13 · 7 files · unapproved

  • low outcomeTone is duplicated between console/lib/runs.ts and web/lib/dashboard-model.ts with only a cross-workspace test to enforce parity; if that test isn't run in the console workspace CI, the copies can drift again.
  • low The parity test imports a TS file from another workspace via a custom Node loader and query-string cache-buster; path/loader assumptions are brittle and may fail after refactors or if console/lib/runs.ts ever gains an import.
cleared

0.10

#61Pre-registration v7: tenants are in by default; clear the stale citation note

○ human drewjst · +48 −25 · 1 file · unapproved

  • low The citation note explaining that `git_labels.py:NNN` anchors are offset by 24 lines relative to `main` is deleted on the assumption the m3-adjudicate branch merged (#59). If that merge has not actually landed, every line anchor in the document silently points 24 lines off with no warning.
  • low Status block repeats itself: "The hash must not enter a receipt before then. Until locked, the hash must not enter any receipt: ..." — leftover from the v6 sentence, reads as an editing artifact.
  • low The new blocker list cites "migration 006's three columns (§11)" while itself residing in §11, and the earlier status block makes the same self-referential citation; likely should point at a different section.
cleared

0.10

#53Exit gate PROVEN (16/16 vs prod) + pepper newline hardening

○ human drewjst · +49 −8 · 6 files · unapproved

  • low Isolation-proof jq now keys off `pr.url` containing "github.com/<owner>/" instead of `pr.repo` prefix; if `url` is absent/null in some rows it silently becomes "" and the foreign-row count can under-report, turning a real isolation leak into a vacuous pass.
cleared

0.10

#112fix(web): About-page cleanup — nav order, dedup, 4-photo gallery

○ human drewjst · +143 −98 · 8 files · unapproved

  • low New offset-based fact selection uses `(prev ?? 0) + offset`, so on the very first click index 0 can never be selected (offset is always >=1 from a base of 0), slightly biasing the first fact shown. Harmless but a behavior change vs. the previous rejection sampling.
  • low Tests assert nav ordering by grepping raw source strings (e.g. `href={GITHUB_REPO_URL}`, `<details className=`). Any harmless refactor (renaming the constant, formatting change) will break these tests without an actual regression.
cleared

0.10

#139docs: stop telling the public Doug is pre-build

○ human drewjst · +230 −116 · 14 files · unapproved

  • low Tests assert exact substrings of marketing/docs copy across many files (e.g. quickstart must not contain "3.12", landing must contain "Will publish its miss rate"). Any future benign wording change or JSX line re-wrap will break CI, inviting hotfixes.
  • low Claims like "--output ... Always written" and the JSON schema example (auc.doug/size_only, hotspot-path) are asserted only against docs text, not against actual CLI output, so docs may still mismatch the implementation.
cleared

0.10

#113fix(api): bind the adjudicator's clients, and make that shape unwritable

○ human drewjst · +371 −155 · 6 files · unapproved

  • low The AST guard only flags direct `factory().attr` chains and matches on bare function/method name, so it can both miss real cases (e.g. client obtained via a differently-named helper or via subscript/await) and false-positive on unrelated calls named `GitHub`, potentially failing CI on innocuous future code.
cleared

0.10

#116fix(deploy): give the image the git it shells out to, and run what CI builds

○ human drewjst · +124 −39 · 4 files · unapproved

  • low apt-get install in final stage adds layers/attack surface to the slim runtime image; acceptable but worth noting for services that never shell out to git.
  • low git installed via apt without version pinning, so image contents can drift between builds (deliberate tradeoff documented, but reproducibility is reduced).
cleared

0.10

#188Name the probe behind 0.69 / 0.67 on the landing page

○ human drewjst · +32 −1 · 4 files · unapproved

  • low The dash class /[\u2014-]/ places a hyphen at the end of a class, which is valid but easy to misread; it accepts em dash or hyphen only, not en dash, so an en-dash typography drift would fail.
  • low Test pins the entire sentence against raw JSX source; any markup insertion (e.g. <strong>) or wording tweak breaks CI even when copy is semantically unchanged. Acknowledged as intentional, but a likely source of follow-up fixes.
cleared

0.10

#91feat(api): convergence finding-diff — pre-registered design, pure module, eval script

○ human drewjst · +1543 −0 · 5 files · unapproved

  • low Settlement-notice parsing depends on settle.py's exact label grammar (' — ', '; ', ': ', ' ('); an emitter format change silently stops abstaining and flips findings to 'resolved' (the dangerous direction). Only a pinning test guards this.
  • low Coverage checks use exact string equality between model-emitted findings.file and diff-derived coverage paths; differing path spellings (e.g. 'api/doug/api.py' vs 'doug/api.py') would produce false 'resolved' instead of abstaining.
cleared

0.10

#13Test the summary script the runner actually gets

○ human drewjst · +62 −14 · 2 files · unapproved

  • low The new indentation check computes min() over non-blank body lines; if the -c body has content on the same line as the opening quote (or a line with zero indent), the computed value is 0 and the assertion fails even for a valid workflow. Also `deepest_shared` is actually the shallowest indent, so the message can be misleading.
  • low _SUMMARY_BLOCK hardcodes the exact pipeline text (`echo "$verdict" | python3 -c "..." >> "$GITHUB_STEP_SUMMARY"\n`); any benign reformatting of the workflow will make both tests fail on the assert-match rather than the intended condition.
cleared

0.10

#22Record the neutral check run as the surface

○ human drewjst · +147 −8 · 5 files · unapproved

  • low Test asserts comments.index("ADR-9999") on a list that could omit ADR-9999, raising ValueError instead of a clear assertion failure; also depends on relative ranking heuristics that may shift.
cleared

0.08

#163Walked Out: evidence-gated resolved — design lock + rule-5 amendment

○ human drewjst · +8843 −3 · 21 files · unapproved

  • low `survives` dict is computed with a `... if False else None` ternary, producing an all-None mapping that is never used; leftover debug code that could confuse future readers or be mistakenly relied upon.
  • low Bar C computation divides by `len(control)` without guarding against an empty control cohort, which would raise ZeroDivisionError if the manifest contains no single-hunk findings.
  • low Scripts hardcode machine-specific absolute paths (/Users/andrew/..., /private/tmp/claude-501/...) so they are not reproducible by anyone else; batches.json also embeds these paths as data.
  • · Partial read: 33% of the diff (100,000 of 298,987 chars). Cut inside docs/design/walked-out/phase0_units.json. Never sent: docs/design/walked-out/span-verification/barb_evidence.json, docs/design/walked-out/phase0-results.md, docs/design/walked-out/lane.md (+8 more). Never fetched: docs/design/walked-out/span-verification/manifest.json. Findings below cover only what was sent; a clear is not evidence about the rest.
cleared

0.08

#254The hero check run clamps each finding to two lines

○ human drewjst · +7 −2 · 1 file · unapproved

  • low Replacing `truncate` with `break-words` on the definition list values allows long values to wrap to multiple lines, which can change the height/alignment of the stat grid on the hero for unexpectedly long strings.
  • low `line-clamp-2` relies on `display: -webkit-box`, which overrides the span's inline display and can interact oddly with `min-w-0` inside a flex row; also requires the Tailwind line-clamp support to be present in the build config.
cleared

0.08

#62Bump js-yaml from 4.3.0 to 4.3.1 in /web

◆ agent dependabot[bot] · +3 −3 · 1 file · unapproved

cleared

0.05

#58ci: fetch full history in deploy's api job too

○ human drewjst · +10 −0 · 1 file · unapproved

cleared

0.05

#98fix(web): main is red — declare the outcome-tone rule as a ruled divergence

○ human drewjst · +23 −0 · 1 file · unapproved

  • low Adding outcomeToneClass/outcomeLabel to the ruled-divergence allowlist permanently suppresses the lockstep comparison for those exports; if they are later ported the allowance can go stale and hide real drift.
cleared

0.05

#10Scope the "never sees" claim to the read, because Doug does see authorship

○ human drewjst · +24 −11 · 1 file · unapproved

cleared

0.05

#242ADR-0022/0023/0024: Doug owns a derived memory store (the lema schema), a merge enqueues a derive job, the spine is Postgres

○ human drewjst · +847 −0 · 5 files · unapproved

  • low ADR-0006 is an accepted record that Doug's own intent provider parses and feeds to the reader; the new amendment blockquote changes the text the model sees for that record (and the ADRs themselves note a 4,000-char clip budget). This is content drift into a live prompt surface rather than a code defect, but could subtly shift reader findings.
cleared

0.05

#162ops(#152): close the doug-web SA item, and revoke four dead secret accessors

○ human drewjst · +162 −25 · 5 files · unapproved

cleared

0.03

#96docs: two-lane plan — spec and the three lane implementation plans

○ human drewjst · +1014 −0 · 4 files · unapproved

cleared

0.03

#115feat(web): add the real Doug photos and correct the About bio

○ human drewjst · +35 −33 · 7 files · unapproved

cleared

0.03

#129Design the plan lane as verticals, lanes, and checkpoints

○ human drewjst · +397 −1 · 3 files · unapproved

cleared

0.03

#212docs: the detector's first positive, audited both directions

○ human drewjst · +131 −0 · 2 files · unapproved

cleared

0.03

#55Bump fast-uri from 3.1.4 to 3.1.5 in /web

◆ agent dependabot[bot] · +3 −3 · 1 file · unapproved

cleared

0.03

#41Give REVIEWING.md a denominator, and correct the drift guard's reach

○ human drewjst · +90 −2 · 2 files · unapproved

cleared

0.03

#46Publish what Doug gets wrong

○ human drewjst · +101 −1 · 2 files · unapproved

cleared

0.03

#262ADR-0027/0028: the mechanical tier may leave Anthropic, and the risk read's transport moves to Vertex

○ human drewjst · +430 −0 · 3 files · unapproved

  • low ADR-0028 is intentionally merged with four blank pre-registration values in "The bar" table. If someone later flips status to accepted without filling them, a declared-but-empty bar could be treated as satisfied. Low risk since the record loudly flags this and proposed records are inert.
cleared

0.03

#60M3 item 7: draft the publication pre-registration (NOT locked)

○ human drewjst · +943 −13 · 3 files · unapproved

cleared

0.03

#89docs: prereg v9 — publish remediated_clears beside the cleared rate, and record the RF baseline

○ human drewjst · +228 −4 · 2 files · unapproved

cleared

0.03

#283Draft the deriver's Gate B pre-registration (bars as proposals, unsigned)

○ human drewjst · +602 −0 · 1 file · unapproved

cleared

0.02

#36Capture health connectors, distillation shape, and survival probe #1

○ human drewjst · +389 −1 · 6 files · unapproved

cleared

0.02

#77docs: correct review quality experiment contracts

○ human drewjst · +189 −19 · 6 files · unapproved

cleared

0.02

#21Record the operational shape of the champion-challenger beta path

○ human drewjst · +2 −0 · 1 file · unapproved

cleared

0.02

#130A lane convention: one folder per unit of work

○ human drewjst · +128 −0 · 2 files · unapproved

  • low lane.md links to idea.md, deterministic-half.md, and design.md within plan-lane/, but those files are not included in this change, producing dead links.
cleared

0.02

#17Check off M0's remaining roadmap items

○ human drewjst · +2 −2 · 1 file · unapproved

cleared

0.02

#47Make the partial-items preamble true again

○ human drewjst · +6 −3 · 1 file · unapproved

cleared

0.02

#92docs: record Task 7 production catch-up complete

○ human drewjst · +15 −6 · 2 files · unapproved

cleared

0.02

#66Record the live M3 adjudicator rollout

○ human drewjst · +48 −72 · 2 files · unapproved

cleared

0.02

#97docs: convergence pre-registered evaluation results — bar 1 FAILS, bar 2 passes, bar 3 no evidence

○ human drewjst · +345 −0 · 1 file · unapproved

cleared

0.02

#79Record Doug's PR #78 finding dispositions

○ human drewjst · +4 −0 · 1 file · unapproved

cleared

0.01

#33Cutover smoke test: does a neutral check run appear?

○ human drewjst · +11 −0 · 1 file · unapproved

cleared

0.01

#128docs: standing issues — deferred work becomes a GitHub issue, not a prose aside

○ human drewjst · +58 −0 · 2 files · unapproved

cleared

0.01

#153Bring plan-lane's lane.md fields current

○ human drewjst · +2 −2 · 1 file · unapproved

cleared
The score is not a grade — it prices what a change touches and how much of it, so it routes attention and does not fall as findings are fixed. Every finding names the pattern it matched, so a score can be argued with. Reader findings come from a model reading the diff; the deterministic fallback names a weighted rule instead. Cleared means not deeply inspected by a human. On one of two research repos the cleared band was not safer than merging blind; the number we are measuring is yours, not theirs.
  • · Partial read: 16% of the diff (100,000 of 626,103 chars). Cut inside web/app/dashboard/dashboard.module.css. Never sent: web/app/dashboard/page.tsx, api/deploy/prove-session-isolation.sh, api/doug/api.py (+20 more). Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
  • · Partial read: 39% of the diff (100,000 of 254,804 chars). Cut inside web/lib/session-api.test.mjs. Never sent: api/tests/test_api.py, api/tests/test_pr_comment.py, api/tests/test_worker.py (+8 more). Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    low
  • · Partial read: 29% of the diff (100,000 of 340,947 chars). Cut inside api/doug/example_pack_hosted.py. Never sent: api/deploy/gcp.sh, api/doug/example_pack_service.py, console/lib/example-packs.ts (+17 more). Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    needs you
  • · Partial read: 18% of the diff (30,000 of 167,842 chars). Cut inside api/doug/store.py. Never sent: api/doug/tenancy.py, api/tests/test_api.py, api/tests/test_check_run.py (+8 more). Never fetched: docs/superpowers/plans/2026-08-04-tenant-api-keys.md. Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    needs you
  • low githubkit extra changed to [auth-app]; if the deployment image is built from a stale lock or the extra's transitive deps (PyJWT/cryptography) are unavailable, App auth import fails at module import of doug.app_auth.
  • · Partial read: 34% of the diff (30,000 of 87,584 chars). Cut inside api/tests/test_app_auth.py. Never sent: api/tests/test_migrations.py, api/tests/test_store.py, api/uv.lock (+1 more). Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    ·
    needs you
  • · Partial read: 100% of the diff (80,221 of 80,221 chars). Never fetched: console/package-lock.json, package-lock.json. Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    needs you
    needs you
    needs you
  • · Partial read: 75% of the diff (100,000 of 132,657 chars). Cut inside api/tests/test_convergence.py. Never sent: docs/design/walked-out/product-spec.md, docs/decisions/ADR-0015-post-read-hunk-attribution-refines-convergence-identity.md. Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    needs you
    needs you
    needs you
    needs you
  • · Partial read: 56% of the diff (30,000 of 53,474 chars). Cut inside api/tests/test_reader.py. Never sent: api/tests/test_review.py, api/tests/test_store.py, web/Dockerfile (+4 more). Findings below cover only what was sent; a clear is not evidence about the rest.
  • needs you
    ·
    needs you
    needs you
    needs you
    needs you