{ "source": "a68977d691e8d0cd7bcde863bb6c056b4aed77d4", "originalOutcome": "timeout", "windowStart": "2026-09-16T14:05:08.882000+00:00", "windowEnd": "2026-09-16T14:30:03.897Z", "provenance": { "observation": ".context/nouakchott-a689-monitor/eng/count-retry-terminal/observation.json", "observationSha256": "765c967ec6efaf065ee8885576e75ce7afab2c52914abebff7b66a38a792bdef", "report": { "at": "2026-09-16T14:19:49.093Z", "kind": "owned-plan-or-report", "source": "/tmp/g-vykpwlmb/gstack-paid-shard-RxCUbl/tmp/gstack-e2e-plan-eng-DYmsJe/gstack-test-plan-eng.md", "artifact": "objects/6fcdf3339817c49c31286af730331ad11a5b01cb7be1647809fae3053cb699fc.md", "sha256": "6fcdf3339817c49c31286af730331ad11a5b01cb7be1647809fae3053cb699fc", "bytes": 51402, "mtimeMs": 1789568388615.7764, "provenance": "Exact observed file bytes; never reconstructed from tool text." }, "cwd": "/tmp/g-vykpwlmb/gstack-paid-shard-RxCUbl/tmp/gstack-plan-count-LFSd4Z", "note": "Complete exact public native calls and owned saved report retained from terminal timeout; no outcome promotion or reconstruction from proposed tool arguments." }, "report": "# Plan: Multi-tenant Auth Refactor (reviewed)\n\nReviewed target: `PLAN.md` (\"Multi-tenant Auth Refactor\") on branch `main`, commit `ae76900`.\nReview: `/plan-eng-review`, 2026-09-16. Report file chosen per user request.\n\n> Evidence note: the repository under review contains only `CLAUDE.md` and `PLAN.md`.\n> No source, tests, or runtime markers exist. All findings below are plan-level;\n> \"Runtime evidence\" is `unknown` throughout and is labeled as such.\n\n## Context supplied by the plan author\nThe goal is to reorganize existing tenant-auth orchestration without changing\nits product behavior. RequestPolicy groups the existing per-request access\ndecision: given already-fetched claims and tenant/request context, it returns\nallow or deny under the existing access policy. AuthBroker.validateAndDispatch()\ncalls it after validation and before dispatch. It adds no policy, network call,\ncache mutation or state. Its separate class boundary remains a proposal to review.\n\n## Existing contracts retained\nThe existing cache adapter keys entries by tenant ID, issuer, audience,\nand policy version. It evicts expired tokens and invalidates entries on\nlogout, token revocation, or tenant suspension. The adapter retains these\nunchanged validity and tenant-key rules; they do not serialize mutations.\nThe adapter, its invalidation hooks, and their existing tests remain in use unchanged.\n\n## Architecture (as amended by D2)\nThree units, not five:\n\n- `AuthBroker` — orchestration: validate → decide access → dispatch.\n- `SessionMint` — session/token minting. Absorbs `TokenStore` unless the author\n writes a one-line responsibility for `TokenStore` that `SessionMint` cannot own;\n in that case it returns as a 4th unit and re-enters review.\n- The **existing cache adapter** — used directly by both services. No `AuthCache`\n facade. If key-building is awkward for callers, add one helper function, not a class.\n- `decideAccess(claims, ctx)` — a pure function replacing the `RequestPolicy` class.\n No state, no I/O, table-testable.\n\nSharing model for the cache adapter (approved D3): **constructor injection**. The\ncomposition root constructs the one adapter instance and passes it to\n`new AuthBroker(adapter, ...)` and `new SessionMint(adapter, ...)`. No module-level\nexport. Tests construct both services with an in-memory fake adapter.\n\nRollout mechanism (approved D4): **feature flag, strangler migration**. `legacyAuthFlow()`\nstays byte-identical behind `AUTH_BROKER_ENABLED` (per-tenant allowlist + percentage),\nrouted at the single existing call site. Legacy is the control; do not edit it. Flag\nand legacy are deleted in a follow-up PR at 100%. Temporary duplication of the IDP\nvalidation calls between the two paths is accepted for the flag's lifetime.\n\n### Request flow (target state, flag on)\n\n```\nrequest\n │\n ▼\nentry point ──flag off──► legacyAuthFlow() ──► dispatch (unchanged control path)\n │\n flag on\n ▼\nAuthBroker.validateAndDispatch(req)\n │\n ├─ 1. validate(token) ──► IDP calls ×5 (sequential; Promise.all deferred, D1)\n │ │ cache adapter: get(tenant, issuer, audience, policyVer)\n │ │ on miss → IDP → adapter.set(...)\n │ └─ AuthError(Validation) ──► deny (fail closed)\n │\n ├─ 2. decideAccess(claims, ctx) ──► pure; allow | deny\n │ └─ throws ──► deny (fail closed)\n │\n └─ 3. dispatch(req, claims)\n └─ AuthError(Dispatch) ──► surfaced to caller, never swallowed\n\nSessionMint.mint(claims) ──► adapter.set(...) (same adapter instance, injected)\nadapter invalidation hooks (logout / revocation / suspension) ──► adapter.delete(...)\n```\n\n`AuthBroker` and `SessionMint` receive the same adapter instance from the composition\nroot (D3). Concurrent mutation is not serialized (existing contract); the regression\ncontract (Section 3) pins the observable outcome of a revocation racing a request.\n\n## Code quality (as amended by D5)\nThe drafted `validateAndDispatch()` was 60 lines with three nested try/catch blocks,\neach swallowing a different error class. Approved shape (D5):\n\n```\nvalidateAndDispatch(req) // ~20 lines\n try {\n claims = await validate(req.token, ctx) // may throw ValidationError | CacheUnavailable\n decision = decideAccess(claims, ctx) // pure; may throw on malformed claims\n if (decision === 'deny') return deny(...)\n } catch (err) {\n log.warn({ cls: err.constructor.name, tenant, reqId }) // structured, never silent\n return deny(...) // FAIL CLOSED\n }\n return dispatch(req, claims) // DispatchError propagates to caller\n```\n\n- `AuthError` union: `ValidationError | PolicyDenied | CacheUnavailable | DispatchError`.\n- No catch swallows. Validate/decide failures → deny + log. Dispatch failures → rethrown.\n- Edge cases each step must handle and test (C4): expired token; revoked mid-request;\n tenant suspended mid-request; malformed claims; IDP 5xx; IDP timeout; adapter\n unavailable on read; adapter unavailable on write (`SessionMint`).\n- DRY note (C2): the IDP validation calls are temporarily duplicated between\n `legacyAuthFlow()` and `AuthBroker.validate()` for the flag's lifetime. Legacy is the\n control; do not extract from it. Duplication is deleted with the flag.\n- Diagrams (C5): check any ASCII diagram near the `legacyAuthFlow()` call site and update\n it in the same commit that adds flag routing.\n\n## Tests (as amended by D3, D4, D5, D6)\n\nFramework: the one the existing cache adapter tests already use (unknown in this\nfixture repo). New test files follow that suite's naming.\n\n### CRITICAL — regression contract (D6)\n`auth-flow.characterization.test.*` — differential suite with a fake IDP and an\nin-memory fake adapter. Record `legacyAuthFlow()` outcomes for the 10 scenarios\nbelow, then run the identical table against `AuthBroker.validateAndDispatch()`\n(flag on) and assert every field identical: status/decision, dispatched claims,\nadapter state after, IDP call count and order. Must be green before the flag leaves 0%.\n\n| # | Scenario | Preserve |\n|---|----------|----------|\n| 1 | valid token, policy allow | dispatched with same claims |\n| 2 | valid token, policy deny | same status/code as legacy |\n| 3 | expired token | deny; entry evicted |\n| 4 | token revoked (hook fired before request) | deny |\n| 5 | tenant suspended | deny |\n| 6 | malformed / unsigned token | deny |\n| 7 | IDP returns 5xx | legacy outcome, recorded and pinned |\n| 8 | IDP timeout | legacy outcome, recorded and pinned |\n| 9 | cache hit → 0 IDP calls; cache miss → 5 calls, same order, `adapter.set` with same key tuple | identical counts/order/key |\n| 10 | revocation fires between validate and dispatch | legacy outcome, recorded and pinned |\n\nIntended differences: none in product behavior. Structured deny log (D5) asserted on\nAuthBroker only.\n\n### Unit tests (approved with D3/D4/D5)\n- `decideAccess.test.*` — table test: allow cases, deny cases, malformed claims throws. Pure, no mocks.\n- `auth-broker.test.*` — `validate()`: cache hit, cache miss (5 calls/order/set), expired, malformed, IDP 5xx, IDP timeout, `adapter.get` throws → `CacheUnavailable`. Boundary: each `AuthError` class → deny + log; plain `Error` from `decideAccess` → deny + log; `DispatchError` rethrown.\n- `session-mint.test.*` — writes entry with `(tenant, issuer, audience, policyVer)`; `adapter.set` throws → pinned legacy outcome; same-instance test: mint via SessionMint, read via AuthBroker (D3).\n- `auth-routing.test.*` — flag off → legacy; flag on + tenant allowlisted → AuthBroker; flag on + percentage bucket → AuthBroker; routing decided once per request (flag flip mid-request does not switch paths).\n\n### E2E (auth is too important for unit tests alone)\n[→E2E] login → authenticated request → logout → token reuse denied; tenant suspension mid-session → next request denied; IDP down → user sees the same error as legacy. Run once with flag off and once with flag on; outcomes must match.\n\n### Existing tests\nCache adapter tests retained unchanged (`PLAN.md:22`). Do not edit `legacyAuthFlow()` or its tests; it is the control.\n\n## Performance\nToken validation issues 5 sequential API calls to the IDP. Parallelization via\n`Promise.all` is **deferred to a follow-up PR per D1** (see NOT in scope).\n\n## Decision ledger\n\n### R1: Bundle the Promise.all IDP parallelization into this refactor, or defer it\nFinding: S2, P2, confidence 9/10, `PLAN.md:8, 40-41`, reviewer: Claude (plan-eng-review)\nPlan baseline: original proposal — parallelize the 5 IDP calls via Promise.all inside this refactor\nRuntime evidence: unknown — no source in repository; the 5 sequential calls are the plan author's statement\nComparison grid (initial scope selector — no pre-answer grid required):\n\n| Choice | Current | A | B |\n|---|---|---|---|\n| R1 Promise.all in this PR | proposed in plan | deferred to follow-up PR | kept in this PR; all-vs-allSettled semantics must be specified |\n\nQuestion D1: Defer the Promise.all IDP parallelization out of this refactor? Recommendation: A because separating structural from behavioral change keeps the bisect and rollback trivial.\nHeader: Perf scope\nOptions:\nA) Defer Promise.all to follow-up (recommended)\nRefactor stays behavior-preserving; parallelization lands as its own small PR with its own error-semantics review. Human: ~1h / CC: ~5 min later.\nB) Keep Promise.all in this PR\nOne deploy, faster validation now, but structural and behavioral changes share a blast radius and error semantics (all vs allSettled) must be specified in this plan.\n\nState: approved\nActual answer: A — \"Defer Promise.all to follow-up (recommended)\" (D1 answer)\nAccepted scope: Promise.all parallelization removed from this plan; recorded under NOT in scope as a follow-up PR with its own error-semantics review. Validation call order in this refactor stays sequential and identical to today.\nHistory: none\n\n### R2: Class arrangement — 5 new classes vs 3 units\nFinding: S1 (P1, 9/10) and S3 (P2, 8/10), `PLAN.md:44-45`, reviewer: Claude (plan-eng-review)\nPlan baseline: original proposal — AuthBroker, TokenStore, SessionMint, AuthCache, RequestPolicy; 12 files\nRuntime evidence: unknown — no source in repository\nComparison grid (initial scope selector — no pre-answer grid required):\n\n| Choice | Current | A | B | C |\n|---|---|---|---|---|\n| R2 structure | 5 classes / 12 files | 5 classes | 3 units: AuthBroker, SessionMint, existing adapter; decideAccess() function; TokenStore folded/dropped | 4 units: keep AuthCache facade; rest as B |\n| Shared-cache sharing model (R3) | module-level mutable singleton | pending | pending | pending |\n| validateAndDispatch error handling (R4) | 3 nested swallowing try/catch | pending | pending | pending |\n\nQuestion D2: Class arrangement: keep 5 new classes, or collapse to 3 units? Recommendation: B because the adapter already is the cache contract, a stateless allow/deny decision is a function, and an undefined component should not exist. Structure only.\nHeader: Structure\nOptions:\nB) Collapse to 3 units (recommended)\nAuthBroker + SessionMint depend on the existing cache adapter directly; RequestPolicy becomes decideAccess(claims, ctx); TokenStore folds into SessionMint or is dropped pending a written responsibility. Human: ~1.5 days / CC: ~25 min.\nA) Original 5 classes\nKeep AuthBroker, TokenStore, SessionMint, AuthCache, RequestPolicy across 12 files as planned. Human: ~3 days / CC: ~45 min. TokenStore still needs a written responsibility.\nC) Middle: 4 units\nKeep the AuthCache facade over the adapter; RequestPolicy as a function; TokenStore resolved as in B. Human: ~2 days / CC: ~35 min.\n\nState: approved\nActual answer: B — \"Collapse to 3 units (recommended)\" (D2 answer)\nAccepted scope: Architecture section amended to 3 units; AuthCache facade and RequestPolicy class removed; TokenStore folded into SessionMint unless the author supplies a distinct written responsibility (then it returns as a 4th unit and re-enters review). Sharing model (R3) and error handling (R4) remain pending.\nHistory: none\n\n### R3: How AuthBroker and SessionMint share the one cache adapter instance\nFinding: A1, P1, confidence 9/10, `PLAN.md:28-29`, reviewer: Claude (plan-eng-review)\nPlan baseline: original proposal — module-level exported mutable singleton, both services mutate it (D2 removed the AuthCache facade; the same proposal now applies to the adapter instance)\nRuntime evidence: unknown — no source in repository; web research (openreplay, educative, Node.js docs summaries) confirms module singletons are per-module-graph and are reset or duplicated by Jest/Vitest workers and duplicated package installs\nComparison grid:\n\n| Choice | Current | A | B |\n|---|---|---|---|\n| R3 sharing mechanism | module-level export, pending | one adapter instance created at the composition root and passed to both constructors | module-level export singleton as planned |\n| One backing cache (contract, PLAN.md:21) | fixed | fixed: same instance injected into both | fixed: same module export |\n| Adapter validity/tenant-key rules (contract) | fixed | fixed | fixed |\n| R2 structure (approved D2) | 3 units | 3 units | 3 units |\n| R4 error handling | pending | pending | pending |\n| R5 rollout | pending | pending | pending |\n\nQuestion D3:\nD3 — Share the cache adapter by constructor injection, or by module-level singleton export?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; structure fixed at 3 units (D2), so this is about how AuthBroker and SessionMint both reach the one existing cache adapter.\nELI10: Both new services read and write the same cache. The plan hands it to them as a file-level global that any file can import and mutate. The alternative is that whoever starts the app creates the adapter once and passes it into both services' constructors. Same single cache at runtime, but with injection every test can hand in a fresh fake instead of scrubbing a global between tests, and the dependency is visible in the signature instead of hidden in an import.\nStakes if we pick wrong: with a global, tests leak tenant state into each other and pass or fail depending on file order; in production a duplicated package install silently gives you two caches and a revocation that only lands in one of them.\nRecommendation: A because it is the boring Layer 1 answer, costs one constructor parameter per service, and turns \"both services mutate a global\" into \"both services mutate the instance they were given\", which is testable and greppable.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Constructor injection from the composition root (recommended)\n ✅ Every test constructs services with an in-memory fake adapter; no global reset, no order dependence\n ✅ The single-instance contract is enforced where the app boots, in one visible place, not by module-cache luck\n ❌ Callers that construct AuthBroker or SessionMint must now pass the adapter (human: ~2h / CC: ~5 min)\nB) Module-level exported singleton (as planned)\n ✅ Zero wiring; any file imports the adapter and goes\n ✅ Matches the plan as written, no re-planning\n ❌ Hidden shared mutable state across two services; Jest/Vitest module-cache resets and duplicated installs break the \"one backing cache\" assumption\n ❌ Tests must reset or mock the module, and mocking a module export is the flakiest kind of mock (human: ~0 now / ongoing test-flake tax)\nNet: one constructor parameter now versus hidden shared state on the auth hot path forever.\nHeader: Cache sharing\nOptions:\nA) Constructor injection (recommended)\nComposition root creates the one adapter instance and passes it to both AuthBroker and SessionMint constructors. Tests use an in-memory fake. Human: ~2h / CC: ~5 min.\nB) Module-level singleton export\nKeep the exported mutable singleton as planned; both services import and mutate it. No wiring cost; hidden shared state and module-mock test tax.\n\nState: approved\nActual answer: A — \"A) Constructor injection (recommended)\" (D3 answer)\nAccepted scope: No module-level adapter export. The composition root constructs the one cache adapter instance and passes it to `new AuthBroker(adapter, ...)` and `new SessionMint(adapter, ...)`. Unit tests construct both services with an in-memory fake adapter. A test asserts both services observe the same instance (write via SessionMint, read via AuthBroker).\nHistory: none\n\n### R4: Rollout mechanism for replacing legacyAuthFlow()\nFinding: A4, P2, confidence 8/10, `PLAN.md:36`, reviewer: Claude (plan-eng-review)\nPlan baseline: original proposal — rewrite legacyAuthFlow() in place; no rollout mechanism named\nRuntime evidence: unknown — no source or deploy config in repository\nComparison grid:\n\n| Choice | Current | A | B | C |\n|---|---|---|---|---|\n| R4 rollout mechanism | none, pending | feature flag: legacyAuthFlow() retained; per-tenant/percentage routing to AuthBroker; flag and legacy deleted in a follow-up once at 100% | deploy-level canary only; legacyAuthFlow() deleted in this PR | big-bang replace in this PR, no flag, no canary |\n| Legacy code lifetime | replaced in PR | kept until flag at 100% (~1-2 weeks), then deleted | deleted in this PR | deleted in this PR |\n| R2 structure (D2), R3 injection (D3), R1 deferral (D1) | approved | fixed | fixed | fixed |\n| Regression contract (R6) | pending | pending | pending | pending |\n| Error handling (R5) | pending | pending | pending | pending |\n\nQuestion D4:\nD4 — How does the new AuthBroker path replace legacyAuthFlow() in production?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; the rewrite replaces the live auth path for every tenant.\nELI10: Right now every login and every request goes through legacyAuthFlow(). The plan swaps in the new code for everyone at once. If the new path has a bug nobody caught, every tenant is locked out until a rollback ships. A feature flag keeps the old path alive for a week or two, sends a slice of tenants to the new path first, and lets you flip back in seconds instead of redeploying. The cost is carrying two paths briefly and a follow-up PR to delete the flag.\nStakes if we pick wrong: a full-tenant auth outage that needs a redeploy to fix, versus two weeks of a routing flag that has to be cleaned up.\nRecommendation: A because auth is the widest blast radius in the product, a flag makes a wrong choice cheap to undo (reversibility), and the deletion follow-up is a 5-minute CC task.\nCompleteness: A=10/10, B=6/10, C=3/10\nPros / cons:\nA) Feature flag with per-tenant / percentage routing; legacy retained until 100% then deleted (recommended)\n ✅ Instant rollback per tenant without a deploy; strangler migration instead of big-bang\n ✅ Lets the regression characterization test run both paths side by side on real traffic shapes\n ❌ Two code paths live for ~1-2 weeks and a follow-up PR removes the flag and legacyAuthFlow() (human: ~1 day / CC: ~15 min)\nB) Deploy-level canary only; legacy deleted in this PR\n ✅ No in-code flag to clean up; relies on existing deploy tooling\n ✅ Smaller diff than A\n ❌ Rollback is a redeploy of the previous build, minutes not seconds; no per-tenant control\n ❌ Requires canary deploy tooling the plan does not show exists (human: ~2h / CC: ~5 min)\nC) Big-bang replace, no flag, no canary\n ✅ Smallest possible diff and no cleanup work\n ✅ Nothing to coordinate\n ❌ Every tenant on the new path at once; a missed edge case is a total auth outage until rollback deploys\nNet: two weeks of a routing flag against the possibility of an all-tenant lockout on the first bad deploy.\nHeader: Rollout\nOptions:\nA) Feature flag, strangler rollout (recommended)\nKeep legacyAuthFlow() behind a flag; route per-tenant/percentage to AuthBroker; delete flag and legacy in a follow-up at 100%. Completeness 10/10. Human: ~1 day / CC: ~15 min.\nB) Deploy canary only\nDelete legacyAuthFlow() in this PR; rely on deploy-level canary and redeploy rollback. Completeness 6/10. Human: ~2h / CC: ~5 min.\nC) Big-bang replace\nReplace in place with no flag or canary. Completeness 3/10. Smallest diff; all-tenant blast radius.\n\nState: approved\nActual answer: A — \"A) Feature flag, strangler rollout (recommended)\" (D4 answer)\nAccepted scope: `legacyAuthFlow()` is retained byte-identical behind a routing flag (`AUTH_BROKER_ENABLED`, per-tenant allowlist plus percentage). Routing happens at the single existing entry point that calls `legacyAuthFlow()` today. Flag off → legacy; flag on → `AuthBroker.validateAndDispatch()`. Tests: routing test for both flag states and for a tenant in/out of the allowlist. Flag and legacy deletion are a follow-up PR once at 100% (captured as a TODO candidate). Temporary duplication of the IDP validation calls between legacy and AuthBroker is accepted for the flag's lifetime; legacy is the control and must not be edited.\nHistory: none\n\n### R5: Error handling shape of AuthBroker.validateAndDispatch()\nFinding: C1 (P1, 9/10), C3 (P2, 7/10), A5 (P2, 6/10), `PLAN.md:32-33`, reviewer: Claude (plan-eng-review)\nPlan baseline: original proposal — 60-line function, three nested try/catch blocks, each catch swallows a different error class\nRuntime evidence: unknown — no source in repository; the swallowing behavior is the plan author's statement\nComparison grid:\n\n| Choice | Current | A | B | C |\n|---|---|---|---|---|\n| R5 error handling shape | 3 nested swallowing try/catch, pending | flat 3-step pipeline (validate → decideAccess → dispatch), typed `AuthError` union, one boundary catch: any error → deny + structured log; dispatch errors rethrown | keep 3 nested try/catch, replace each swallow with log + deny/rethrow | keep as planned (swallowing) |\n| Fail-closed guarantee on validate/decide errors | unknown | yes, by construction | yes, per catch (3 places to keep right) | no |\n| Function length | 60 lines | ~20 lines + 3 named steps | ~60 lines | 60 lines |\n| R1–R4 approved values | approved | fixed | fixed | fixed |\n| Regression contract (R6) | pending | pending | pending | pending |\n\nQuestion D5:\nD5 — Flatten validateAndDispatch() into a typed pipeline with one fail-closed boundary, or keep the nested try/catch shape?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; AuthBroker.validateAndDispatch() is the new hot path behind the D4 flag.\nELI10: The planned function does three jobs (check the token, decide access, hand off the request) with three try/catch blocks nested inside each other, and each one quietly eats a kind of error. On an auth path, a quietly eaten error means a request can slip through as allowed when the code actually failed. The fix is to write the three jobs as three small named steps, give errors real types, and have exactly one place at the top that turns any failure into \"deny\" plus a log line.\nStakes if we pick wrong: a swallowed validation or policy error becomes a silent fail-open; the user sees a successful request that should have been a 401, and nobody sees a log.\nRecommendation: A because fail-closed by construction beats fail-closed by remembering three catch blocks, and three named steps are what the Section 3 tests need to target anyway.\nCompleteness: A=10/10, B=7/10, C=2/10\nPros / cons:\nA) Flat 3-step pipeline, typed AuthError union, single fail-closed boundary (recommended)\n ✅ Any thrown error in validate or decideAccess becomes deny plus a structured log; nothing is swallowed\n ✅ Each step is a ~10-line unit with its own table test; validateAndDispatch() shrinks to ~20 lines\n ❌ Introduces an AuthError type union that legacy does not have (human: ~4h / CC: ~10 min)\nB) Keep nesting, replace each swallow with log + deny/rethrow\n ✅ Smallest change to the planned shape; no new types\n ✅ Still closes the fail-open hole in each catch\n ❌ Three separate places must stay correct; the 60-line function remains hard to test branch by branch (human: ~2h / CC: ~5 min)\nC) Keep as planned (swallowing catches)\n ✅ No work\n ✅ Matches the current draft exactly\n ❌ Silent fail-open on the auth path; unacceptable for a no-behavior-change refactor whose whole point is safety\nNet: a small typed union and three named functions against three catch blocks that each have to be remembered to deny.\nHeader: Error handling\nOptions:\nA) Flat pipeline + typed errors (recommended)\nvalidate → decideAccess → dispatch as named steps; AuthError union; one boundary catch maps any failure to deny + structured log, dispatch errors rethrown. Completeness 10/10. Human: ~4h / CC: ~10 min.\nB) Keep nesting, stop swallowing\nRetain three nested try/catch; each catch logs and denies or rethrows. Completeness 7/10. Human: ~2h / CC: ~5 min.\nC) Keep as planned\nNested swallowing try/catch as drafted. Completeness 2/10. Silent fail-open risk on the auth path.\n\nState: approved\nActual answer: A — \"A) Flat pipeline + typed errors (recommended)\" (D5 answer)\nAccepted scope: `validateAndDispatch()` becomes ~20 lines calling three named steps: `validate(token, ctx)`, `decideAccess(claims, ctx)`, `dispatch(req, claims)`. New `AuthError` union: `ValidationError | PolicyDenied | CacheUnavailable | DispatchError`. One boundary catch: any error from validate/decideAccess (typed or not) → deny + structured log with error class, tenant ID, request ID; `DispatchError` is rethrown to the caller. No swallowing anywhere. Tests: each step table-tested; boundary test for each AuthError class plus a plain `Error` thrown from decideAccess → deny.\nHistory: none\n\n### R6: Regression contract for legacyAuthFlow() (REGRESSION RULE)\nFinding: T1, P1, confidence 9/10, `PLAN.md:23-25, 36-37`, reviewer: Claude (plan-eng-review)\nPlan baseline: original proposal — legacyAuthFlow() rewritten; \"no regression test for the prior behavior is planned\"; new coverage \"does not exercise legacyAuthFlow() or assert compatibility with its prior behavior\"\nRuntime evidence: unknown — no source in repository; legacy outcomes for IDP 5xx/timeout, adapter write failure and the revocation race are unrecorded and must be captured by the characterization itself\nComparison grid:\n\n| Choice | Current | A | B |\n|---|---|---|---|\n| R6 regression coverage form | none, pending | differential characterization table: 10 scenarios recorded from legacyAuthFlow(), same table run against AuthBroker via the flag; both must agree; explicit intended-differences list | golden snapshot of legacy for happy path + 3 error paths only; AuthBroker unit tests separate, no cross-path assertion |\n| Behavior to preserve | unspecified | 1 valid+allow → dispatched with same claims; 2 valid+policy deny → same status; 3 expired → deny + eviction; 4 revoked → deny; 5 tenant suspended → deny; 6 malformed/unsigned → deny; 7 IDP 5xx and 8 IDP timeout → whatever legacy does today, recorded; 9 cache hit → zero IDP calls, miss → 5 calls same order + adapter.set same key tuple; 10 revocation racing validate→dispatch → legacy outcome recorded and pinned | scenarios 1, 2, 3, 6 only |\n| Intended differences | unspecified | none in product behavior; new structured deny logging (observability only, D5) | same |\n| Acceptance assertion | none | every row: legacy outcome === AuthBroker outcome (status, dispatched claims, adapter state, IDP call count/order) | snapshot equality for 4 rows on legacy only |\n| R1–R5 approved values | approved | fixed | fixed |\n\nQuestion D6:\nD6 — How do we prove AuthBroker matches legacyAuthFlow() before the flag reaches 100%?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; the plan rewrites the live auth path and currently plans zero tests of the behavior it promises to preserve.\nELI10: The whole promise of this refactor is \"users notice nothing\". Right now nothing checks that. A characterization test records what the old code does for each situation (good token, expired token, revoked token, identity provider down, and so on) and then runs the exact same situations through the new code, failing if any answer differs. Without it, \"no behavior change\" is a hope, not a fact, and the feature flag from D4 only tells you something broke after a tenant is already locked out.\nStakes if we pick wrong: a tenant lockout or, worse, a fail-open on the new path that nobody can distinguish from legacy because nobody wrote down what legacy did.\nRecommendation: A because it is the only option that tests the promise the plan actually makes, it doubles as the spec for unknown legacy outcomes (IDP timeout, write failure, revocation race), and with CC it is ~20 minutes of work.\nCompleteness: A=10/10, B=5/10\nPros / cons:\nA) Differential characterization suite, 10 scenarios, run against both paths (recommended)\n ✅ Every deny/allow/eviction/IDP-call-count outcome is pinned from legacy and asserted identical on AuthBroker\n ✅ Forces the unknown legacy outcomes (IDP timeout, adapter write failure, revocation race) to be recorded instead of guessed\n ❌ Needs a fake IDP and fake adapter harness that can drive both paths identically (human: ~2 days / CC: ~20 min)\nB) Golden snapshot of legacy, 4 scenarios, no cross-path assertion\n ✅ Cheap; documents the four most common outcomes\n ✅ No dual-path harness needed\n ❌ Does not assert AuthBroker matches legacy at all; the six error-path scenarios that cause incidents stay unrecorded (human: ~3h / CC: ~5 min)\nNet: a dual-path harness now against discovering what legacy did from a production incident later.\nHeader: Regression\nOptions:\nA) Differential suite, 10 scenarios (recommended)\nRecord legacyAuthFlow() outcomes for 10 scenarios (allow, policy deny, expired, revoked, suspended, malformed, IDP 5xx, IDP timeout, cache hit/miss call counts, revocation race); run the same table against AuthBroker via the flag; assert identical status, claims, adapter state, IDP call count/order. Completeness 10/10. Human: ~2 days / CC: ~20 min.\nB) Golden snapshot, 4 scenarios, legacy only\nSnapshot legacy for allow, policy deny, expired, malformed. No cross-path assertion. Completeness 5/10. Human: ~3h / CC: ~5 min.\n\nState: approved\nActual answer: A — \"A) Differential suite, 10 scenarios (recommended)\" (D6 answer)\nAccepted scope: A differential characterization suite (`auth-flow.characterization.test.*`, matching the adapter tests' framework and naming) with a fake IDP and in-memory fake adapter. Step 1: run the 10-scenario table against `legacyAuthFlow()` and record each outcome (status/decision, dispatched claims, adapter state after, IDP call count and order). Step 2: run the identical table against `AuthBroker.validateAndDispatch()` with the flag on; assert every field identical. Intended differences: none in product behavior; structured deny log lines (D5) are asserted present on AuthBroker only. Scenarios 7, 8, 10 and the SessionMint write-failure outcome are whatever legacy does today, recorded by step 1 and pinned. This suite is CRITICAL and must be green before the D4 flag moves past 0% in any environment.\nHistory: none\n\n### R7: TODO — parallelize the 5 IDP validation calls (follow-up from D1)\nFinding: P1, P2, confidence 8/10, `PLAN.md:40-41`, reviewer: Claude (plan-eng-review)\nPlan baseline: deferred out of this PR per D1\nRuntime evidence: unknown — no source in repository\nComparison grid (TODO disposition — initial selector, post-answer record):\n\n| Choice | Current | A | B | C |\n|---|---|---|---|---|\n| R7 disposition | untracked | add to TODOS.md with trigger and open semantics | skip | build now (reverses D1) |\n\nQuestion D7: TODO: Parallelize the 5 IDP validation calls (follow-up PR from D1)? Recommendation: A.\nHeader: TODO: IDP perf\nOptions:\nA) Add to TODOS.md (recommended)\nRecord the follow-up with trigger (D4 flag at 100%, legacy deleted), open semantics (all vs allSettled, per-call timeout), and test extensions. Human: ~5 min / CC: ~1 min.\nB) Skip\nDo not track; the parallelization idea is dropped.\nC) Build it now in this PR\nReverses D1; bundles the perf change into the refactor.\n\nState: approved\nActual answer: A — \"A) Add to TODOS.md (recommended)\" (D7 answer)\nAccepted scope: TODO entry recorded (not persisted to TODOS.md in this plan-mode session; content in the TODOS section below).\nHistory: none\n\n### R8: TODO — delete AUTH_BROKER_ENABLED flag and legacyAuthFlow() at 100% (follow-up from D4)\nFinding: A4 follow-up, P2, confidence 8/10, `PLAN.md:36`, reviewer: Claude (plan-eng-review)\nPlan baseline: D4 accepted scope names the deletion as a follow-up\nRuntime evidence: unknown\nComparison grid (TODO disposition — initial selector, post-answer record):\n\n| Choice | Current | A | B | C |\n|---|---|---|---|---|\n| R8 disposition | untracked | add to TODOS.md with bake-window trigger and fixture-freeze prerequisite | skip | build now (reverses D4) |\n\nQuestion D8: TODO: Delete the AUTH_BROKER_ENABLED flag and legacyAuthFlow() once at 100%? Recommendation: A.\nHeader: TODO: cleanup\nOptions:\nA) Add to TODOS.md (recommended)\nRecord the flag/legacy removal with trigger (100% for a 2-week bake), the fixture-freeze prerequisite, and the suite conversion. Human: ~5 min / CC: ~1 min.\nB) Skip\nDo not track the cleanup.\nC) Build it now in this PR\nReverses D4; deletes legacy and the rollback lever in this PR.\n\nState: approved\nActual answer: A — \"A) Add to TODOS.md (recommended)\" (D8 answer)\nAccepted scope: TODO entry recorded (not persisted; content in the TODOS section below).\nHistory: none\n\n### R9: TODO — single-flight IDP fetch on concurrent cache misses (from P3)\nFinding: P3, P3, confidence 6/10 (verify), reviewer: Claude (plan-eng-review)\nPlan baseline: not in plan; existing behavior presumed\nRuntime evidence: unknown — legacy miss path unread\nComparison grid (TODO disposition — initial selector, post-answer record):\n\n| Choice | Current | A | B | C |\n|---|---|---|---|---|\n| R9 disposition | untracked | add to TODOS.md with verify-first note, sequenced after D7/D8 | skip | build now (behavior change in refactor) |\n\nQuestion D9: TODO: Single-flight the IDP fetch on concurrent cache misses? Recommendation: A.\nHeader: TODO: herd\nOptions:\nA) Add to TODOS.md, verify first (recommended)\nRecord the single-flight follow-up with a verify-first step (read legacy miss path), rejection handling note, and sequencing after D7/D8. Human: ~5 min / CC: ~1 min.\nB) Skip\nDo not track; accept the possible herd as existing behavior.\nC) Build it now in this PR\nAdds a behavior change to the refactor and breaks scenario 9 parity with legacy.\n\nState: approved\nActual answer: A — \"A) Add to TODOS.md, verify first (recommended)\" (D9 answer)\nAccepted scope: TODO entry recorded (not persisted; content in the TODOS section below).\nHistory: none\n\nApproval readiness: PASS — checked R1 (D1=A), R2 (D2=B), R3 (D3=A), R4 (D4=A), R5 (D5=A), R6 (D6=A), R7 (D7=A), R8 (D8=A), R9 (D9=A). Every accepted remedy cites its own actual answer. Required proof for approved behaviors (D3 same-instance test, D4 routing tests, D5 boundary tests, D6 characterization suite) is carried as necessary work under those answers. No pending records.\n\n## Review findings by section\n\n### Step 0 — Scope Challenge (scope reduced per recommendation)\n| # | Sev | Conf | Source | Finding | Disposition |\n|---|-----|------|--------|---------|-------------|\n| S1 | P1 | 9/10 | `PLAN.md:44-45` | 5 new classes / 12 files for a behavior-preserving reorg | accepted → D2=B (3 units) |\n| S2 | P2 | 9/10 | `PLAN.md:8, 40-41` | Promise.all is a behavior change bundled into a no-behavior-change refactor | accepted → D1=A (deferred) |\n| S3 | P2 | 8/10 | `PLAN.md:44` | `TokenStore` has no stated responsibility | accepted → D2=B (folded unless author writes its job) |\n\n### Section 1 — Architecture (5 issues)\n| # | Sev | Conf | Source | Finding | Disposition |\n|---|-----|------|--------|---------|-------------|\n| A1 | P1 | 9/10 | `PLAN.md:28-29` | module-level mutable singleton shared by two mutating services | accepted → D3=A (constructor injection) |\n| A2 | P2 | 8/10 | `PLAN.md:19` | non-serialized concurrent mutation outcome unstated (revocation racing a request) | accepted → pinned by D6 scenario 10 |\n| A3 | P2 | 8/10 | `PLAN.md:8-13` | no data-flow diagram | accepted → diagram added to Architecture |\n| A4 | P2 | 8/10 | `PLAN.md:36` | live auth path replaced with no rollout mechanism | accepted → D4=A (flag, strangler) |\n| A5 | P2 | 6/10 | — | `decideAccess` must be fail-closed; current catch behavior unknown | accepted → D5=A boundary catch |\n\n### Section 2 — Code quality (5 issues)\n| # | Sev | Conf | Source | Finding | Disposition |\n|---|-----|------|--------|---------|-------------|\n| C1 | P1 | 9/10 | `PLAN.md:32-33` | 60-line function, 3 nested swallowing try/catch | accepted → D5=A |\n| C2 | P2 | 8/10 | — | IDP validation duplicated between legacy and AuthBroker under the flag | accepted as temporary; dies with D8 cleanup |\n| C3 | P2 | 7/10 | `PLAN.md:33` | error taxonomy implied but unnamed | accepted → `AuthError` union in D5 |\n| C4 | P2 | 7/10 | — | 8 edge cases unnamed in plan | accepted → listed in Code quality; tested per D6/unit list |\n| C5 | P3 | — | — | existing diagrams in touched files unverifiable here | implementation task T8 |\n\n### Section 3 — Tests (30 gaps, 1 CRITICAL)\n| # | Sev | Conf | Source | Finding | Disposition |\n|---|-----|------|--------|---------|-------------|\n| T1 | P1 | 9/10 | `PLAN.md:23-25, 36-37` | rewrite of `legacyAuthFlow()` with zero regression coverage | accepted → D6=A (differential suite, 10 scenarios) |\n| T2 | P1 | 9/10 | diagram | 29 further GAP branches/flows on new code | accepted → unit + E2E list in Tests section (proof of D3/D4/D5) |\n\n### Section 4 — Performance (4 issues)\n| # | Sev | Conf | Source | Finding | Disposition |\n|---|-----|------|--------|---------|-------------|\n| P1 | P2 | 8/10 | `PLAN.md:40-41` | 5 sequential IDP calls | deferred → D1=A; TODO per D7=A |\n| P2 | P2 | 8/10 | — | cache-hit path must short-circuit before IDP | accepted → pinned by D6 scenario 9 |\n| P3 | P3 | 6/10 | — | probable cache stampede on concurrent misses (existing) | NOT in scope; TODO per D9=A (verify first) |\n| P4 | P3 | 6/10 | — | deny logging volume under credential-stuffing burst | noted; sampling is a behavior change, out of scope |\n\n### Suppressed findings (confidence ≤ 4)\nNone.\n\n## NOT in scope\n- **Promise.all parallelization of IDP calls** — behavior change; follow-up PR (D1, TODO D7).\n- **Deleting `AUTH_BROKER_ENABLED` and `legacyAuthFlow()`** — after 100% + 2-week bake (D4, TODO D8).\n- **Single-flight / stampede protection on cache miss** — behavior change; verify legacy first (TODO D9).\n- **Deny-log sampling** — behavior change; revisit with D7.\n- **`AuthCache` facade and `RequestPolicy` class** — removed by D2; facade returns only if the adapter API proves awkward, as a helper function.\n- **`TokenStore` as a separate unit** — folded into `SessionMint` unless the author supplies a one-line distinct responsibility (D2).\n- **Editing the cache adapter or its tests** — retained unchanged (`PLAN.md:22`).\n- **Distribution/CI** — no new artifact; N/A.\n\n## What already exists\n| Existing | Plan reuses? | Note |\n|---|---|---|\n| Cache adapter (keys, eviction, invalidation hooks, tests) | Yes, directly (D2/D3) | Original plan wrapped it in an `AuthCache` facade; removed. |\n| `legacyAuthFlow()` | Yes, as the flag-off control and the characterization oracle (D4/D6) | Original plan rewrote it in place with no regression test. |\n| Per-request access decision logic | Yes, extracted as `decideAccess()` | Original plan wrapped it in a `RequestPolicy` class; now a pure function. |\n| 5 IDP validation calls | Yes, moved into `AuthBroker.validate()` unchanged in order | Temporarily duplicated with legacy under the flag. |\n| Error classes caught by the 3 nested catches | Yes, named in the `AuthError` union (D5) | Plan never listed them; implementation must map each existing class. |\n\n## Diagrams\n- Plan: request-flow diagram (Architecture) and pipeline sketch (Code quality) added above.\n- Inline ASCII comments to add in implementation: `AuthBroker.validateAndDispatch()` (3-step pipeline + fail-closed boundary); entry-point routing (flag decision tree); `SessionMint.mint()` (adapter key tuple and write path); `auth-flow.characterization.test.*` header (dual-path harness shape and the \"legacy is the oracle\" rule).\n- Check and update any existing diagram near the `legacyAuthFlow()` call site in the routing commit (C5).\n\n## Failure modes\n| New codepath | Realistic failure | Test | Handling | User sees | Critical gap? |\n|---|---|---|---|---|---|\n| Flag routing | flag service unreachable / undefined | routing test (default off) | default to legacy when flag unresolved | nothing changes | no |\n| `validate()` cache read | adapter down | `CacheUnavailable` unit test | boundary → deny + log | 401, logged | no |\n| `validate()` IDP | timeout / 5xx | scenarios 7, 8 | `ValidationError` → deny; outcome pinned to legacy | same as legacy | no |\n| `decideAccess()` | malformed claims throws | unit + boundary test | boundary → deny + log | 401, logged | no |\n| Boundary catch | unexpected non-`AuthError` | plain-`Error` test | deny + log (fail closed) | 401, logged | no |\n| `dispatch()` | downstream failure | rethrow test | `DispatchError` propagates | existing 5xx handling | no |\n| `SessionMint.mint()` | adapter write fails | unit + pinned legacy outcome | per legacy (recorded) | same as legacy | no |\n| Shared adapter | revocation races validate→dispatch | scenario 10 | per legacy (recorded) | same as legacy | no |\n| Concurrent misses | IDP herd on mass invalidation | none in this PR | none (existing behavior) | slow/rate-limited (same as today) | no — existing, tracked D9 |\n\nCritical gaps: **0**.\n\n## Worktree parallelization strategy\n| Step | Modules touched | Depends on |\n|---|---|---|\n| S1 `decideAccess()` + table tests | auth/policy | — |\n| S2 `AuthError` union + `AuthBroker` (validate/boundary/dispatch) + unit tests | auth/broker, auth/errors | S1 |\n| S3 `SessionMint` + unit tests | auth/session | — |\n| S4 Composition root wiring (inject adapter into S2, S3) | app bootstrap | S2, S3 |\n| S5 Flag routing at entry point + routing tests | auth/entry | S4 |\n| S6 Characterization suite: record legacy (step 1) | test/auth (new) | — |\n| S7 Characterization suite: run against AuthBroker (step 2) | test/auth | S5, S6 |\n\nLanes: `Lane A: S1 → S2 (shared auth/broker)` / `Lane B: S3 (independent)` / `Lane C: S6 (independent; records legacy outcomes, touches no production code)`. Launch A + B + C in parallel worktrees. Merge. Then `S4 → S5 → S7` sequentially. Conflict flag: none; A, B, C touch disjoint modules. S6 must not modify `legacyAuthFlow()`.\n\n## Implementation Tasks\nSynthesized from this review's findings. Each task derives from a specific\nfinding above. Run with Claude Code or Codex; checkbox as you ship.\n\n- [ ] **T1 (P1, human: ~2 days / CC: ~20 min)** — test/auth — Write the 10-scenario differential characterization suite; record legacy outcomes first, then assert AuthBroker parity\n - Surfaced by: Test review — T1 / REGRESSION RULE (`PLAN.md:23-25, 36-37`), D6=A\n - Files: `auth-flow.characterization.test.*`, fake IDP, in-memory fake adapter\n - Verify: suite green against legacy alone, then against both paths; must pass before flag > 0%\n- [ ] **T2 (P1, human: ~4h / CC: ~10 min)** — auth/broker — Implement `validateAndDispatch()` as validate → decideAccess → dispatch with `AuthError` union and one fail-closed boundary catch\n - Surfaced by: Code quality — C1, C3; Architecture — A5; D5=A\n - Files: `auth/broker.*`, `auth/errors.*`, `auth-broker.test.*`\n - Verify: boundary tests: each AuthError class → deny+log; plain Error → deny+log; DispatchError rethrown\n- [ ] **T3 (P1, human: ~1 day / CC: ~15 min)** — auth/entry — Add `AUTH_BROKER_ENABLED` routing (per-tenant allowlist + percentage) at the single `legacyAuthFlow()` call site; legacy untouched\n - Surfaced by: Architecture — A4, D4=A\n - Files: entry point module, `auth-routing.test.*`\n - Verify: routing tests for flag off / on+allowlisted / on+bucket / decided-once-per-request\n- [ ] **T4 (P1, human: ~2h / CC: ~5 min)** — app bootstrap — Construct one cache adapter at the composition root and inject into `AuthBroker` and `SessionMint`; no module-level export\n - Surfaced by: Architecture — A1 (`PLAN.md:28-29`), D3=A\n - Files: composition root, `auth/broker.*`, `auth/session-mint.*`, `session-mint.test.*`\n - Verify: same-instance test (mint via SessionMint, read via AuthBroker) with a fake adapter\n- [ ] **T5 (P2, human: ~3h / CC: ~5 min)** — auth/policy — Extract `decideAccess(claims, ctx)` as a pure function with a table test (allow, deny, malformed-throws)\n - Surfaced by: Scope Challenge — S1 (`PLAN.md:9-13`), D2=B\n - Files: `auth/policy.*`, `decideAccess.test.*`\n - Verify: table test passes; no mocks required\n- [ ] **T6 (P2, human: ~1 day / CC: ~15 min)** — auth/session — Implement `SessionMint.mint()` writing the `(tenant, issuer, audience, policyVer)` key; fold `TokenStore` unless the author supplies a distinct one-line responsibility\n - Surfaced by: Scope Challenge — S3 (`PLAN.md:44`), D2=B; Code quality — C4\n - Files: `auth/session-mint.*`, `session-mint.test.*`\n - Verify: write-path test; adapter.set-throws test pinned to legacy outcome\n- [ ] **T7 (P2, human: ~2h / CC: ~10 min)** — e2e — Add login → request → logout → reuse-denied, suspension mid-session, and IDP-down flows; run with flag off and on\n - Surfaced by: Test review — [→E2E] rows\n - Files: existing E2E suite location\n - Verify: identical outcomes across flag states\n- [ ] **T8 (P3, human: ~30 min / CC: ~2 min)** — auth/entry — Check and update any ASCII diagram near the `legacyAuthFlow()` call site in the routing commit\n - Surfaced by: Code quality — C5\n - Files: whichever files near the entry point carry diagrams\n - Verify: diagram matches the flag decision tree\n- [ ] **T9 (P3, human: ~30 min / CC: ~3 min)** — auth/broker, auth/session, auth/entry — Add inline ASCII diagram comments (pipeline, routing tree, mint write path, characterization harness header)\n - Surfaced by: Diagrams section\n - Files: as above\n - Verify: diagrams present and consistent with the plan's flow diagram\n\n_No new tasks from Performance review (P1 deferred to TODO; P2 covered by T1; P3/P4 out of scope)._\n\n## TODOS.md updates (accepted; **not persisted** — TODOS.md is outside this plan-mode session's authorized writes)\n\n### Parallelize the 5 IDP validation calls (D7)\n- **What:** Replace sequential IDP calls in `AuthBroker.validate()` with concurrent calls.\n- **Why:** ~5x lower validation latency on every cache miss.\n- **Pros:** ~10-line diff; single call site after cleanup; large user-visible win.\n- **Cons:** must choose `Promise.all` vs `allSettled`, per-call timeout, partial-failure semantics; IDP rate limits at 5 concurrent calls per request.\n- **Context:** deferred from the refactor by D1 to keep it behavior-identical. Extend characterization scenario 9 (5 calls still made, order unpinned) and scenarios 7/8 (first-failure semantics).\n- **Depends on / blocked by:** D8 cleanup (flag at 100%, legacy deleted); characterization suite green.\n\n### Delete `AUTH_BROKER_ENABLED` and `legacyAuthFlow()` (D8)\n- **What:** Remove the routing flag, legacy path, its tests, and the temporary IDP duplication; convert the characterization suite from differential to a frozen AuthBroker spec.\n- **Why:** flags that outlive their rollout leave a permanent second auth path: attack surface and reviewer load.\n- **Pros:** single auth path; deletes all D4-accepted duplication.\n- **Cons:** needs a bake window and a fixture-freeze step before legacy (the oracle) is deleted.\n- **Context:** trigger = 100% of tenants for a 2-week clean error-budget window. Freeze recorded legacy outcomes as fixtures first.\n- **Depends on / blocked by:** flag at 100%; bake elapsed; fixtures frozen.\n\n### Single-flight IDP fetch on concurrent cache misses (D9) — verify first\n- **What:** Per-key in-flight promise map in `AuthBroker.validate()` so concurrent misses share one IDP fetch.\n- **Why:** protects the IDP from herds on cold start and mass invalidation (policy-version bump).\n- **Pros:** ~20 lines; large IDP call reduction on cold start.\n- **Cons:** confidence 6/10 that legacy lacks it; must not cache rejections; per-process only; interacts with D7.\n- **Context:** read the legacy miss path first; close if single-flight already exists. Sequence after D7.\n- **Depends on / blocked by:** D8 cleanup; D7 landed or explicitly sequenced.\n\n## Unresolved decisions that may bite you later\nNone. D1–D9 all answered.\n\n## Completion summary\n- Step 0: Scope Challenge — scope reduced per recommendation (5 classes → 3 units; Promise.all deferred)\n- Architecture Review: 5 issues found\n- Code Quality Review: 5 issues found\n- Test Review: diagram produced, 30 gaps identified (1 CRITICAL regression, resolved by D6)\n- Performance Review: 4 issues found\n- NOT in scope: written\n- What already exists: written\n- TODOS.md updates: 3 items proposed to user (3 accepted, not persisted)\n- Failure modes: 0 critical gaps flagged\n- Unresolved decisions: 0 in this review\n- Outside voice: codex, disabled (codex_reviews=disabled; no native replacement)\n- Parallelization: 3 lanes, 3 parallel / 3 sequential steps after merge\n- Lake Score: 3/3 (D4, D5, D6 all at 10/10; D1, D2, D3, D7–D9 differ in kind)\n\n## Sources (search check)\n- [Singletons in JavaScript: Useful Tool or Hidden Trap?](https://blog.openreplay.com/singletons-javascript-tool-trap/)\n- [Module Caching in Node.js: A Practical Singleton?](https://www.linkedin.com/pulse/module-caching-nodejs-practical-singleton-jo%C3%A3o-pedro-samarino-usidf)\n- [Wiring modules: dependency injection (Node.js Design Patterns)](https://www.educative.io/courses/nodejs-design-patterns/wiring-modules-dependency-injection)\n- [Singleton, dependency injection, IoC, and Service Locator in Node.js](https://medium.com/@moali314/singleton-dependency-injection-ioc-and-service-locator-in-node-js-9a9c7a3326b7)\n\n## GSTACK REVIEW REPORT\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |\n| Outside Review | codex via `/plan-eng-review` | Independent 2nd opinion | 1 | disabled | skipped (codex_reviews=disabled) |\n| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | CLEAR | 44 issues, 0 critical gaps |\n| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |\n| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — |\n\n- **OUTSIDE COVERAGE:** provider codex, phase plan-review, status disabled (user opt-out via `codex_reviews=disabled`); no findings; no native replacement dispatched.\n- **VERDICT:** ENG CLEARED — ready to implement (scope reduced per recommendation; 9 decisions approved, 0 unresolved).\n\nNO UNRESOLVED DECISIONS\n", "calls": [ { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01KF3nUBBbWBGghdW88CGKh6", "questions": [ { "question": "D1 — Defer the Promise.all IDP parallelization out of this refactor?\nProject/branch/task: main — reviewing PLAN.md \"Multi-tenant Auth Refactor\", a stated no-behavior-change reorg of tenant auth.\nELI10: The plan promises to move code around without changing what users experience, but it also bundles in making 5 identity-provider calls run at once instead of one after another. That is a real behavior change: timing changes, and if one call fails the others are abandoned mid-flight, which changes which error the user sees. Mixing a rewrite with a speed-up means if something breaks after deploy, you cannot tell which change did it.\nStakes if we pick wrong: a login regression after ship that nobody can bisect, because the structural move and the timing change landed in the same diff.\nRecommendation: A because Beck's rule (separate structural and behavioral changes) makes the rollback and the bisect trivial, and the perf PR is a 10-line follow-up once the refactor is green.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Defer Promise.all to a follow-up PR (recommended)\n ✅ Refactor stays provably behavior-preserving; the legacy characterization test passes unchanged\n ✅ Perf change gets its own review of error semantics (first-rejection, partial failure, IDP rate limits)\n ❌ Users wait one more release for the ~5x faster token validation (human: ~1h / CC: ~5 min follow-up)\nB) Keep Promise.all in this PR\n ✅ One PR, one deploy, faster validation lands immediately\n ✅ Avoids touching the validation path twice in two weeks\n ❌ Rewrite and timing change share a blast radius; a 3am incident has two suspects\n ❌ Error-path behavior changes silently unless the plan also specifies allSettled vs all semantics\nNet: trading one release of latency for a clean bisect on the highest-blast-radius path in the product.", "header": "Perf scope", "multiSelect": false, "options": [ { "label": "Defer Promise.all to follow-up (recommended)", "description": "Refactor stays behavior-preserving; parallelization lands as its own small PR with its own error-semantics review. Human: ~1h / CC: ~5 min later." }, { "label": "Keep Promise.all in this PR", "description": "One deploy, faster validation now, but structural and behavioral changes share a blast radius and error semantics (all vs allSettled) must be specified in this plan." } ] } ], "answered": true, "failed": false, "answers": { "D1 — Defer the Promise.all IDP parallelization out of this refactor?\nProject/branch/task: main — reviewing PLAN.md \"Multi-tenant Auth Refactor\", a stated no-behavior-change reorg of tenant auth.\nELI10: The plan promises to move code around without changing what users experience, but it also bundles in making 5 identity-provider calls run at once instead of one after another. That is a real behavior change: timing changes, and if one call fails the others are abandoned mid-flight, which changes which error the user sees. Mixing a rewrite with a speed-up means if something breaks after deploy, you cannot tell which change did it.\nStakes if we pick wrong: a login regression after ship that nobody can bisect, because the structural move and the timing change landed in the same diff.\nRecommendation: A because Beck's rule (separate structural and behavioral changes) makes the rollback and the bisect trivial, and the perf PR is a 10-line follow-up once the refactor is green.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Defer Promise.all to a follow-up PR (recommended)\n ✅ Refactor stays provably behavior-preserving; the legacy characterization test passes unchanged\n ✅ Perf change gets its own review of error semantics (first-rejection, partial failure, IDP rate limits)\n ❌ Users wait one more release for the ~5x faster token validation (human: ~1h / CC: ~5 min follow-up)\nB) Keep Promise.all in this PR\n ✅ One PR, one deploy, faster validation lands immediately\n ✅ Avoids touching the validation path twice in two weeks\n ❌ Rewrite and timing change share a blast radius; a 3am incident has two suspects\n ❌ Error-path behavior changes silently unless the plan also specifies allSettled vs all semantics\nNet: trading one release of latency for a clean bisect on the highest-blast-radius path in the product.": "Defer Promise.all to follow-up (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:07:51.710Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01DLRdV6LHuTVzrLece6Reco", "questions": [ { "question": "D2 — Class arrangement: keep 5 new classes, or collapse to 3 units?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; D1 already deferred Promise.all, so both options below are behavior-preserving reorganizations.\nELI10: The plan adds five new classes to move existing auth logic around. Two of them look like wrappers around things that already exist: RequestPolicy is one pure allow/deny function with no state, and AuthCache is a pass-through over a cache adapter that already has the right rules and tests. A third, TokenStore, is named but never described. Every extra class is another file to keep in sync, another mock in every test, and another place a 3am debugger has to look. The question is how many moving parts this reorg actually needs.\nStakes if we pick wrong: too many parts means indirection nobody can explain in six months; too few means SessionMint or AuthBroker grow a hidden responsibility and become the next 60-line validateAndDispatch().\nRecommendation: B because the adapter already IS the cache contract (PLAN.md:16-22), a stateless allow/deny decision is a function not a class (PLAN.md:9-13), and a component with no written responsibility (TokenStore) should not exist until someone can write its one-line job description. This chooses structure only; the shared-singleton question and the error-handling rewrite are asked separately.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Original: 5 classes (AuthBroker, TokenStore, SessionMint, AuthCache, RequestPolicy), 12 files\n ✅ Each concern has a named home; matches the plan as written, no re-planning\n ✅ AuthCache facade gives one place to add service-facing helpers later\n ❌ Two of five classes are wrappers with no behavior of their own; TokenStore is undefined (PLAN.md:44)\n ❌ Every AuthBroker test mocks 3-4 collaborators instead of 1-2 (human: ~3 days / CC: ~45 min)\nB) Collapse to 3 units: AuthBroker, SessionMint, existing adapter; RequestPolicy as a pure function; TokenStore folded into SessionMint or dropped (recommended)\n ✅ Services depend directly on the adapter whose keys, eviction and invalidation are already tested; no facade to keep in sync\n ✅ decideAccess(claims, ctx) is trivially unit-testable with a table of cases and needs no mocks\n ❌ If the adapter interface is awkward for services, callers repeat small key-building code (fix: one helper function, not a class)\n ❌ Folding TokenStore requires the author to state what it does; if it is a real separate store, it comes back as a 4th unit (human: ~1.5 days / CC: ~25 min)\nC) Middle: 4 units — keep AuthCache facade, RequestPolicy as function, TokenStore resolved as in B\n ✅ Keeps a single service-facing cache surface if the adapter API is low-level\n ✅ Still removes the stateless class and the undefined class\n ❌ The facade is only justified by an adapter-API problem the plan does not show; likely YAGNI (human: ~2 days / CC: ~35 min)\nNet: trading a named-home-for-everything layout against the smallest arrangement that still keeps orchestration, minting and the proven cache adapter as distinct, testable seams.", "header": "Structure", "multiSelect": false, "options": [ { "label": "B) Collapse to 3 units (recommended)", "description": "AuthBroker + SessionMint depend on the existing cache adapter directly; RequestPolicy becomes decideAccess(claims, ctx); TokenStore folds into SessionMint or is dropped pending a written responsibility. Human: ~1.5 days / CC: ~25 min." }, { "label": "A) Original 5 classes", "description": "Keep AuthBroker, TokenStore, SessionMint, AuthCache, RequestPolicy across 12 files as planned. Human: ~3 days / CC: ~45 min. TokenStore still needs a written responsibility." }, { "label": "C) Middle: 4 units", "description": "Keep the AuthCache facade over the adapter; RequestPolicy as a function; TokenStore resolved as in B. Human: ~2 days / CC: ~35 min." } ] } ], "answered": true, "failed": false, "answers": { "D2 — Class arrangement: keep 5 new classes, or collapse to 3 units?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; D1 already deferred Promise.all, so both options below are behavior-preserving reorganizations.\nELI10: The plan adds five new classes to move existing auth logic around. Two of them look like wrappers around things that already exist: RequestPolicy is one pure allow/deny function with no state, and AuthCache is a pass-through over a cache adapter that already has the right rules and tests. A third, TokenStore, is named but never described. Every extra class is another file to keep in sync, another mock in every test, and another place a 3am debugger has to look. The question is how many moving parts this reorg actually needs.\nStakes if we pick wrong: too many parts means indirection nobody can explain in six months; too few means SessionMint or AuthBroker grow a hidden responsibility and become the next 60-line validateAndDispatch().\nRecommendation: B because the adapter already IS the cache contract (PLAN.md:16-22), a stateless allow/deny decision is a function not a class (PLAN.md:9-13), and a component with no written responsibility (TokenStore) should not exist until someone can write its one-line job description. This chooses structure only; the shared-singleton question and the error-handling rewrite are asked separately.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Original: 5 classes (AuthBroker, TokenStore, SessionMint, AuthCache, RequestPolicy), 12 files\n ✅ Each concern has a named home; matches the plan as written, no re-planning\n ✅ AuthCache facade gives one place to add service-facing helpers later\n ❌ Two of five classes are wrappers with no behavior of their own; TokenStore is undefined (PLAN.md:44)\n ❌ Every AuthBroker test mocks 3-4 collaborators instead of 1-2 (human: ~3 days / CC: ~45 min)\nB) Collapse to 3 units: AuthBroker, SessionMint, existing adapter; RequestPolicy as a pure function; TokenStore folded into SessionMint or dropped (recommended)\n ✅ Services depend directly on the adapter whose keys, eviction and invalidation are already tested; no facade to keep in sync\n ✅ decideAccess(claims, ctx) is trivially unit-testable with a table of cases and needs no mocks\n ❌ If the adapter interface is awkward for services, callers repeat small key-building code (fix: one helper function, not a class)\n ❌ Folding TokenStore requires the author to state what it does; if it is a real separate store, it comes back as a 4th unit (human: ~1.5 days / CC: ~25 min)\nC) Middle: 4 units — keep AuthCache facade, RequestPolicy as function, TokenStore resolved as in B\n ✅ Keeps a single service-facing cache surface if the adapter API is low-level\n ✅ Still removes the stateless class and the undefined class\n ❌ The facade is only justified by an adapter-API problem the plan does not show; likely YAGNI (human: ~2 days / CC: ~35 min)\nNet: trading a named-home-for-everything layout against the smallest arrangement that still keeps orchestration, minting and the proven cache adapter as distinct, testable seams.": "B) Collapse to 3 units (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:08:17.845Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01MrweLbYBtGxabTu3kAy9Ha", "questions": [ { "question": "D3 — Share the cache adapter by constructor injection, or by module-level singleton export?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; structure fixed at 3 units (D2), so this is about how AuthBroker and SessionMint both reach the one existing cache adapter.\nELI10: Both new services read and write the same cache. The plan hands it to them as a file-level global that any file can import and mutate. The alternative is that whoever starts the app creates the adapter once and passes it into both services' constructors. Same single cache at runtime, but with injection every test can hand in a fresh fake instead of scrubbing a global between tests, and the dependency is visible in the signature instead of hidden in an import.\nStakes if we pick wrong: with a global, tests leak tenant state into each other and pass or fail depending on file order; in production a duplicated package install silently gives you two caches and a revocation that only lands in one of them.\nRecommendation: A because it is the boring Layer 1 answer, costs one constructor parameter per service, and turns \"both services mutate a global\" into \"both services mutate the instance they were given\", which is testable and greppable.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Constructor injection from the composition root (recommended)\n ✅ Every test constructs services with an in-memory fake adapter; no global reset, no order dependence\n ✅ The single-instance contract is enforced where the app boots, in one visible place, not by module-cache luck\n ❌ Callers that construct AuthBroker or SessionMint must now pass the adapter (human: ~2h / CC: ~5 min)\nB) Module-level exported singleton (as planned)\n ✅ Zero wiring; any file imports the adapter and goes\n ✅ Matches the plan as written, no re-planning\n ❌ Hidden shared mutable state across two services; Jest/Vitest module-cache resets and duplicated installs break the \"one backing cache\" assumption\n ❌ Tests must reset or mock the module, and mocking a module export is the flakiest kind of mock (human: ~0 now / ongoing test-flake tax)\nNet: one constructor parameter now versus hidden shared state on the auth hot path forever.", "header": "Cache sharing", "multiSelect": false, "options": [ { "label": "A) Constructor injection (recommended)", "description": "Composition root creates the one adapter instance and passes it to both AuthBroker and SessionMint constructors. Tests use an in-memory fake. Human: ~2h / CC: ~5 min." }, { "label": "B) Module-level singleton export", "description": "Keep the exported mutable singleton as planned; both services import and mutate it. No wiring cost; hidden shared state and module-mock test tax." } ] } ], "answered": true, "failed": false, "answers": { "D3 — Share the cache adapter by constructor injection, or by module-level singleton export?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; structure fixed at 3 units (D2), so this is about how AuthBroker and SessionMint both reach the one existing cache adapter.\nELI10: Both new services read and write the same cache. The plan hands it to them as a file-level global that any file can import and mutate. The alternative is that whoever starts the app creates the adapter once and passes it into both services' constructors. Same single cache at runtime, but with injection every test can hand in a fresh fake instead of scrubbing a global between tests, and the dependency is visible in the signature instead of hidden in an import.\nStakes if we pick wrong: with a global, tests leak tenant state into each other and pass or fail depending on file order; in production a duplicated package install silently gives you two caches and a revocation that only lands in one of them.\nRecommendation: A because it is the boring Layer 1 answer, costs one constructor parameter per service, and turns \"both services mutate a global\" into \"both services mutate the instance they were given\", which is testable and greppable.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Constructor injection from the composition root (recommended)\n ✅ Every test constructs services with an in-memory fake adapter; no global reset, no order dependence\n ✅ The single-instance contract is enforced where the app boots, in one visible place, not by module-cache luck\n ❌ Callers that construct AuthBroker or SessionMint must now pass the adapter (human: ~2h / CC: ~5 min)\nB) Module-level exported singleton (as planned)\n ✅ Zero wiring; any file imports the adapter and goes\n ✅ Matches the plan as written, no re-planning\n ❌ Hidden shared mutable state across two services; Jest/Vitest module-cache resets and duplicated installs break the \"one backing cache\" assumption\n ❌ Tests must reset or mock the module, and mocking a module export is the flakiest kind of mock (human: ~0 now / ongoing test-flake tax)\nNet: one constructor parameter now versus hidden shared state on the auth hot path forever.": "A) Constructor injection (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:10:13.552Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_014a8s93sdnAgKr7tpX6RYv7", "questions": [ { "question": "D4 — How does the new AuthBroker path replace legacyAuthFlow() in production?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; the rewrite replaces the live auth path for every tenant.\nELI10: Right now every login and every request goes through legacyAuthFlow(). The plan swaps in the new code for everyone at once. If the new path has a bug nobody caught, every tenant is locked out until a rollback ships. A feature flag keeps the old path alive for a week or two, sends a slice of tenants to the new path first, and lets you flip back in seconds instead of redeploying. The cost is carrying two paths briefly and a follow-up PR to delete the flag.\nStakes if we pick wrong: a full-tenant auth outage that needs a redeploy to fix, versus two weeks of a routing flag that has to be cleaned up.\nRecommendation: A because auth is the widest blast radius in the product, a flag makes a wrong choice cheap to undo (reversibility), and the deletion follow-up is a 5-minute CC task.\nCompleteness: A=10/10, B=6/10, C=3/10\nPros / cons:\nA) Feature flag with per-tenant / percentage routing; legacy retained until 100% then deleted (recommended)\n ✅ Instant rollback per tenant without a deploy; strangler migration instead of big-bang\n ✅ Lets the regression characterization test run both paths side by side on real traffic shapes\n ❌ Two code paths live for ~1-2 weeks and a follow-up PR removes the flag and legacyAuthFlow() (human: ~1 day / CC: ~15 min)\nB) Deploy-level canary only; legacy deleted in this PR\n ✅ No in-code flag to clean up; relies on existing deploy tooling\n ✅ Smaller diff than A\n ❌ Rollback is a redeploy of the previous build, minutes not seconds; no per-tenant control\n ❌ Requires canary deploy tooling the plan does not show exists (human: ~2h / CC: ~5 min)\nC) Big-bang replace, no flag, no canary\n ✅ Smallest possible diff and no cleanup work\n ✅ Nothing to coordinate\n ❌ Every tenant on the new path at once; a missed edge case is a total auth outage until rollback deploys\nNet: two weeks of a routing flag against the possibility of an all-tenant lockout on the first bad deploy.", "header": "Rollout", "multiSelect": false, "options": [ { "label": "A) Feature flag, strangler rollout (recommended)", "description": "Keep legacyAuthFlow() behind a flag; route per-tenant/percentage to AuthBroker; delete flag and legacy in a follow-up at 100%. Completeness 10/10. Human: ~1 day / CC: ~15 min." }, { "label": "B) Deploy canary only", "description": "Delete legacyAuthFlow() in this PR; rely on deploy-level canary and redeploy rollback. Completeness 6/10. Human: ~2h / CC: ~5 min." }, { "label": "C) Big-bang replace", "description": "Replace in place with no flag or canary. Completeness 3/10. Smallest diff; all-tenant blast radius." } ] } ], "answered": true, "failed": false, "answers": { "D4 — How does the new AuthBroker path replace legacyAuthFlow() in production?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; the rewrite replaces the live auth path for every tenant.\nELI10: Right now every login and every request goes through legacyAuthFlow(). The plan swaps in the new code for everyone at once. If the new path has a bug nobody caught, every tenant is locked out until a rollback ships. A feature flag keeps the old path alive for a week or two, sends a slice of tenants to the new path first, and lets you flip back in seconds instead of redeploying. The cost is carrying two paths briefly and a follow-up PR to delete the flag.\nStakes if we pick wrong: a full-tenant auth outage that needs a redeploy to fix, versus two weeks of a routing flag that has to be cleaned up.\nRecommendation: A because auth is the widest blast radius in the product, a flag makes a wrong choice cheap to undo (reversibility), and the deletion follow-up is a 5-minute CC task.\nCompleteness: A=10/10, B=6/10, C=3/10\nPros / cons:\nA) Feature flag with per-tenant / percentage routing; legacy retained until 100% then deleted (recommended)\n ✅ Instant rollback per tenant without a deploy; strangler migration instead of big-bang\n ✅ Lets the regression characterization test run both paths side by side on real traffic shapes\n ❌ Two code paths live for ~1-2 weeks and a follow-up PR removes the flag and legacyAuthFlow() (human: ~1 day / CC: ~15 min)\nB) Deploy-level canary only; legacy deleted in this PR\n ✅ No in-code flag to clean up; relies on existing deploy tooling\n ✅ Smaller diff than A\n ❌ Rollback is a redeploy of the previous build, minutes not seconds; no per-tenant control\n ❌ Requires canary deploy tooling the plan does not show exists (human: ~2h / CC: ~5 min)\nC) Big-bang replace, no flag, no canary\n ✅ Smallest possible diff and no cleanup work\n ✅ Nothing to coordinate\n ❌ Every tenant on the new path at once; a missed edge case is a total auth outage until rollback deploys\nNet: two weeks of a routing flag against the possibility of an all-tenant lockout on the first bad deploy.": "A) Feature flag, strangler rollout (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:11:12.928Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_0198gUJmMPFsFi7k5k53U3kL", "questions": [ { "question": "D5 — Flatten validateAndDispatch() into a typed pipeline with one fail-closed boundary, or keep the nested try/catch shape?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; AuthBroker.validateAndDispatch() is the new hot path behind the D4 flag.\nELI10: The planned function does three jobs (check the token, decide access, hand off the request) with three try/catch blocks nested inside each other, and each one quietly eats a kind of error. On an auth path, a quietly eaten error means a request can slip through as allowed when the code actually failed. The fix is to write the three jobs as three small named steps, give errors real types, and have exactly one place at the top that turns any failure into \"deny\" plus a log line.\nStakes if we pick wrong: a swallowed validation or policy error becomes a silent fail-open; the user sees a successful request that should have been a 401, and nobody sees a log.\nRecommendation: A because fail-closed by construction beats fail-closed by remembering three catch blocks, and three named steps are what the Section 3 tests need to target anyway.\nCompleteness: A=10/10, B=7/10, C=2/10\nPros / cons:\nA) Flat 3-step pipeline, typed AuthError union, single fail-closed boundary (recommended)\n ✅ Any thrown error in validate or decideAccess becomes deny plus a structured log; nothing is swallowed\n ✅ Each step is a ~10-line unit with its own table test; validateAndDispatch() shrinks to ~20 lines\n ❌ Introduces an AuthError type union that legacy does not have (human: ~4h / CC: ~10 min)\nB) Keep nesting, replace each swallow with log + deny/rethrow\n ✅ Smallest change to the planned shape; no new types\n ✅ Still closes the fail-open hole in each catch\n ❌ Three separate places must stay correct; the 60-line function remains hard to test branch by branch (human: ~2h / CC: ~5 min)\nC) Keep as planned (swallowing catches)\n ✅ No work\n ✅ Matches the current draft exactly\n ❌ Silent fail-open on the auth path; unacceptable for a no-behavior-change refactor whose whole point is safety\nNet: a small typed union and three named functions against three catch blocks that each have to be remembered to deny.", "header": "Error handling", "multiSelect": false, "options": [ { "label": "A) Flat pipeline + typed errors (recommended)", "description": "validate → decideAccess → dispatch as named steps; AuthError union; one boundary catch maps any failure to deny + structured log, dispatch errors rethrown. Completeness 10/10. Human: ~4h / CC: ~10 min." }, { "label": "B) Keep nesting, stop swallowing", "description": "Retain three nested try/catch; each catch logs and denies or rethrows. Completeness 7/10. Human: ~2h / CC: ~5 min." }, { "label": "C) Keep as planned", "description": "Nested swallowing try/catch as drafted. Completeness 2/10. Silent fail-open risk on the auth path." } ] } ], "answered": true, "failed": false, "answers": { "D5 — Flatten validateAndDispatch() into a typed pipeline with one fail-closed boundary, or keep the nested try/catch shape?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; AuthBroker.validateAndDispatch() is the new hot path behind the D4 flag.\nELI10: The planned function does three jobs (check the token, decide access, hand off the request) with three try/catch blocks nested inside each other, and each one quietly eats a kind of error. On an auth path, a quietly eaten error means a request can slip through as allowed when the code actually failed. The fix is to write the three jobs as three small named steps, give errors real types, and have exactly one place at the top that turns any failure into \"deny\" plus a log line.\nStakes if we pick wrong: a swallowed validation or policy error becomes a silent fail-open; the user sees a successful request that should have been a 401, and nobody sees a log.\nRecommendation: A because fail-closed by construction beats fail-closed by remembering three catch blocks, and three named steps are what the Section 3 tests need to target anyway.\nCompleteness: A=10/10, B=7/10, C=2/10\nPros / cons:\nA) Flat 3-step pipeline, typed AuthError union, single fail-closed boundary (recommended)\n ✅ Any thrown error in validate or decideAccess becomes deny plus a structured log; nothing is swallowed\n ✅ Each step is a ~10-line unit with its own table test; validateAndDispatch() shrinks to ~20 lines\n ❌ Introduces an AuthError type union that legacy does not have (human: ~4h / CC: ~10 min)\nB) Keep nesting, replace each swallow with log + deny/rethrow\n ✅ Smallest change to the planned shape; no new types\n ✅ Still closes the fail-open hole in each catch\n ❌ Three separate places must stay correct; the 60-line function remains hard to test branch by branch (human: ~2h / CC: ~5 min)\nC) Keep as planned (swallowing catches)\n ✅ No work\n ✅ Matches the current draft exactly\n ❌ Silent fail-open on the auth path; unacceptable for a no-behavior-change refactor whose whole point is safety\nNet: a small typed union and three named functions against three catch blocks that each have to be remembered to deny.": "A) Flat pipeline + typed errors (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:12:58.626Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01EoAaca9PDfeVVKC2fgAu1y", "questions": [ { "question": "D6 — How do we prove AuthBroker matches legacyAuthFlow() before the flag reaches 100%?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; the plan rewrites the live auth path and currently plans zero tests of the behavior it promises to preserve.\nELI10: The whole promise of this refactor is \"users notice nothing\". Right now nothing checks that. A characterization test records what the old code does for each situation (good token, expired token, revoked token, identity provider down, and so on) and then runs the exact same situations through the new code, failing if any answer differs. Without it, \"no behavior change\" is a hope, not a fact, and the feature flag from D4 only tells you something broke after a tenant is already locked out.\nStakes if we pick wrong: a tenant lockout or, worse, a fail-open on the new path that nobody can distinguish from legacy because nobody wrote down what legacy did.\nRecommendation: A because it is the only option that tests the promise the plan actually makes, it doubles as the spec for unknown legacy outcomes (IDP timeout, write failure, revocation race), and with CC it is ~20 minutes of work.\nCompleteness: A=10/10, B=5/10\nPros / cons:\nA) Differential characterization suite, 10 scenarios, run against both paths (recommended)\n ✅ Every deny/allow/eviction/IDP-call-count outcome is pinned from legacy and asserted identical on AuthBroker\n ✅ Forces the unknown legacy outcomes (IDP timeout, adapter write failure, revocation race) to be recorded instead of guessed\n ❌ Needs a fake IDP and fake adapter harness that can drive both paths identically (human: ~2 days / CC: ~20 min)\nB) Golden snapshot of legacy, 4 scenarios, no cross-path assertion\n ✅ Cheap; documents the four most common outcomes\n ✅ No dual-path harness needed\n ❌ Does not assert AuthBroker matches legacy at all; the six error-path scenarios that cause incidents stay unrecorded (human: ~3h / CC: ~5 min)\nNet: a dual-path harness now against discovering what legacy did from a production incident later.", "header": "Regression", "multiSelect": false, "options": [ { "label": "A) Differential suite, 10 scenarios (recommended)", "description": "Record legacyAuthFlow() outcomes for 10 scenarios (allow, policy deny, expired, revoked, suspended, malformed, IDP 5xx, IDP timeout, cache hit/miss call counts, revocation race); run the same table against AuthBroker via the flag; assert identical status, claims, adapter state, IDP call count/order. Completeness 10/10. Human: ~2 days / CC: ~20 min." }, { "label": "B) Golden snapshot, 4 scenarios, legacy only", "description": "Snapshot legacy for allow, policy deny, expired, malformed. No cross-path assertion. Completeness 5/10. Human: ~3h / CC: ~5 min." } ] } ], "answered": true, "failed": false, "answers": { "D6 — How do we prove AuthBroker matches legacyAuthFlow() before the flag reaches 100%?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; the plan rewrites the live auth path and currently plans zero tests of the behavior it promises to preserve.\nELI10: The whole promise of this refactor is \"users notice nothing\". Right now nothing checks that. A characterization test records what the old code does for each situation (good token, expired token, revoked token, identity provider down, and so on) and then runs the exact same situations through the new code, failing if any answer differs. Without it, \"no behavior change\" is a hope, not a fact, and the feature flag from D4 only tells you something broke after a tenant is already locked out.\nStakes if we pick wrong: a tenant lockout or, worse, a fail-open on the new path that nobody can distinguish from legacy because nobody wrote down what legacy did.\nRecommendation: A because it is the only option that tests the promise the plan actually makes, it doubles as the spec for unknown legacy outcomes (IDP timeout, write failure, revocation race), and with CC it is ~20 minutes of work.\nCompleteness: A=10/10, B=5/10\nPros / cons:\nA) Differential characterization suite, 10 scenarios, run against both paths (recommended)\n ✅ Every deny/allow/eviction/IDP-call-count outcome is pinned from legacy and asserted identical on AuthBroker\n ✅ Forces the unknown legacy outcomes (IDP timeout, adapter write failure, revocation race) to be recorded instead of guessed\n ❌ Needs a fake IDP and fake adapter harness that can drive both paths identically (human: ~2 days / CC: ~20 min)\nB) Golden snapshot of legacy, 4 scenarios, no cross-path assertion\n ✅ Cheap; documents the four most common outcomes\n ✅ No dual-path harness needed\n ❌ Does not assert AuthBroker matches legacy at all; the six error-path scenarios that cause incidents stay unrecorded (human: ~3h / CC: ~5 min)\nNet: a dual-path harness now against discovering what legacy did from a production incident later.": "A) Differential suite, 10 scenarios (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:15:14.679Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01WnJez3yiVvd3LRTqqriZLh", "questions": [ { "question": "D7 — TODO: Parallelize the 5 IDP validation calls (follow-up PR from D1)?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; D1 deferred this out of the refactor.\nELI10: Token validation waits on five identity-provider calls one after another. They do not depend on each other, so they can run at once and cut validation latency roughly 5x. We pulled it out of this refactor so the refactor stays behavior-identical; this decides whether the follow-up gets written down so it does not get lost.\nWhat: follow-up PR replacing sequential IDP calls in AuthBroker.validate() with concurrent calls.\nWhy: ~5x lower validation latency on every cache miss; users wait less on login and cold requests.\nPros: small diff (~10 lines); big user-visible latency win; AuthBroker.validate() is the single place to change after this refactor.\nCons: must decide Promise.all vs allSettled, per-call timeout, and partial-failure semantics; changes which error surfaces first; IDP rate limits may bite at 5 concurrent calls per request.\nContext: after the D4 flag reaches 100% and legacy is deleted, validate() is the only IDP call site. Start there; extend the characterization suite scenario 9 to assert 5 calls still made (order no longer pinned) and scenarios 7/8 for first-failure semantics.\nDepends on / blocked by: D4 flag at 100% and legacyAuthFlow() deleted (so there is one call site); characterization suite green.\nStakes if we pick wrong: a real latency win is forgotten, or it lands before the refactor is proven and muddies the bisect.\nRecommendation: A because it is a durable, high-value follow-up with a clear trigger, and losing it would waste the reason we deferred it cleanly.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Add to TODOS.md (recommended)\n ✅ Captures the trigger (flag at 100%) and the open semantics questions so the follow-up starts informed\n ✅ Keeps this refactor behavior-identical while guaranteeing the perf win is scheduled\n ❌ One more TODO entry to groom (human: ~5 min / CC: ~1 min)\nB) Skip — not valuable enough\n ✅ Nothing to track\n ✅ Zero grooming cost\n ❌ A 5x latency win on every cache miss silently evaporates\nC) Build it now in this PR instead of deferring\n ✅ Users get the latency win one release sooner\n ✅ Validation path touched once\n ❌ Reverses D1: mixes a behavior change into the no-behavior-change refactor and breaks the clean bisect\nNet: a one-line TODO with a trigger versus losing the follow-up or re-bundling it.", "header": "TODO: IDP perf", "multiSelect": false, "options": [ { "label": "A) Add to TODOS.md (recommended)", "description": "Record the follow-up with trigger (D4 flag at 100%, legacy deleted), open semantics (all vs allSettled, per-call timeout), and test extensions. Human: ~5 min / CC: ~1 min." }, { "label": "B) Skip", "description": "Do not track; the parallelization idea is dropped." }, { "label": "C) Build it now in this PR", "description": "Reverses D1; bundles the perf change into the refactor." } ] } ], "answered": true, "failed": false, "answers": { "D7 — TODO: Parallelize the 5 IDP validation calls (follow-up PR from D1)?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; D1 deferred this out of the refactor.\nELI10: Token validation waits on five identity-provider calls one after another. They do not depend on each other, so they can run at once and cut validation latency roughly 5x. We pulled it out of this refactor so the refactor stays behavior-identical; this decides whether the follow-up gets written down so it does not get lost.\nWhat: follow-up PR replacing sequential IDP calls in AuthBroker.validate() with concurrent calls.\nWhy: ~5x lower validation latency on every cache miss; users wait less on login and cold requests.\nPros: small diff (~10 lines); big user-visible latency win; AuthBroker.validate() is the single place to change after this refactor.\nCons: must decide Promise.all vs allSettled, per-call timeout, and partial-failure semantics; changes which error surfaces first; IDP rate limits may bite at 5 concurrent calls per request.\nContext: after the D4 flag reaches 100% and legacy is deleted, validate() is the only IDP call site. Start there; extend the characterization suite scenario 9 to assert 5 calls still made (order no longer pinned) and scenarios 7/8 for first-failure semantics.\nDepends on / blocked by: D4 flag at 100% and legacyAuthFlow() deleted (so there is one call site); characterization suite green.\nStakes if we pick wrong: a real latency win is forgotten, or it lands before the refactor is proven and muddies the bisect.\nRecommendation: A because it is a durable, high-value follow-up with a clear trigger, and losing it would waste the reason we deferred it cleanly.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Add to TODOS.md (recommended)\n ✅ Captures the trigger (flag at 100%) and the open semantics questions so the follow-up starts informed\n ✅ Keeps this refactor behavior-identical while guaranteeing the perf win is scheduled\n ❌ One more TODO entry to groom (human: ~5 min / CC: ~1 min)\nB) Skip — not valuable enough\n ✅ Nothing to track\n ✅ Zero grooming cost\n ❌ A 5x latency win on every cache miss silently evaporates\nC) Build it now in this PR instead of deferring\n ✅ Users get the latency win one release sooner\n ✅ Validation path touched once\n ❌ Reverses D1: mixes a behavior change into the no-behavior-change refactor and breaks the clean bisect\nNet: a one-line TODO with a trigger versus losing the follow-up or re-bundling it.": "A) Add to TODOS.md (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:17:10.667Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_015uBCoQcgtNd81aqXKiFztg", "questions": [ { "question": "D8 — TODO: Delete the AUTH_BROKER_ENABLED flag and legacyAuthFlow() once at 100% (follow-up from D4)?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; D4 chose a strangler rollout that keeps legacy alive behind a flag.\nELI10: The flag from D4 keeps the old auth code around as a safety net while the new code proves itself. Once every tenant is on the new path and nothing has broken for a while, the old code and the flag are dead weight: two paths to read, two to keep secure. This decides whether the cleanup is written down with a trigger so the flag does not live forever.\nWhat: follow-up PR removing the routing flag, legacyAuthFlow(), its tests, and the now-redundant IDP call duplication; convert the characterization suite from differential to a plain AuthBroker spec.\nWhy: flags that outlive their rollout are the #1 source of dead auth code paths; every extra path is attack surface and reviewer load.\nPros: deletes ~all the temporary duplication accepted in D4; single auth path; smaller security review surface.\nCons: needs a defined bake period and a rollback-free commitment; deleting legacy means the characterization suite loses its oracle and must be frozen as the spec first.\nContext: trigger is flag at 100% for all tenants for an agreed bake window (suggest 2 weeks of clean error budget). Freeze the recorded legacy outcomes as fixtures before deleting legacy so the suite keeps its assertions.\nDepends on / blocked by: flag at 100%; bake window elapsed; characterization suite outcomes frozen as fixtures.\nStakes if we pick wrong: a permanent flag with a permanent second auth path, or a premature deletion that removes the rollback lever.\nRecommendation: A because every strangler needs its funeral scheduled at birth; the trigger and the fixture-freeze step are the two things people forget.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Add to TODOS.md (recommended)\n ✅ Names the bake window and the fixture-freeze prerequisite so deletion is safe and actually happens\n ✅ Prevents the flag from becoming permanent architecture\n ❌ One more entry to groom (human: ~5 min / CC: ~1 min)\nB) Skip — not valuable enough\n ✅ Nothing to track\n ✅ Zero grooming cost\n ❌ Flag and legacyAuthFlow() likely live for years; two auth paths forever\nC) Build it now in this PR instead of deferring\n ✅ No cleanup PR later\n ✅ Single path immediately\n ❌ Reverses D4: deletes the rollback lever before the new path has served a single production request\nNet: schedule the cleanup with a trigger, or accept a permanent second auth path.", "header": "TODO: cleanup", "multiSelect": false, "options": [ { "label": "A) Add to TODOS.md (recommended)", "description": "Record the flag/legacy removal with trigger (100% for a 2-week bake), the fixture-freeze prerequisite, and the suite conversion. Human: ~5 min / CC: ~1 min." }, { "label": "B) Skip", "description": "Do not track the cleanup." }, { "label": "C) Build it now in this PR", "description": "Reverses D4; deletes legacy and the rollback lever in this PR." } ] } ], "answered": true, "failed": false, "answers": { "D8 — TODO: Delete the AUTH_BROKER_ENABLED flag and legacyAuthFlow() once at 100% (follow-up from D4)?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; D4 chose a strangler rollout that keeps legacy alive behind a flag.\nELI10: The flag from D4 keeps the old auth code around as a safety net while the new code proves itself. Once every tenant is on the new path and nothing has broken for a while, the old code and the flag are dead weight: two paths to read, two to keep secure. This decides whether the cleanup is written down with a trigger so the flag does not live forever.\nWhat: follow-up PR removing the routing flag, legacyAuthFlow(), its tests, and the now-redundant IDP call duplication; convert the characterization suite from differential to a plain AuthBroker spec.\nWhy: flags that outlive their rollout are the #1 source of dead auth code paths; every extra path is attack surface and reviewer load.\nPros: deletes ~all the temporary duplication accepted in D4; single auth path; smaller security review surface.\nCons: needs a defined bake period and a rollback-free commitment; deleting legacy means the characterization suite loses its oracle and must be frozen as the spec first.\nContext: trigger is flag at 100% for all tenants for an agreed bake window (suggest 2 weeks of clean error budget). Freeze the recorded legacy outcomes as fixtures before deleting legacy so the suite keeps its assertions.\nDepends on / blocked by: flag at 100%; bake window elapsed; characterization suite outcomes frozen as fixtures.\nStakes if we pick wrong: a permanent flag with a permanent second auth path, or a premature deletion that removes the rollback lever.\nRecommendation: A because every strangler needs its funeral scheduled at birth; the trigger and the fixture-freeze step are the two things people forget.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Add to TODOS.md (recommended)\n ✅ Names the bake window and the fixture-freeze prerequisite so deletion is safe and actually happens\n ✅ Prevents the flag from becoming permanent architecture\n ❌ One more entry to groom (human: ~5 min / CC: ~1 min)\nB) Skip — not valuable enough\n ✅ Nothing to track\n ✅ Zero grooming cost\n ❌ Flag and legacyAuthFlow() likely live for years; two auth paths forever\nC) Build it now in this PR instead of deferring\n ✅ No cleanup PR later\n ✅ Single path immediately\n ❌ Reverses D4: deletes the rollback lever before the new path has served a single production request\nNet: schedule the cleanup with a trigger, or accept a permanent second auth path.": "A) Add to TODOS.md (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:17:31.340Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01V222mV28XvKFn1rJRpHEng", "questions": [ { "question": "D9 — TODO: Single-flight the IDP fetch on concurrent cache misses (from P3)?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; Section 4 flagged a probable cache stampede that is existing behavior.\nELI10: When many requests for the same tenant arrive at once and the cache is cold, each one separately calls the identity provider five times. Ten simultaneous requests means fifty calls for one answer. A single-flight guard makes the first request fetch and the other nine wait for that result. This is almost certainly how legacy behaves today too, so fixing it is a behavior change and does not belong in this refactor; this decides whether to write it down.\nWhat: follow-up adding a per-key in-flight promise map in AuthBroker.validate() so concurrent misses share one IDP fetch.\nWhy: protects the IDP from thundering herds on cold start and after mass invalidation (tenant policy version bump evicts every entry at once).\nPros: ~20 lines; big IDP call reduction on cold start; reduces IDP rate-limit incidents.\nCons: medium confidence (6/10) that legacy lacks it, unverified without source; in-flight map must handle rejection (do not cache failures) and must be per-process only; interacts with the D7 parallelization PR.\nContext: verify first by reading legacyAuthFlow() cache-miss path; if a single-flight already exists, close this TODO. Sequence after D7 so both changes to validate() are reviewed with the perf semantics together.\nDepends on / blocked by: D8 cleanup (single call site); D7 parallelization landed or explicitly sequenced.\nStakes if we pick wrong: an IDP rate-limit outage on the next mass invalidation, or a TODO that turns out to describe behavior that already exists.\nRecommendation: A because the failure mode (mass invalidation → herd) is realistic for a multi-tenant auth system and the verification step is cheap; a TODO with a \"verify first\" note is the honest shape for a 6/10 finding.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Add to TODOS.md with a verify-first note (recommended)\n ✅ Captures a realistic IDP-protection gap with its verification step and sequencing after D7/D8\n ✅ Cheap to close if legacy already single-flights\n ❌ Medium-confidence finding; may be a no-op after verification (human: ~5 min / CC: ~1 min)\nB) Skip — not valuable enough\n ✅ Nothing to track\n ✅ Avoids a TODO built on an unverified assumption\n ❌ If the herd is real, the next policy-version bump is an IDP rate-limit incident nobody planned for\nC) Build it now in this PR instead of deferring\n ✅ IDP protected immediately\n ✅ One review of validate()\n ❌ Behavior change inside the no-behavior-change refactor; breaks scenario 9's call-count pinning against legacy\nNet: a verify-first TODO versus betting the IDP on an unverified assumption about legacy.", "header": "TODO: herd", "multiSelect": false, "options": [ { "label": "A) Add to TODOS.md, verify first (recommended)", "description": "Record the single-flight follow-up with a verify-first step (read legacy miss path), rejection handling note, and sequencing after D7/D8. Human: ~5 min / CC: ~1 min." }, { "label": "B) Skip", "description": "Do not track; accept the possible herd as existing behavior." }, { "label": "C) Build it now in this PR", "description": "Adds a behavior change to the refactor and breaks scenario 9 parity with legacy." } ] } ], "answered": true, "failed": false, "answers": { "D9 — TODO: Single-flight the IDP fetch on concurrent cache misses (from P3)?\nProject/branch/task: main — PLAN.md \"Multi-tenant Auth Refactor\"; Section 4 flagged a probable cache stampede that is existing behavior.\nELI10: When many requests for the same tenant arrive at once and the cache is cold, each one separately calls the identity provider five times. Ten simultaneous requests means fifty calls for one answer. A single-flight guard makes the first request fetch and the other nine wait for that result. This is almost certainly how legacy behaves today too, so fixing it is a behavior change and does not belong in this refactor; this decides whether to write it down.\nWhat: follow-up adding a per-key in-flight promise map in AuthBroker.validate() so concurrent misses share one IDP fetch.\nWhy: protects the IDP from thundering herds on cold start and after mass invalidation (tenant policy version bump evicts every entry at once).\nPros: ~20 lines; big IDP call reduction on cold start; reduces IDP rate-limit incidents.\nCons: medium confidence (6/10) that legacy lacks it, unverified without source; in-flight map must handle rejection (do not cache failures) and must be per-process only; interacts with the D7 parallelization PR.\nContext: verify first by reading legacyAuthFlow() cache-miss path; if a single-flight already exists, close this TODO. Sequence after D7 so both changes to validate() are reviewed with the perf semantics together.\nDepends on / blocked by: D8 cleanup (single call site); D7 parallelization landed or explicitly sequenced.\nStakes if we pick wrong: an IDP rate-limit outage on the next mass invalidation, or a TODO that turns out to describe behavior that already exists.\nRecommendation: A because the failure mode (mass invalidation → herd) is realistic for a multi-tenant auth system and the verification step is cheap; a TODO with a \"verify first\" note is the honest shape for a 6/10 finding.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Add to TODOS.md with a verify-first note (recommended)\n ✅ Captures a realistic IDP-protection gap with its verification step and sequencing after D7/D8\n ✅ Cheap to close if legacy already single-flights\n ❌ Medium-confidence finding; may be a no-op after verification (human: ~5 min / CC: ~1 min)\nB) Skip — not valuable enough\n ✅ Nothing to track\n ✅ Avoids a TODO built on an unverified assumption\n ❌ If the herd is real, the next policy-version bump is an IDP rate-limit incident nobody planned for\nC) Build it now in this PR instead of deferring\n ✅ IDP protected immediately\n ✅ One review of validate()\n ❌ Behavior change inside the no-behavior-change refactor; breaks scenario 9's call-count pinning against legacy\nNet: a verify-first TODO versus betting the IDP on an unverified assumption about legacy.": "A) Add to TODOS.md, verify first (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:17:55.534Z" }, { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_013PbWrLRKSkP7LkkoprUwpx", "questions": [ { "question": "D10 — Next step after the engineering review\nProject/branch/task: gstack-plan-count-LFSd4Z on main — Multi-tenant Auth Refactor plan, eng review CLEAR with 9 approved decisions.\nELI10: The plan has now been checked for architecture, code shape, test coverage and performance, and every open call was decided. What's left is choosing what happens next: start building, or run another kind of review first. There is no UI in this plan, so a design review has nothing to look at; the scope was already challenged and cut in Step 0, so a CEO review would mostly repeat that.\nStakes if we pick wrong: an unnecessary review costs time and re-opens settled decisions; skipping a needed one ships a gap — but no review type with an unmet trigger remains here.\nRecommendation: C because all relevant reviews are complete — no UI scope, scope already reduced, outside voice disabled by config.\nNote: options differ in kind, not coverage — no completeness score.\nNet: ship-readiness vs. extra scrutiny on a plan whose remaining review types have no trigger.", "header": "Next step", "multiSelect": false, "options": [ { "label": "C) Ready to implement (recommended)", "description": "✅ All relevant reviews complete: 0 unresolved decisions, 0 critical gaps, regression suite gates the flag. ✅ Tasks T1–T9 and 3 lanes are ready to hand to worktrees today. ❌ No independent second opinion on record because codex is disabled in this install. Then run /ship when the work is done." }, { "label": "A) Run /plan-ceo-review first", "description": "✅ Re-examines whether the refactor is worth doing at all and whether 3 units is still too many. ✅ Useful if a stakeholder doubts the value of a behavior-preserving reorg. ❌ Step 0 already ran the scope challenge and cut 5 classes to 3; this would largely repeat it (human: ~1h / CC: ~10 min)." }, { "label": "B) Run /plan-devex-review first", "description": "✅ Would check the constructor-injection and flag ergonomics for other engineers wiring auth. ✅ Catches awkward adapter APIs before the composition root is written. ❌ The plan exposes no new developer-facing surface beyond two constructors; low expected yield (human: ~1h / CC: ~10 min)." } ] } ], "answered": true, "failed": false, "answers": { "D10 — Next step after the engineering review\nProject/branch/task: gstack-plan-count-LFSd4Z on main — Multi-tenant Auth Refactor plan, eng review CLEAR with 9 approved decisions.\nELI10: The plan has now been checked for architecture, code shape, test coverage and performance, and every open call was decided. What's left is choosing what happens next: start building, or run another kind of review first. There is no UI in this plan, so a design review has nothing to look at; the scope was already challenged and cut in Step 0, so a CEO review would mostly repeat that.\nStakes if we pick wrong: an unnecessary review costs time and re-opens settled decisions; skipping a needed one ships a gap — but no review type with an unmet trigger remains here.\nRecommendation: C because all relevant reviews are complete — no UI scope, scope already reduced, outside voice disabled by config.\nNote: options differ in kind, not coverage — no completeness score.\nNet: ship-readiness vs. extra scrutiny on a plan whose remaining review types have no trigger.": "C) Ready to implement (recommended)" }, "unansweredQuestionIndices": [], "answeredAt": "2026-09-16T14:22:47.928Z" } ], "planReadyRequests": [ { "sessionId": "144ae20f-4d81-4ba7-8779-bc669eedd3bd", "toolUseId": "toolu_01NSUnHqoqYYBfB7YazacqEq", "timestamp": "2026-09-16T14:23:44.531Z", "failed": false, "source": "pre_tool_use" } ] }