# cse-sweep — agent notes

Impl reference for cse-sweep skill. Read before editing scripts. Plugin conventions: `../../../AGENTS.md`. Shared read rule: `../../shared/read-strategy.md`.

## Layout

Scripts under `scripts/` + `scripts/lib/`. Entrypoint `sweep.sh` → orchestrator `cse-sweep.mjs` (plan → seed → fan-out → score → synthesize). Lib modules: `mcp-client`, `cse-tools`, `branch-plan`, `nav-loop`, `scorer`, `synthesize`, `daemon-client`, `realtime_client`. Run `ls scripts/` only with this skill directory as the working directory.

## Inference plane (rtinfer/1)

All model calls go through the always-on rtinfer endpoint served by `cse-toold serve` (third loopback listener, default `127.0.0.1:8765`). Never per-skill realtime/Codex connection. Endpoint = loopback `/v1/infer` (`packages/cse-runtime/src/http/infer.mjs`), off MCP `tools/list` plane so navigators cannot recurse into it.

`lib/daemon-client.mjs` discovers daemon + POSTs `rtinfer/1` requests:
- Discovery: `$CSE_RTINFER_URL` → `~/.cse-rtinfer/endpoint.json`. Candidate used only if `GET /v1/infer/health` returns `{contract:"rtinfer/1", ready:true}`.
- Tiers: `realtime_structured` (navigators/scorer, gpt-realtime-*), `responses_structured` (strict-schema gpt-5.x), `responses_text` (freeform synthesis).
- Daemon = ONLY path. `daemonAskBatch` throws `DaemonUnreachable` when nothing reachable; orchestrator writes `daemon-unreachable.json` to run dir + exits 3. No seed-only degrade.

## Verified interfaces (do not re-derive)

MCP plane (signed Node SEA `cse-toold` launcher with the in-process credential store):
- `POST http://127.0.0.1:9901/mcp/full`, JSON-RPC 2.0. GET → 405 (expected).
- `initialize` returns 200 with NO `Mcp-Session-Id` → plane stateless; `tools/call` works without session. `mcp-client.mjs` does initialize→session→call and degrades to stateless.
- `tools/call` request: `{jsonrpc:"2.0", id:<int>, method:"tools/call", params:{name, arguments}}`. Result: `{result:{content:[{type:"text",text}], structuredContent?, isError?}}`.
- Tool names include `jira_search`, `jira_get_issue`, `confluence_search`, `confluence_get_page`, `slack_search`, `list_communications`, `get_communications`, `get_salesforce_account`, `kg_search`, `kg_account_signals`, `kg_traverse`, `granola_list_meetings`, `granola_get_meetings`, `google_calendar_events`, `semantic_context_search`. Fetch live input schemas from `tools/list` — never hardcode arg shapes.

Degradation: `list_communications`/`kg_search` need kepler_full configured. `cse-tools.mjs` surfaces `{ok:false, error}` per-call so navigator loop degrades per-branch. Slack xoxe reads have no client-side pacing; the shared runner handles real 429 `Retry-After` responses.

rtinfer endpoint (cse-toold serve):
- `POST <base_url>/v1/infer`, body `{contract:"rtinfer/1", tier, system, user, schema?, schema_name?, model?}`. Loopback-gated.
- Success: `{contract, ok:true, tier, object|text, model}`. Error: `{contract, ok:false, error:{code, message, retryable}}` + matching HTTP status.
- `GET /v1/infer/health` → `{contract:"rtinfer/1", ready, provider, tiers}`. `ready` reflects server-side Codex auth reachability.
- OpenAI bridge (restored from retired Rust rtinferd): `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/responses`. `stream:true` returns `text/event-stream` (assembled-after-complete SSE). `gpt-realtime-*` → warm pool; `gpt-5.*` (incl. luna/terra) → Codex responses pool. Tools only on realtime models. cse-sweep itself stays on `/v1/infer`.
- cse-sweep sends gpt-5.6-terra by default to `responses_text` tier — per-sweep selection, not shared daemon fallback change. 1M context but 2x billing past 272K → map-reduce above ~250K packed (`--max-pack-tokens` default 230000).
- Codex OAuth slots live in the SE-encrypted cred store (bootstrapped once from `~/.codex/auth.json`). Skill carries no Codex tokens.

## Conventions

- Synthesis modes: `--mode evidence` (default, factual report) | `--mode strategy` (business commentary: thesis, cross-customer patterns, recommended play, optional draft copy; optional `--audience`). Both prompts live in `lib/synthesize.mjs` (`SYNTH_INSTRUCTIONS` / `STRATEGY_INSTRUCTIONS`, plus topic variants, selected by `instructionsFor(mode, audience, scopeKind)`). Strategy applies only at the final/reduce call; map (group) phase keeps `GROUP_INSTRUCTIONS` factual in both modes. Citation discipline identical. PRDs: `docs/research/cse-sweep-strategy-mode-prd.md`, `docs/research/cse-sweep-topic-scope-prd.md`.
- Scope kind (`person|account|ticket|topic`) is inferred from flags, recorded in `branches.json` as `scopeKind`, printed on `--dry-run`, and forwarded to synthesis as `--scope`.
- Topic scope: bare `--topic` only. `TOPIC_QUERY_TOOLS` = semantic (`allow_global_discovery:true`)/kg/jira/confluence/slack/granola/calendar. Never emit empty `customer_names`/`customers`/`account_ids`. Exclude `list_communications` (no topical filter). Ticket-only also skips scope-less semantic + list_comms in the account planner. Topic navigators use `supportedTools()` (full read-only; no deep-dive owns Slack here).
- Script path stays `/mcp/full`. Named `tools/call` requests here are the script contract (same interactive default plane: named tools plus the five facades).
- Business brief (`lib/business-brief.mjs`): account scope only, max 3 accounts, runs in the same `Promise.all` wave as the navigator fan-out, calls read-only `web_business_brief` (daemon-owned hosted `web_search`; see `docs/flows/mcp/web-business-brief.md`). Public web evidence NEVER enters shards, scorer, or the synthesis pack — it is written to `business-brief.json` and prepended to `answer.md` as `## Business brief`. Every rendered fact links its source; uncited facts are dropped. Non-fatal by contract. Kill `CSE_SWEEP_BUSINESS_BRIEF=0`.
- Routine known person/account/ticket/operator read uses direct `context_assemble`. Sweep only for open-ended gather or explicit deep narrative.
- Read-only. No external writes. Run dir only (default `~/.cse-tools/sweep-runs/<ISO>/`, override `CSE_SWEEP_RUNS_DIR`, or `--out`).
- Person mode: `--emails` truth. `--people` optional paired labels. Max five. No account/ticket mix. Seed: orchestrator-only `context_assemble`, participant Kepler, active-workspace Granola inventory/detail, person Slack. Navigators cannot call context engine; host limits person tools/args.
- Any non-smoke account count, even one, uses `seedMultiAccount` + per-account `slackDeepDive`. Smoke uses `seedOne`. Person/ticket/topic uses `seedAll`.
- Slack deep-dive: one broad question, then keyword fan-out. Question-only loses coverage. Deep-dive owns account Slack: seed + navigator paths MUST exclude duplicate `slack_search`. Independent xoxe reads run concurrently under the per-account hard call budget. See `docs/slack/rts-search.md`.
- No emojis, no coaching voice in any rendered string.
- Keep env names `EXPLORE_RT_NAV_MODEL`, `EXPLORE_RT_SYNTH_MODEL`, `EXPLORE_RT_NAV_COUNT` (parity with explore).
- `CSE_RT_REASONING_STEER`: unset prefixes navigator + scorer prompts with `Respond quickly, do not reason.`; empty disables prefix; nonempty overrides.
- `CSE_RT_NAV_REASONING_EFFORT`: navigator-only; unset or empty omits explicit effort, nonempty forwards it.
- Vendored files carry `// Vendored from ...` header noting origin + adaptation.

## Live check

Read only. Never print tokens:

```sh
base="$(node -p 'process.env.CSE_RTINFER_URL || require(process.env.HOME + "/.cse-rtinfer/endpoint.json").base_url')" && curl -fsS "$base/v1/infer/health"
```

Need `contract: "rtinfer/1"` and `ready: true`.
