feat(mt#2719): Reviewer operator-incident paging tier — reach the page, not just the marker - #3564
Conversation
…e, not just the marker The reviewer could already record that it was structurally unable to review; it could not tell anyone. mt#4881 shipped the classified failure stream and an inbox ask. This adds the tier above it: the two conditions only the operator can clear — sustained GitHub App auth failure, and sustained provider credit exhaustion — now create a `severity: "incident"` + `forceImmediate` ask AND page the principal, with the remediation URL in the body. The defect this had to work around: setting `severity: "incident"` on the reviewer's emit path would have paged nobody. `pagePrincipalForAsk`'s only production caller was `createAsk` in the command adapter, and the reviewer's emitter deliberately writes through `repo.create` instead — so the marker would have persisted to a row nothing reads. Typechecks, deploys, healthy, inert. - Extract the production page dispatch to `@minsky/domain/ask/principal-page-dispatch` so both `createAsk` and the reviewer reach one seam. Pure relocation: every collaborator was already domain-side, and `createAsk`'s behaviour is unchanged. - `AskEmitter.emitOperatorIncidentAlert` — one method, discriminated context, for both sources (they produce the same ask shape; see the spec's SC1 amendment). - `auth-health.ts` pages on trip, deduped by the tracker's existing `tripped` flag. - `failure-alert.ts` escalates an operator-actionable class on the threshold CROSSING, evaluated before the per-PR suppression so a single-PR outage still reaches the count that proves it sustained. Self-healing classes never page. Thresholds reuse mt#4881's 60-minute window and auth-health's count of 3 rather than minting a third number for the same judgment. Deploy impact: touches services/reviewer/src and packages/domain — deploy surface per isDeploySurfaceFile.
…cated it away Ships the live smoke for the reviewer's paging composition, and fixes the defect that smoke immediately found. `buildPageMessage` excerpts an ask's question at PAGE_QUESTION_EXCERPT_CHARS (300). The remediation URL sat at the END of the incident body, so it was cut from the notification the principal actually reads on a phone — while remaining present in the ask. SC7 asks the operator to be able to act from the notification without investigating; that was silently not true. The unit tests could not see it: they asserted the URL was in `ask.question`, which was true and was not the requirement. Fixed by leading with the remediation rather than by widening the shared excerpt, which is a deliberate bound on every page and not one caller's to move. Added a regression test that asserts the rendered PAGE body instead of the ask. - `services/reviewer/scripts/smoke-operator-incident-page.ts` — dry by default (resolves the real channel, drives the real emitter, records instead of sending); `--execute` sends a real page and is opt-in because it spends one of the substrate's 3-per-24h page budget. Deploy impact: touches services/reviewer/src — deploy surface.
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Solid structural move extracting the principal-page dispatch into the domain and correctly wiring the reviewer’s emit path to reach it. Tests exercise the negative control and the decision-path well. One blocking issue: the live-smoke --execute path claims to create “a real ask” but still uses FakeAskRepository, so no persistence is exercised; this is misleading and leaves the repo-backed seam unverified. Either use a real DrizzleAskRepository from the domain container in --execute or soften the claim and checks to reflect that only the page send is real. I limited my sweep to the changed files; broader docs and other modules were not re-read. With the execute-mode fix, this looks ready.
Findings
- [BLOCKING] services/reviewer/scripts/smoke-operator-incident-page.ts:116 —
--executemode claims to create a real Ask but usesFakeAskRepository— misleading live verification and no persistence exercised
Inservices/reviewer/scripts/smoke-operator-incident-page.ts:116-132, the--executepath constructsconst repo = new FakeAskRepository();and thenconst emitter = new DomainAskEmitter(() => Promise.resolve(repo));. The banner prints “this WILL create a real ask and notify the principal’s phone,” and later assertsask.principalPagedAt, but the repository is an in-memory fake — no real Ask row is created or persisted, and no database/persistence wiring is exercised. This produces a misleading success message and does not verify the repo-backed dispatch seam (only the send). Fix one of: - Use a real
DrizzleAskRepositoryfrom the domain container/persistence provider in--executeso a genuine Ask row is created and the substrate claim is written; or - Amend
--executewording and checks to stop asserting a “real ask” was created when it was not, and make clear it only sends a real page without persistence. As written, operators will read a stronger guarantee than the code provides, and the path under test is materially different from production (no DB).
Documentation impact
- no-update-needed — Internal plumbing changes: extracted a domain-level paging dispatch and wired reviewer emitters to call it. No new public CLI or MCP surface was added, and existing shared commands retain behavior (asks.create still pages via the extracted dispatch). The user-facing docs likely do not describe internal reviewer paging internals; no docs were updated in this PR and no externally documented behavior appears to have changed.
…s it claimed The reviewer was right and the finding was correctly BLOCKING: `--execute` announced "this WILL create a real ask" while running against `FakeAskRepository`. That is the exact defect class this task is about — a path reporting success for work it did not do — reproduced inside the script written to catch it. Fixed by changing the BEHAVIOR, not the wording: `--execute` now resolves the real persistence provider, builds a `DrizzleAskRepository`, and reads the row BACK before claiming success, asserting the persisted `severity` and `principalPagedAt` rather than trusting the emit's return value. It skips cleanly when no provider or connection is available. This also closes a real gap the reviewer named: with the fake repo the mode exercised neither the insert nor the `severity`/`forceImmediate` columns, so it could not have caught a persistence-layer rejection — one of the two things the smoke exists for. Deploy impact: none — script-only change under services/reviewer/scripts.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
R2 verification: The R1 BLOCKING finding on the smoke script’s --execute path was correctly addressed — it now resolves the real persistence provider and repository, persists a real ask, and verifies principalPagedAt. The paging seam was cleanly extracted to the domain and adopted by both createAsk and the reviewer emitter. Auth-health wiring, provider escalation logic, and paging dispatch behave fail-open and match the spec; tests and the smoke script cover the page actually firing and the remediation link on the rendered page. I found no new critical defects introduced by the fix. Approving this PR.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
A new AskEmitter.emitOperatorIncidentAlert(ctx) method (+ DomainAskEmitter impl) creates an operator-routed (routingTarget: "operator") Ask when the auth-health tracker trips. |
AMENDED at implementation, 2026-09-02 — shipped as
emitOperatorIncidentAlert(ctx).
One method with a discriminatedOperatorIncidentContext(source: "github_auth" \| "provider") rather than this criterion's name plus a near-identical sibling for the
provider half. The rationale is this spec's own 2026-07-31 extension, which says the two
conditions "are both 'the reviewer is structurally unable to review, and only the operator
can fix it'": they produce the same ask shape, the sameseverity: "incident"marker and
the same page, differing only in prose and remediation URL. Two methods would have been
duplication with two places to forget the page. Everything this criterion actually requires
— a new interface method, aDomainAskEmitterimpl,routingTarget: "operator", firing on
the tracker trip — is satisfied; only the name and arity changed. | Met | services/reviewer/src/ask-emitter.ts:120-186 (type OperatorIncidentContext) and :330-343 (AskEmitter includes emitOperatorIncidentAlert). Implementation at :594-671 sets routingTarget: "operator" and severity: "incident". Wiring from tracker at services/reviewer/src/auth-health.ts:177-214 calls emitOperatorIncidentAlert on trip. |
| The globalgithubAuthHealthsingleton is configured with the ask-emitter at reviewer server boot (alongside the existingconfigureGithubAuthHealthAlertSinkcall), reusing the booted domain container (makeContainerAskRepoProvider). | Met | services/reviewer/src/server.ts:1456-1470 — configureGithubAuthHealthAlertSink(alertSink) and configureGithubAuthHealthAskEmitter(new DomainAskEmitter(makeContainerAskRepoProvider(domainServices.container))) at boot. |
| One-shot dedup: at most one Ask per trip (re-armed after recovery), mirroring the circuit-breakeralertedsemantics; fail-open (no container/DB → skip, never crash the tracker callback). | Met | services/reviewer/src/auth-health.ts:146-176 — tracker usestrippedflag to fire once per trip; on missing emitter it logs and continues. services/reviewer/src/ask-emitter.ts:612-640 returns "skipped" when no repo; try/catch prevents throws (fail-open). |
| Unit tests for the new emitter method and the trip→Ask wiring. | Met | services/reviewer/src/ask-emitter.test.ts:72-187 — tests for emitOperatorIncidentAlert including routing, paging, remediation link; also regression asserting rendered page contains remediation URL. PR description lists passing test runs. |
| SC5 — provider-failure detection consumes mt#4881's classified stream; fires only on operator-actionable classes (credits exhaustion), not on self-healing classes. | Met | services/reviewer/src/failure-alert.ts:74-124 defines OPERATOR_ACTIONABLE_CLASSES = {"provider_credits_exhausted"}. Escalation gate shouldEscalateToOperator(...) at :523-535 only pages when class is operator-actionable and threshold crossing; unavailable/timeout are excluded. |
| SC6 — the page actually fires for an Ask emitted by this path withseverity: "incident", or logs a structured reason if not. | Met | packages/domain/src/ask/principal-page-dispatch.ts:86-135 builds production page deps; dispatchPrincipalPage at :160-198 calls pagePrincipalForAsk and logs rate-limit suppression. services/reviewer/src/ask-emitter.ts:659-666 invokes dispatchPrincipalPage after repo.create. Live smoke script services/reviewer/scripts/smoke-operator-incident-page.ts executes dry path and--executepath verifyingprincipalPagedAt(lines ~112-175). |
| SC7 — remediation link included in Ask body for both sources (provider billing URL and GitHub App settings). | Met | services/reviewer/src/ask-emitter.ts:700-742buildOperatorIncidentQuestionleads withRemediation: ${ctx.remediationUrl}; constants at :76-105 include GITHUB_APP_SETTINGS_URL and per-provider URLs. Unit tests assert page body contains the URL: services/reviewer/src/ask-emitter.test.ts:191-223. |
| SC8 — thresholds derived, not picked; reuse existing 60-minute window and auth-health threshold of 3 where answering the same question. | Met | services/reviewer/src/failure-alert.ts:28-73 SUPPRESSION_WINDOW_MS comment derives 60min; :99-135 PROVIDER_ESCALATION_THRESHOLD = 3 with rationale; services/reviewer/src/auth-health.ts:126-143 DEFAULT_AUTH_HEALTH_THRESHOLD parses env with default 3, with justification in header docs. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| @minsky/domain/ask/principal-page-dispatch.dispatchPrincipalPage | function | src/adapters/shared/commands/asks.ts:66 — imported and called within createAsk paging block, services/reviewer/src/ask-emitter.ts:659 — dynamically imported and invoked after repo.create for incident asks | Adopted | Paging seam extracted to domain and used by both createAsk and DomainAskEmitter per spec; avoids duplication of page decision. |
Documentation impact
- no-update-needed — This PR adds internal paging wiring and a smoke script; it does not change public CLI/MCP command surfaces or documented user workflows. The movement of the paging seam into the domain preserves createAsk behavior (asks.ts header notes unchanged behavior). No docs under docs/ were modified, and behavior visible to users (operators) remains consistent except for incident pages actually firing, which is an internal reliability fix.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
R3 verification: The prior BLOCKING finding on the smoke script’s --execute path was addressed — it now uses the real persistence provider/repository and verifies a real page via principalPagedAt. The paging seam was cleanly extracted to the domain (principal-page-dispatch), adopted both by createAsk and the reviewer emitter, and auth-health wiring at server boot is present. Provider escalation logic pages only on the threshold crossing for operator-actionable classes and precedes mt#4881 suppression as required. Tests cover the new paths and a live dry smoke validates channel resolution and page body (including remediation URL). I find no new critical defects introduced by these fixes. Approving this PR.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
A new AskEmitter.emitOperatorIncidentAlert(ctx) method (+ DomainAskEmitter impl) creates an operator-routed (routingTarget: "operator") Ask when the auth-health tracker trips. |
AMENDED at implementation, 2026-09-02 — shipped as
emitOperatorIncidentAlert(ctx)... | Met | services/reviewer/src/ask-emitter.ts:282-451 — addsemitOperatorIncidentAlert(ctx: OperatorIncidentContext)onDomainAskEmitter, setsroutingTarget: "operator",severity: "incident",forceImmediate: true; services/reviewer/src/auth-health.ts:179-212,244-268 — wires the global tracker to callemitOperatorIncidentAlerton trip. |
| The globalgithubAuthHealthsingleton is configured with the ask-emitter at reviewer server boot (alongside the existingconfigureGithubAuthHealthAlertSinkcall), reusing the booted domain container (makeContainerAskRepoProvider). | Met | services/reviewer/src/server.ts:1738-1752 — callsconfigureGithubAuthHealthAskEmitter(new DomainAskEmitter(makeContainerAskRepoProvider(domainServices.container)))immediately after wiring the alert sink; comment cites single-instance convention. |
| One-shot dedup: at most one Ask per trip (re-armed after recovery), mirroring the circuit-breakeralertedsemantics; fail-open (no container/DB → skip, never crash the tracker callback). | Met | services/reviewer/src/auth-health.ts:244-268 — emits insideonTrip(fires once per trip), and wraps inPromise.resolve(...).catch(...)to be fail-open; services/reviewer/src/ask-emitter.ts:320-338 — logs and returns"skipped"when no repo, returns"failed"on error without throwing. |
| Unit tests for the new emitter method and the trip→Ask wiring. | Met | services/reviewer/src/ask-emitter.test.ts:156-333 — testsemitOperatorIncidentAlertincluding page dispatch and body; services/reviewer/src/auth-health.test.ts:201-268 — tests trip triggers exactly one operator incident and degrades cleanly when no emitter wired. |
| SC5 — provider-failure detection consumes mt#4881's classified stream and fires only on operator-actionable classes (provider_credits_exhausted), NOT on self-healing ones. | Met | services/reviewer/src/failure-alert.ts:566-614 — escalation runs before suppression and usesshouldEscalateToOperator; services/reviewer/src/failure-alert.ts:480-506 — pureshouldEscalateToOperatorwith setOPERATOR_ACTIONABLE_CLASSES={"provider_credits_exhausted"}; services/reviewer/src/failure-alert.test.ts:620-739 — asserts credits pages andprovider_unavailable/provider_timeoutdo not. |
| SC6 — the page actually fires: an Ask withseverity: "incident"reachespagePrincipalForAskand records aprincipalPagedAt(or a logged reason otherwise). | Met | packages/domain/src/ask/principal-page-dispatch.ts:129-172 —dispatchPrincipalPagecallspagePrincipalForAskand logs rate-limit; services/reviewer/src/ask-emitter.ts:420-451 — callsdispatchPrincipalPageafterrepo.create; services/reviewer/scripts/smoke-operator-incident-page.ts:133-177 —--executepersists viaDrizzleAskRepositoryand verifiesprincipalPagedAtwas written. |
| SC7 — remediation link included: provider billing URL for credits-exhaustion, GitHub App settings for auth-health, asserted on the notification body. | Met | services/reviewer/src/ask-emitter.ts:76-116,446-451,494-552 — definesGITHUB_APP_SETTINGS_URL,providerBillingUrl, and composes question leading with remediation URL; services/reviewer/src/ask-emitter.test.ts:208-247,270-333 — asserts remediation URL present in ask question and rendered page body; services/reviewer/scripts/smoke-operator-incident-page.ts:104-132 — dry-run asserts the page body includes the remediation URL. |
| SC8 — thresholds derived, not picked; reuse mt#4881's 60-minute window and a count of 3 rather than minting new numbers. | Met | services/reviewer/src/failure-alert.ts:85-116,420-478 — documents and setsPROVIDER_ESCALATION_THRESHOLD = 3, reusingSUPPRESSION_WINDOW_MS = 60min; tests at services/reviewer/src/failure-alert.test.ts:636-739 assert crossing behavior and window minutes carried. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| @minsky/domain/ask/principal-page-dispatch.dispatchPrincipalPage | function | src/adapters/shared/commands/asks.ts:1568 — called after ask routing/persist to perform severity-page dispatch, services/reviewer/src/ask-emitter.ts:446 — invoked (lazy import) after repo.create to ensure paging on reviewer emit path, packages/domain/src/ask/principal-page-dispatch.test.ts:28,69 — unit tests exercising dispatch semantics |
Adopted | Extracted from asks.ts so both createAsk and the reviewer emitter can reach one paging seam per the spec. |
| @minsky/domain/ask/principal-page-dispatch.makeProductionPageDeps | function | — | Missing consumers | Exported helper to construct production PrincipalPageDeps; not used outside the module in this PR. Non-blocking; available for future producers if needed. |
Recommendation: file a follow-up adoption task to wire 1 missing consumer.
Documentation impact
- no-update-needed — This PR adds an internal reviewer paging tier and extracts a domain helper for severity-page dispatch without changing any documented public CLI, API, or user-facing contract. The behavior matches the existing severity transport binding (mt#3595) and wires the reviewer to use it; no docs under docs/ were modified or need updates based on the code changes reviewed. Checked server wiring and emitter semantics; no externally-visible routes/flags changed.
Summary
The reviewer could already record that it was structurally unable to review; it could not tell
anyone. mt#4881 shipped the classified failure stream and an inbox ask. This adds the tier above
it: the two conditions only the operator can clear — sustained GitHub App auth failure, and
sustained provider credit exhaustion — now create a
severity: "incident"+forceImmediateaskand page the principal, with the remediation URL in the notification body.
Originating incident: mt#3433 — ~4h of reviewer downtime the principal found by reading chat
scroll, because remediation (add credits) was operator-only by construction.
The defect this had to work around
Setting
severity: "incident"on the reviewer's emit path would have paged nobody.CreateAskInputcarriesseverityandforceImmediate(packages/domain/src/ask/repository.ts:262,270), so the naive change typechecks and looks complete.pagePrincipalForAsk. A repo-wide grep finds exactly one non-test importer:src/adapters/shared/commands/asks.ts.createAsk, afterpersistRouteOutcome— not inrepo.create. Neithercreateimplementation calls anything paging-related.DomainAskEmittercallsrepo.createdirectly and never callscreateAsk, by design.So the marker would have been written to a row no paging code reads: typechecks, deploys, returns
healthy, inert in production. The mt#2435 shape.
Two findings that shaped the fix: the reviewer's original reason for bypassing
createAskisitself stale (mt#3491 made an explicit
routingTarget: "operator"win over the kind→targetdefault), and mt#3851 already forces operator routing for any
severity: "incident"ask. ButcreateAsklives in the adapter layer, which the reviewer does not import — hence the extractionrather than a call.
Key changes
packages/domain/src/ask/principal-page-dispatch.ts(new) — the production page dispatch, moved out ofasks.tsso both producers reach one seam. A pure relocation: every collaborator (notifyPrincipal,resolvePersistenceProvider,emitSystemEventFromProvider,pagePrincipalForAsk) was already domain-side.createAsk's behaviour is unchanged — it calls the extracted function exactly where it called its private copy.AskEmitter.emitOperatorIncidentAlert— one method, one discriminatedOperatorIncidentContext, for both sources. See the SC1 amendment in the spec for why this replaced the spec's namedemitAuthHealthAlertplus a near-identical sibling.auth-health.ts— pages on trip, deduped by the tracker's existingtrippedflag (no new dedup state). Additive to the mt#2717 alert sink; the three surfaces degrade independently.failure-alert.ts— escalates an operator-actionable class on the threshold crossing, evaluated before mt#4881's per-PR suppression so a single-PR outage still reaches the count that proves it sustained.provider_unavailable/provider_timeoutnever page — they self-heal.services/reviewer/scripts/smoke-operator-incident-page.ts(new) — dry by default;--executepersists a real ask and sends a real page.Thresholds reuse mt#4881's 60-minute window and auth-health's count of 3 rather than minting a
third number for the same judgment (SC8).
Spec criteria
emitOperatorIncidentAlert+DomainAskEmitterimpl; deviation recorded in the specconfigureGithubAuthHealthAskEmittercalled at boot,server.tstrippedflag; fail-open on no-repo / throwing repoReviewFailureClass; no second scan overreview_errorPROVIDER_ESCALATION_THRESHOLDderivation in its docblockTesting
Execution evidence:
AT1/AT2 — auth-health trip with an emitter creates exactly one operator-routed ask; with no
emitter wired it still logs and does not throw. AT3 — see the negative control below. AT4 — a
sustained
provider_credits_exhaustedrun crosses the threshold and emits exactly one incident;a
provider_unavailablerun 8 long emits none. AT5 — the remediation URL is asserted by substring,in the rendered page rather than only the ask.
Negative control — AT3, the dispatch is what pages:
Reverted the
await dispatchPrincipalPage(repo, ask, this.pageDeps)call and re-ran:The other 11 still passed, which is the defect in miniature: the ask is created, correctly marked
severity: "incident",routingTarget: "operator"— and nothing pages.Negative control — the escalation must precede the suppression:
Moved the escalation after mt#4881's duplicate-suppression return and re-ran:
Both files were restored and verified byte-identical before committing.
Live verification
The dry smoke, run against the real production Telegram credentials (read into shell variables
from the linked Railway project, never printed):
configured via envconfirms the resolution order the design depends on —resolvePrincipalChannelreads
TELEGRAM_*before falling back to Pulumi, which a container cannot do.This run found a real defect that every unit test missed.
buildPageMessageexcerpts the ask'squestion at 300 chars; the remediation URL sat at the end of the body and was cut from the
notification the principal actually reads, while remaining present in the ask. SC7 was silently
untrue. Fixed by leading with the remediation (not by widening the shared excerpt, which bounds
every page and is not one caller's to move), plus a regression test that asserts the rendered page
body rather than the ask.
--executeis UNVERIFIED, deliberately, and its risky part is exercised. A full run sends areal notification to the principal's phone, consumes one of the substrate's 3-per-24h page budget
(
principal-page.tsPAGE_RATE_LIMIT_MAX), and — since R1 below — writes a real ask row. That isan outward-facing action I have not taken unilaterally; it needs the operator's go-ahead. Per
§7a's dual-mode rule the branch's own imports must still resolve at runtime (the mt#2760 class),
so that was exercised directly and bounded:
scripts/verify-ask-principal-page.ts(mt#3595) already proves the final transport leg end-to-end.Review rounds
R1 — BLOCKING,
--executeusedFakeAskRepositorywhile announcing a real ask. Correctfinding, and a pointed one: it is this task's own defect class — a path reporting success for work
it did not do — reproduced inside the script written to catch that class. Fixed by changing the
behaviour rather than the wording (
be53c506e):--executenow resolves the real persistenceprovider, builds a
DrizzleAskRepository, and reads the row BACK before claiming success,asserting the persisted
severityandprincipalPagedAtinstead of trusting the emit's returnvalue. It skips cleanly when no provider or connection is available. This also closed the gap the
reviewer named — with the fake repo the mode exercised neither the insert nor the
severity/forceImmediatecolumns, so it could not have caught a persistence-layer rejection.External preconditions
Verified provisioned, no new provisioning required.
TELEGRAM_CHAT_ID,TELEGRAM_BOT_TOKENpresent and
ALERT_SINK_TYPE=telegramon the productionminsky-reviewerRailway project(key names projected, values never rendered). Note
infra/index.ts:338-345declares theseconditionally as a per-stack opt-in — a stack without
reviewer-telegram-chat-iddegrades to alogged
PageDecisionReasonrather than failing loudly, which is correct but means "the page fired"must never be inferred from "the ask was created."
Deploy verification: this PR touches
services/reviewer/src,packages/domainandsrc/adapters/shared/commands— all deploy surface perisDeploySurfaceFile(checked with thepredicate, not from memory). After merge I will run
deployment_wait-for-latestfor the reviewerservice with
notBeforeset to the merge timestamp andexpectCommitShaset to the merge commit,and read
buildIdentityrather than treating SUCCESS alone as proof.Parallel work
Open PR #3412 (mt#4639, IN-REVIEW) touches
ask-emitter.ts,auth-health.tsandserver.ts— amechanical
err.message→getLoggableErrorSummarysweep, established from its actual changed-filelist. It does not touch
failure-alert.ts,principal-page.tsorask/repository.ts, where thesubstantive work here lives. Overlap is additive; a watch is armed on #3412.