sphinx-understand 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ # Configuration, models, and cost
2
+
3
+ This page is the reference for reviewer models, usage limits, repository policy, and file exclusions. For the first-run path, see the [README](../README.md).
4
+
5
+ - [Settings locations](#settings-locations)
6
+ - [Reviewer models](#reviewer-models)
7
+ - [Routing and question counts](#routing-and-question-counts)
8
+ - [Model calls, tokens, and limits](#model-calls-tokens-and-limits)
9
+ - [Repository policy](#repository-policy)
10
+ - [Project context](#project-context)
11
+ - [Files Sphinx never reads](#files-sphinx-never-reads)
12
+ - [Background preparation](#background-preparation)
13
+
14
+ ## Settings locations
15
+
16
+ | What | Where |
17
+ | --- | --- |
18
+ | Machine-wide settings | `~/.config/sphinx/config.json` |
19
+ | OpenRouter key and OpenAI-compatible endpoint | `~/.config/sphinx/credentials.json` (mode `0600`) |
20
+ | Machine-wide ignore patterns | `~/.config/sphinx/ignore` |
21
+ | Skip and yolo log | `~/.config/sphinx/overrides.jsonl` |
22
+ | Repository policy | `.sphinxconfig` (legacy `.sphinx.json` is read when `.sphinxconfig` is absent) |
23
+ | Repository exclusions | `.sphinxignore` |
24
+ | Project context | `.sphinxcontext.md` |
25
+
26
+ `SPHINX_HOME` relocates all machine-wide settings, credentials, and assessment state. `sphinx config` shows effective settings and paths. A complete global example using the paid Haiku preset is in [examples/config.json](../examples/config.json).
27
+
28
+ Model routes, credentials, and usage limits are machine-wide only. A repository cannot redirect review to another provider, supply credentials, or raise your limits.
29
+
30
+ ## Reviewer models
31
+
32
+ Sphinx picks one of three saved routes (small, medium, large) for each change. Presets fill all three; you can change them at any time without reinstalling:
33
+
34
+ ```sh
35
+ sphinx config --preset claude-economy
36
+ sphinx config --preset openrouter-value --allow-paid
37
+ sphinx config --small claude:haiku --medium claude:haiku --large claude:sonnet
38
+ ```
39
+
40
+ | Preset | Small and medium | Large | Usage source |
41
+ | --- | --- | --- | --- |
42
+ | `codex-economy` | `codex:gpt-6-luna` | `codex:gpt-6-luna` | Your Codex CLI account |
43
+ | `claude-economy` | `claude:haiku` | `claude:haiku` | Your Claude CLI account |
44
+ | `openrouter-value` (experimental) | `openrouter:anthropic/claude-haiku-5.5` | `openrouter:anthropic/claude-haiku-5.5` | Paid OpenRouter credits |
45
+ | `openrouter-deepseek` (experimental) | `openrouter:deepseek/deepseek-v3.2` | `openrouter:deepseek/deepseek-v3.2` | Paid OpenRouter credits |
46
+ | `openrouter-free` | `openrouter:openrouter/free` | `openrouter:openrouter/free` | OpenRouter free availability and limits |
47
+
48
+ Unattended installation needs an explicit preset or route; Sphinx does not silently choose a free model:
49
+
50
+ ```sh
51
+ sphinx install --yes --preset codex-economy
52
+ sphinx install --yes --preset claude-economy
53
+ sphinx install --yes --preset openrouter-value --allow-paid
54
+ ```
55
+
56
+ Reinstalling keeps saved routes unless you choose a replacement. Updating Sphinx does not migrate saved routes. Changing a route invalidates prepared questions and completion records made under the previous route.
57
+
58
+ ### Codex and Claude routes
59
+
60
+ These use your signed-in Codex or Claude CLI and consume that account's allowance. You need a recent CLI with access to the selected model; see [Codex models](https://learn.chatgpt.com/docs/models) and [Claude model configuration](https://code.claude.com/docs/en/model-config). The economy presets keep the same economical model for all tiers; investigation effort and question count still scale with change size. The Claude `haiku` alias follows the available Haiku model. Neither preset upgrades to a more expensive model.
61
+
62
+ **These routes bill your signed-in plan by default.** Sphinx removes `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `CODEX_API_KEY`, and similar variables from the reviewer CLI's environment, so a key in your shell cannot silently switch reviews to API credits. If the CLI is not signed in with a Claude or ChatGPT plan, the check stops with instructions. To let the CLIs use API keys instead, run `sphinx config --cli-billing api`.
63
+
64
+ Each call is a fresh, isolated CLI invocation used as a model transport underneath the embedded [Pi SDK](https://pi.dev/docs/latest/sdk) reviewer. It does not reuse your coding conversation. See [architecture](architecture.md#independent-investigation-and-compaction).
65
+
66
+ Pi, Cursor, and OpenCode integrations present checkpoints but do not supply a reviewer model. Their users pick a Codex/Claude CLI route, OpenRouter, or an OpenAI-compatible endpoint.
67
+
68
+ ### OpenRouter routes
69
+
70
+ **Every OpenRouter route requires an API key, including free models.** The installer accepts an existing `OPENROUTER_API_KEY` or prompts for one with hidden input; `sphinx auth` replaces it later. Create a key in [OpenRouter's key settings](https://openrouter.ai/settings/keys). The key is stored in a user-only local file, not an OS keychain, and never in project configuration.
71
+
72
+ Filtered evidence goes to OpenRouter and its selected upstream provider, billed separately from your coding-agent plan. Paid routes need OpenRouter credits.
73
+
74
+ - **Paid consent.** Guided setup labels paid choices. Noninteractive commands need `--allow-paid` or an existing `allowPaid: true` setting. Free routes send zero-price provider limits even when another route permits paid models. There are no automatic paid fallbacks.
75
+ - **Free router.** `openrouter-free` is best-effort. Its model selection, rate limits, and tool support can interrupt a check; see [OpenRouter limits](https://openrouter.ai/docs/api_reference/limits). Sphinx does not switch providers or upgrade models after a failure.
76
+ - **Endpoint compatibility.** Requests need endpoint support for tools and output parameters. Capability metadata is a compatibility check, not a guarantee; unusable output leaves the check pending. See OpenRouter [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection) and [structured outputs](https://openrouter.ai/docs/guides/features/structured-outputs).
77
+
78
+ ### OpenAI-compatible endpoints
79
+
80
+ Any OpenAI chat-completions-compatible endpoint (OpenAI, Groq, Together, vLLM, Ollama, LM Studio, and others) can run reviews. It must support tool calls and JSON-schema structured output.
81
+
82
+ ```sh
83
+ sphinx auth --provider openai --base-url https://api.openai.com/v1 # prompts for the key (hidden)
84
+ sphinx config --provider openai --model gpt-5-mini # or per size: --large openai:<model>
85
+ ```
86
+
87
+ The key is stored next to its URL and only ever sent there. Plain `http://` is allowed only for localhost, where a key is optional. For scripts, set `SPHINX_OPENAI_BASE_URL`, plus `SPHINX_OPENAI_API_KEY` unless the endpoint is localhost. Sphinx never reads `OPENAI_API_KEY`.
88
+
89
+ ### Reference pricing and quality
90
+
91
+ The paid value preset uses Haiku 5.5 with an 8,192-token output ceiling, including reasoning. Reference prices checked **October 15, 2026** start at $0.10 input / $0.50 output per million tokens below 100,000 input tokens; the higher context tier is $0.50 / $2.50. Rates vary by endpoint and over time; see [Haiku endpoints](https://openrouter.ai/api/v1/models/anthropic/claude-haiku-5.5/endpoints). At the lower rates, 30,000 input plus 2,000 output tokens costs about $0.004. A whole check makes several requests with repeated context.
92
+
93
+ The [evaluation results](../eval/RESULTS.md) record measured question quality, grading failures, and corrections across Haiku, Qwen, DeepSeek, and GLM on synthetic changes. They are a limited development benchmark, not a reliability guarantee. The Haiku preset is experimental because it rejected a sufficient qualitative answer in a focused test; DeepSeek is an experimental open-weight preset. Price and tool support alone do not establish question quality.
94
+
95
+ ## Routing and question counts
96
+
97
+ Routing is deterministic and makes no model request. It counts **added plus deleted lines**, changed files, and consequential path terms. Ignored files do not count.
98
+
99
+ | Tier | Default rule |
100
+ | --- | --- |
101
+ | Small | At most 80 changed lines and 3 files |
102
+ | Medium | Between the small and large thresholds |
103
+ | Large | At least 400 changed lines, 12 files, or a consequential path |
104
+
105
+ Consequential paths contain `auth`, `permission`, `migration`, `billing`, `payment`, `crypto`, `security`, or `terraform`. A matching path makes even a tiny patch large. These are configurable heuristics, not a semantic risk classification.
106
+
107
+ The tier and your chosen depth set the maximum number of initial questions:
108
+
109
+ | Change tier | Relaxed | Standard | Thorough |
110
+ | --- | --- | --- | --- |
111
+ | Small | 1 | 1 | 1 |
112
+ | Medium | Up to 2 | Up to 2 | Up to 3 |
113
+ | Large | Up to 2 | Up to 3 | Up to 5 |
114
+
115
+ - `relaxed`: observable behavior and the most consequential edge case.
116
+ - `standard`: behavior, failure modes, and relevant system interactions.
117
+ - `thorough`: also supported rollout, rollback, and compatibility assumptions.
118
+
119
+ These are ceilings, not quotas: the reviewer asks fewer questions when the evidence supports fewer distinct behaviors, and never pads. A wrong answer adds a replacement question for the same concept, so a check can ask more than the initial count. Changing the model does not change the question count.
120
+
121
+ ## Model calls, tokens, and limits
122
+
123
+ **Model calls have no default cap** (`maxModelCalls: null`, `agentMaxTurns: null`). Every request is counted: background investigation, compaction, the question audit, grading, replacement audits, and failed requests. The questionnaire shows calls and tokens as the check runs, plus cost when the provider reports it.
124
+
125
+ | Step | Typical requests |
126
+ | --- | --- |
127
+ | Investigation for a small / medium / large change | Aims for 5 / 7 / 8, including the question audit. These are planning targets, not stops. |
128
+ | Each answer, learning request, or dispute | 1 |
129
+ | A proposed replacement question | 1 more, for an independent source audit |
130
+ | Status, help, config, pending, or pushing an already approved change | 0 |
131
+
132
+ Set or clear an optional cap. This changes no routes and makes no model request:
133
+
134
+ ```sh
135
+ sphinx config --max-model-calls 30 # 1–100
136
+ sphinx config --max-model-calls unlimited # `none` is an alias
137
+ ```
138
+
139
+ - Existing numeric caps stay in effect until changed. A check adopts an edited cap when resumed, preserving questions, progress, and counted calls. Lowering a cap below current usage blocks further calls.
140
+ - An explicit cap reserves calls for grading and the final audit; a lone remaining request cannot start a question synthesis that would lack its audit.
141
+ - `agentMaxTurns` (advanced) caps investigation, compaction, question generation, and auditing for one review attempt.
142
+
143
+ Other limits always apply:
144
+
145
+ | Setting | Default | Bounds |
146
+ | --- | --- | --- |
147
+ | `timeoutSeconds` | 180 | The whole investigation. Unlimited calls do not mean unlimited time. |
148
+ | `maxContextBytes` | 100,000 | Serialized review context (prompts, tools, schemas), not the whole diff. Pi compacts before this; context that still does not fit is rejected before a model call. |
149
+ | `toolResultBytes` | 16,000 | Each tool page. Pages shrink to leave room for batched reads. |
150
+ | `maxOutputTokens` | 4,096 (8,192 in `openrouter-value`) | Every request, 512–16,384. Set with `sphinx config --max-output-tokens 8192`. |
151
+
152
+ OpenRouter counts hidden reasoning within `maxOutputTokens`; a higher ceiling can prevent truncation and permits more billed output. Codex and Claude CLIs get concise-output instructions and local timeout and output-byte limits, but their interfaces have no equivalent hard token cap. Small and medium changes request low reasoning effort; large changes request medium effort where the CLI supports it.
153
+
154
+ **Retries.** Provider failures never retry automatically and never switch providers. Cache warming is disabled. Without a call cap, the reviewer stops after three consecutive invalid responses. Correctable tool or question-validation errors are returned to Pi as feedback within any explicit cap. If a review fails, the questionnaire offers to continue from saved work; Pause is the default. From a terminal, `sphinx check --id <id>` resumes saved work, and `--retry-review` gives a failed, questionless review one further attempt (with an explicit cap, up to `maxModelCalls` additional requests). Setup, credential, stale-code, and evidence-loading failures must be fixed first.
155
+
156
+ Call caps bound requests and byte limits bound evidence. Neither is a dollar or subscription ceiling. **Set provider-side spending limits for paid accounts.**
157
+
158
+ ## Repository policy
159
+
160
+ `sphinx init` creates `.sphinxconfig`, `.sphinxignore`, and `.sphinxcontext.md` when they are absent and never overwrites them. Edit the files directly afterwards. Commit them to share policy with collaborators. Start from `sphinx init` or [.sphinxconfig.example](../.sphinxconfig.example):
161
+
162
+ ```json
163
+ {
164
+ "version": 1,
165
+ "events": ["pre-push"],
166
+ "branches": ["main", "release/*"],
167
+ "level": "standard",
168
+ "exclude": ["vendor/**"]
169
+ }
170
+ ```
171
+
172
+ | Key | Meaning |
173
+ | --- | --- |
174
+ | `enabled` | `false` disables Sphinx for this repository |
175
+ | `events` | `pre-push`, `pre-commit`, or both |
176
+ | `branches` | Push policies match the **destination** branch; commit policies match the current local branch |
177
+ | `remotes` | Remote name patterns for pushes; default `*` |
178
+ | `level` | `relaxed`, `standard`, or `thorough` |
179
+ | `exclude` | Extra ignore patterns, added to machine-wide ones |
180
+ | `backgroundPreparation` | `false` opts this repository out |
181
+
182
+ Branch defaults come from machine-wide settings (initially `main` and `master`), not from the branch you happen to have checked out. Without prompts:
183
+
184
+ ```sh
185
+ sphinx init --yes --branches main --events pre-push --level standard
186
+ ```
187
+
188
+ If both `.sphinxconfig` and legacy `.sphinx.json` exist, `.sphinxconfig` wins and they are not merged; `init` copies a legacy policy into `.sphinxconfig` and leaves the original.
189
+
190
+ **Commit checkpoints.** Enable `pre-commit`. To run one by hand, `sphinx check --staged` assesses the index, including partial staging.
191
+
192
+ **Manual push checks.** `sphinx check --remote origin --branch main` selects the destination; otherwise Sphinx uses the upstream. Multiple destination refs are assessed separately. Tag-only pushes and deletions do not trigger checks in v0.1.
193
+
194
+ ## Project context
195
+
196
+ `.sphinxcontext.md` describes what the project does, who uses it, important workflows, and architecture boundaries. It is included in every reviewer, compaction, and grading request.
197
+
198
+ ```sh
199
+ sphinx init --yes --context 'A booking service for shared equipment. Reservations must never overlap.'
200
+ ```
201
+
202
+ - Keep it under 16,000 bytes and free of secrets. It respects `.sphinxignore`.
203
+ - The reviewer checks its claims against code. It cannot override Sphinx policy or authorize reading ignored files.
204
+ - Without context, the reviewer establishes purpose from documentation and source rather than inventing requirements.
205
+ - Changing it invalidates prepared questions and completion records.
206
+
207
+ ## Files Sphinx never reads
208
+
209
+ Built-in exclusions apply recursively and cannot be undone by project negations:
210
+
211
+ - `.env` and `.env.*`, including `.env.example`.
212
+ - Private key and keystore names: `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `id_rsa`, `id_ed25519`, and related SSH names.
213
+ - Credential files: `.netrc`, `.npmrc`, `.pypirc`, `*credentials*.json`, `service-account*.json`, `secrets.json`, `secrets.yaml`, `secrets.yml`.
214
+
215
+ Add project exclusions to `.sphinxignore`:
216
+
217
+ ```gitignore
218
+ # Project-specific private or generated files
219
+ internal/customer-exports/
220
+ /config/production.json
221
+ *.generated.ts
222
+ dist/
223
+ ```
224
+
225
+ Supported syntax is `*`, `**`, `?`, root `/`, directory suffix `/`, `#` comments, and local `!` negations; escape a leading `#` or `!` with `\`. This is a small gitignore-like parser, not the full Git pattern language. Project rules cannot undo machine-wide (`~/.config/sphinx/ignore`) or built-in exclusions. Ignore-policy symlinks are rejected. `.gitignore` is not applied to tracked files.
226
+
227
+ **Filtering happens before patch, line-count, and content retrieval, including searches of unchanged files.** Ignored files' contents are not read, saved as evidence, or sent to a model. Git may list their names and object IDs locally to identify changes. A change that touches only ignored files needs no assessment.
228
+
229
+ Ignore rules match paths. They do not detect a secret inside an included source file. Add sensitive paths before your first assessment.
230
+
231
+ Evidence comes from Git objects, not your working files: pre-commit reads the staged index, pre-push reads commits. `.sphinxcontext.md` is read from the working directory and its hash is bound to the assessment. Snapshot identity covers the full commit or index, so editing an ignored file alongside an assessed change can still invalidate the earlier completion.
232
+
233
+ ## Background preparation
234
+
235
+ After commits, merges, rewrites, and checkouts, Sphinx schedules a detached reviewer for the expected destination so questions are ready before you push. It uses local remote-tracking refs and never fetches. It never opens a questionnaire or approves a push.
236
+
237
+ - `sphinx status` shows preparation progress; `sphinx prepare` schedules it explicitly; `sphinx prepare --retry` retries a failed job (failures never retry on their own).
238
+ - Preparation and the push check share one assessment, lock, question set, and usage count. A change to source, destination, policy, ignore rules, or context discards prepared work.
239
+ - Set `backgroundPreparation: false` in `config.json` or `.sphinxconfig` to opt out. A repository cannot re-enable preparation disabled machine-wide.
240
+ - While yolo is on, preparation is paused: no new model request starts, although one already in progress may finish.
241
+ - A first push, a branch without local tracking history, or a push to an unexpected destination needs a cold review at push time. Pre-commit checks always review on demand. Run `git fetch` if remote history is missing.
@@ -0,0 +1,135 @@
1
+ # Integrations and troubleshooting
2
+
3
+ The Git hook is the checkpoint. It works on its own, including for pushes from a terminal or GUI client. Coding-agent integrations add two things: guidance telling the agent to wait for a blocked Git command, and fallback forms for checks that could not open in the browser. For the first-run path, see the [README](../README.md).
4
+
5
+ - [Supported clients](#supported-clients)
6
+ - [Choosing and opening clients](#choosing-and-opening-clients)
7
+ - [Per-client notes](#per-client-notes)
8
+ - [How a checkpoint reaches you](#how-a-checkpoint-reaches-you)
9
+ - [Remote machines, SSH, and CI](#remote-machines-ssh-and-ci)
10
+ - [Git hooks and existing hooks](#git-hooks-and-existing-hooks)
11
+ - [Manual JSON protocol](#manual-json-protocol)
12
+ - [Troubleshooting](#troubleshooting)
13
+ - [Uninstall](#uninstall)
14
+
15
+ ## Supported clients
16
+
17
+ | Client | Checkpoint | Fallback | Installed integration |
18
+ | --- | --- | --- | --- |
19
+ | Codex | Local browser questionnaire | Native MCP form | MCP server and lifecycle hooks |
20
+ | Claude Code | Local browser questionnaire | Native MCP form | MCP server and lifecycle hooks |
21
+ | Pi 1.2+ | Local browser questionnaire | Native dialogs | Local Pi extension |
22
+ | Cursor 1.7+ | Local browser questionnaire | Browser, after Git exits | Local user hooks |
23
+ | OpenCode 1.x | Local browser questionnaire | Browser, after Git exits | Local OpenCode plugin |
24
+
25
+ Client integrations are separate from the reviewer model. Pi, Cursor, and OpenCode do not supply reviewer authentication; choose a Codex/Claude CLI route, OpenRouter, or an OpenAI-compatible endpoint (see [configuration](configuration.md#reviewer-models)).
26
+
27
+ ## Choosing and opening clients
28
+
29
+ `--agents auto` (the default) detects clients by executable, configuration directory, or an existing Sphinx installation. Guided setup shows checkboxes with **All supported agents** first and detected clients preselected: arrow keys move, Space toggles, Enter confirms. Clear every box to skip agent setup; previously connected integrations stay installed.
30
+
31
+ ```sh
32
+ sphinx install --agents codex,claude,pi,cursor,opencode # explicit subset
33
+ sphinx install --agents none # Git only
34
+ ```
35
+
36
+ Run `sphinx install` again after installing a new supported client; Sphinx does not discover new clients on its own.
37
+
38
+ Interactive installation opens the selected clients afterwards: fresh Codex, Claude Code, Pi, and OpenCode CLI sessions, or a new Cursor window. Opening a client sends no prompt and makes no Sphinx model request. Existing client processes are not restarted.
39
+
40
+ ```sh
41
+ sphinx install --no-launch # install without opening clients
42
+ sphinx install --yes --preset codex-economy --launch # unattended install, then open
43
+ sphinx activate # open configured clients later
44
+ sphinx activate --agents codex,claude
45
+ ```
46
+
47
+ `activate` does not reinstall or change settings; `--json` returns launch results. If a client is missing or no terminal or editor can be opened, Sphinx prints the command to run yourself.
48
+
49
+ Opening a client does not verify MCP connectivity or hook trust. Sphinx never bypasses a client's trust review. Check what Sphinx can see with `sphinx doctor`.
50
+
51
+ ## Per-client notes
52
+
53
+ **Codex.** Open `/hooks` and trust the Sphinx hooks; `sphinx doctor` cannot verify this. Codex approval policies can cancel native forms: in Codex 0.162.1, approval policy `never` cancels them unless the host enables full-access form input for the user thread. A cancelled form leaves the check pending; finish it with `sphinx check` in a terminal. Sphinx does not change your approval policy.
54
+
55
+ **Claude Code.** Start a new session to load the hooks. Confirm the `sphinx` server is connected in `/mcp`.
56
+
57
+ **Pi.** Start a new session, or run `/reload` in an existing one. Native fallback dialogs need an interactive session or a dialog-capable RPC client; noninteractive modes leave the check pending for `sphinx check`.
58
+
59
+ **Cursor.** Start a new Agent conversation in the opened window. Activation flags versions older than 1.7 for an update. The installed user hooks cover local sessions, not Cursor cloud agents.
60
+
61
+ **OpenCode.** Start a new session. The plugin is installed in OpenCode's configuration directory (honoring `OPENCODE_CONFIG_DIR` or `XDG_CONFIG_HOME`).
62
+
63
+ Each client controls how its own forms and dialogs look, and may keep its own interaction logs.
64
+
65
+ ## How a checkpoint reaches you
66
+
67
+ 1. **Browser (normal).** The blocked Git hook serves a questionnaire on `127.0.0.1` and prints its URL. On macOS it opens in a background browser tab without activating Terminal or moving your coding window; elsewhere, open the printed URL. One page handles every checkpoint in a Git operation. Git continues when you pass.
68
+ 2. **Native fallback.** If the browser page cannot be presented, Git stops and the check is queued. Codex and Claude Code show an MCP form after the shell command exits; Pi shows extension dialogs; Cursor and OpenCode point you to the browser page. Because Git has already exited, passing a fallback check does not resume it; run the Git command again once.
69
+ 3. **Terminal.** `sphinx check` runs the same check in your terminal at any time. It supports `/learn`, `/dispute <reason>`, and `/quit` (save and leave pending).
70
+
71
+ All three share one assessment per change. **Your answers go directly to Sphinx; the coding model never sees questions, answers, or feedback.** It receives only status, assessment IDs, progress, and usage counts. It does not start the interview or relay answers, and there is no extra coding-model turn per answer. A paused or dismissed check does not reopen on unrelated shell commands; retrying the blocked Git command queues it again. The integration follows checks across `cd` and `git -C`.
72
+
73
+ Agent guidance tells the coding agent not to start a duplicate Git command while one is waiting and not to answer for you. The host may still cancel a long-running shell command before you finish; in that case, complete the check and retry once.
74
+
75
+ ## Remote machines, SSH, and CI
76
+
77
+ The installation covers your local machine. Remote machines, containers, and hosted coding agents need their own installation.
78
+
79
+ Sphinx opens the browser automatically only on macOS. On Linux, WSL, and over SSH it prints the loopback URL instead. From a remote session, forward the same port number (for example `ssh -L <port>:127.0.0.1:<port> host`) and open the printed URL unchanged in your local browser; the page rejects other hosts and ports. Or run `sphinx check` on the remote machine. A remote connection may time out sooner than Sphinx's 25-minute wait.
80
+
81
+ CI never starts a questionnaire. The check stays pending and Git stops.
82
+
83
+ ## Git hooks and existing hooks
84
+
85
+ `sphinx install` sets a machine-wide Git `core.hooksPath`, which covers existing and new repositories that use that Git configuration. Existing repository hooks still run first, with their arguments and input, and their rejection is preserved. Hooks with streaming protocols are forwarded directly. Reinstallation is idempotent, and agent settings are merged rather than replaced.
86
+
87
+ A repository that sets its own `core.hooksPath` needs a compatibility step. `sphinx doctor` reports this:
88
+
89
+ ```sh
90
+ cd your-repo
91
+ sphinx install --repo
92
+ ```
93
+
94
+ Besides the checkpoint hooks, `post-commit`, `post-merge`, `post-rewrite`, and `post-checkout` schedule [background preparation](configuration.md#background-preparation).
95
+
96
+ ## Manual JSON protocol
97
+
98
+ For integrations and debugging, the CLI returns questions and feedback as JSON. It never returns answer keys. Codex and Claude use `sphinx mcp` internally, and Pi calls the same controller from its extension; none of them asks the coding model to run these commands.
99
+
100
+ ```sh
101
+ sphinx pending --json
102
+ sphinx check --json
103
+ sphinx answer <assessment-id> --text 'Your answer and reason' --json
104
+ sphinx explain <assessment-id> --json
105
+ sphinx dispute <assessment-id> --text 'Why the premise is wrong' --json
106
+ ```
107
+
108
+ These are for a person or a test harness. A coding agent must not answer on your behalf.
109
+
110
+ `help`, `status`, `pending`, and `config` never call a model. `--json` output contains no banners or ANSI styling; color is off when output is redirected, and `NO_COLOR=1` disables it.
111
+
112
+ ## Troubleshooting
113
+
114
+ | Symptom | What to do |
115
+ | --- | --- |
116
+ | Git is waiting and no page opened | Open the `Sphinx questionnaire:` URL that Git printed. |
117
+ | You closed the tab | Git keeps waiting. Reopen the same URL until the 25-minute deadline. |
118
+ | You paused, the deadline passed, or the host cancelled the shell | Progress is saved. Run the Git command once more, or `sphinx check` first. |
119
+ | A native form was cancelled or never appeared | Run `sphinx check` in a terminal. In Codex, see the approval policy note above. |
120
+ | The review failed before questions appeared | The page offers to continue from saved work. From a terminal: `sphinx check --id <id>`, or `--retry-review` for a failed, questionless review. See [limits](configuration.md#model-calls-tokens-and-limits). |
121
+ | The check keeps coming back after you passed | Something changed: code, destination base, policy, ignore rules, context, or reviewer route. Completion also expires after 24 hours. |
122
+ | Hooks do not run in one repository | `sphinx doctor`, then `sphinx install --repo` if it reports a hooks override. |
123
+ | Missing remote history | `git fetch`, then push again. |
124
+
125
+ Never start a second copy of a Git command that is still waiting.
126
+
127
+ ## Uninstall
128
+
129
+ ```sh
130
+ sphinx uninstall --repo # remove an explicit repository integration, if present
131
+ sphinx uninstall # remove machine-wide Git and agent integrations
132
+ npm uninstall -g sphinx-understand
133
+ ```
134
+
135
+ Uninstall restores the previous hook configuration when Sphinx still owns it and removes only its own unchanged agent handlers and MCP registration. Later edits are preserved and reported. Machine-wide uninstall leaves explicit repository installations in place, with a warning; remove those first. Configuration, credentials, and assessment state stay in `~/.config/sphinx` for you to keep or delete.
package/docs/logo.svg ADDED
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="#16130f">
2
+ <style>@media (prefers-color-scheme: dark){svg{fill:#ede7dd}}</style><mask id="m"><rect width="64" height="64" fill="#fff"/><path d="M24 22h16v14c0 6-3.6 10-8 10s-8-4-8-10z" fill="#000"/><rect x="26.5" y="44" width="11" height="12" fill="#000"/><rect x="17" y="18.2" width="30" height="1.8" fill="#000"/><g fill="#000"><rect y="26" width="21" height="2.2"/><rect x="43" y="26" width="21" height="2.2"/><rect y="32" width="21" height="2.2"/><rect x="43" y="32" width="21" height="2.2"/><rect y="38" width="21" height="2.2"/><rect x="43" y="38" width="21" height="2.2"/><rect y="44" width="21" height="2.2"/><rect x="43" y="44" width="21" height="2.2"/><rect y="50" width="21" height="2.2"/><rect x="43" y="50" width="21" height="2.2"/></g></mask>
3
+ <path mask="url(#m)" d="M24 5h16c5 0 8.5 3.5 10 9l6 16-4 2v28H12V32l-4-2 6-16c1.5-5.5 5-9 10-9z"/><rect x="26.6" y="27.4" width="4.2" height="2.2" rx="1.1"/><rect x="33.2" y="27.4" width="4.2" height="2.2" rx="1.1"/><rect x="29.2" y="38.6" width="5.6" height="1.8" rx=".9"/><path d="M29.4 47.2h5.2l-.6 9.4h-4z"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphinx-understand",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Outsource your thinking, not your understanding. A comprehension checkpoint for agent-assisted development.",
5
5
  "homepage": "https://github.com/parthh01/sphinx#readme",
6
6
  "repository": {
@@ -9,6 +9,8 @@ import { prepareInteractiveCheck } from './check-recovery.js';
9
9
  import { claimCheckpointDialog, finishCheckpointDialog, waitForCheckpointTerminal } from './terminal-launch.js';
10
10
  import { browserPage } from './browser-page.js';
11
11
  import { recordOverride } from './overrides.js';
12
+ import { withTextStream, partialField } from './stream.js';
13
+ import { loadChangeView } from './change-view.js';
12
14
 
13
15
  const ID = /^[a-f0-9]{64}$/u;
14
16
  const cancelled = () => Object.assign(new Error('Check paused. Progress is saved.'), { code: 'SPHINX_CANCELLED' });
@@ -35,7 +37,7 @@ export async function openQuestionnaireBrowser(url, { env = process.env, platfor
35
37
  function publicFields(state) {
36
38
  if (!state) return {};
37
39
  return {
38
- id: state.id, target: state.target,
40
+ id: state.id, target: state.target, event: state.event,
39
41
  route: state.route && { provider: state.route.provider, model: state.route.model, tier: state.route.tier },
40
42
  progress: state.progress, usage: state.usage, tokens: state.tokens,
41
43
  ...(state.question ? { question: { id: state.question.id, prompt: state.question.prompt, choices: state.question.choices || [] } } : {}),
@@ -50,6 +52,7 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
50
52
  store: providedStore, readConfig = loadConfig, revalidate = revalidateSnapshot,
51
53
  reserve = claimCheckpointDialog, release = finishCheckpointDialog, waitForExisting = waitForCheckpointTerminal,
52
54
  openBrowser = openQuestionnaireBrowser, output = line => process.stderr.write(`${line}\n`), logOverride = recordOverride,
55
+ loadView = loadChangeView,
53
56
  } = {}) {
54
57
  const token = randomBytes(32).toString('base64url'), nonce = randomBytes(24).toString('base64');
55
58
  let server, origin, url, opened = false, closed = false, active, skip, resolver, sequence = 0, waitingGit = false;
@@ -60,6 +63,12 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
60
63
  // The chat transcript for the current checkpoint lives only in this process.
61
64
  // It contains public questions, feedback, and the human's own text.
62
65
  let transcript = [], turns = 0;
66
+ // Read-only display of the checkpoint's frozen comparison. It is loaded
67
+ // beside the questionnaire and can never block, grade, or approve it.
68
+ let view, evidence, evidenceKey;
69
+ const viewState = () => !view ? undefined : view.status === 'ready'
70
+ ? { status: 'ready', header: view.model.header, totals: view.model.totals }
71
+ : { status: view.status, ...(view.reason ? { reason: view.reason } : {}) };
63
72
  const say = item => { transcript = [...transcript, { id: `m${++turns}`, ...item }]; };
64
73
  const settle = status => { transcript = transcript.map(item => item.role === 'human' && item.status === 'checking' ? { ...item, status } : item); };
65
74
  const showQuestion = visible => {
@@ -78,8 +87,10 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
78
87
  else stream.write(event);
79
88
  } catch { stream.destroy(); streams.delete(stream); }
80
89
  };
81
- const publish = next => {
82
- state = { ...next, messages: transcript, waitingGit, operationClosed: closed, revision: randomUUID(), sequence: ++sequence };
90
+ // Display-only updates (change view, evidence) keep the revision, so they
91
+ // never turn an answer already in flight into a stale submission.
92
+ const publish = (next, { display = false } = {}) => {
93
+ state = { ...next, messages: transcript, change: viewState(), evidence: evidence && evidence.checkpoint === next.id ? { questionId: evidence.questionId, refs: evidence.refs } : undefined, waitingGit, operationClosed: closed, revision: display ? next.revision : randomUUID(), sequence: ++sequence };
83
94
  const event = `data: ${JSON.stringify(state)}\n\n`;
84
95
  for (const stream of streams) writeEvent(stream, event);
85
96
  };
@@ -103,11 +114,20 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
103
114
  if (req.headers.host !== new URL(origin).host || (req.headers['sec-fetch-site'] && !['same-origin', 'none'].includes(req.headers['sec-fetch-site']))) return send(res, 403, { error: 'Forbidden request.' });
104
115
  const route = req.url?.split('?')[0];
105
116
  const segments = route?.split('/') || [];
106
- if (!equal(segments[1], token) || segments.length > 3 || (segments[2] && !['state', 'events', 'action'].includes(segments[2]))) return send(res, 404, { error: 'Not found.' });
117
+ if (!equal(segments[1], token) || segments.length > 3 || (segments[2] && !['state', 'events', 'action', 'change', 'diff'].includes(segments[2]))) return send(res, 404, { error: 'Not found.' });
107
118
  if (req.method === 'GET' && !segments[2]) {
108
119
  res.writeHead(200, { ...headers, 'Content-Type': 'text/html; charset=utf-8' }); res.end(browserPage({ token, nonce })); return;
109
120
  }
110
121
  if (req.method === 'GET' && segments[2] === 'state') return send(res, 200, state);
122
+ if (req.method === 'GET' && ['change', 'diff'].includes(segments[2])) {
123
+ const query = new URL(req.url, origin).searchParams, current = view;
124
+ if (!current || query.get('id') !== current.id || state.id !== current.id) return send(res, 409, { error: 'This checkpoint changed. Reload the current change.' });
125
+ if (current.status !== 'ready') return send(res, current.status === 'loading' ? 202 : 200, viewState());
126
+ try { await current.check(); } catch { return send(res, 409, { error: 'The code or settings changed. This view no longer describes the checkpoint.' }); }
127
+ if (segments[2] === 'change') return send(res, 200, { status: 'ready', id: current.id, ...current.model.summary() });
128
+ try { return send(res, 200, await current.model.diff(query.get('path') ?? '', { context: query.get('context') ?? '3' })); }
129
+ catch (error) { return send(res, 404, { error: error.code === 'CHANGE_VIEW_UNAVAILABLE' ? error.message : 'This file diff is unavailable.' }); }
130
+ }
111
131
  if (req.method === 'GET' && segments[2] === 'events') {
112
132
  if (streams.size >= 8) return send(res, 429, { error: 'Too many open questionnaire pages.' });
113
133
  res.writeHead(200, { ...headers, 'Content-Type': 'text/event-stream; charset=utf-8', Connection: 'keep-alive' });
@@ -167,6 +187,37 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
167
187
  output(automatic ? 'Sphinx: questionnaire opened in a background browser tab. Your coding window stays in place.' : 'Sphinx: open the local URL above to answer. No application was brought to the foreground.');
168
188
  }
169
189
  }
190
+ function startView(request, snapshot, config, operationEnv, store) {
191
+ evidence = undefined; evidenceKey = undefined;
192
+ const current = view = { id: request.id, status: 'loading', store, check: async () => {
193
+ const latest = await readConfig(snapshot.repoRoot, operationEnv);
194
+ if (policyHash(latest) !== policyHash(config) || !(await revalidate(snapshot, latest, { env: operationEnv }))) throw new Error('stale');
195
+ } };
196
+ Promise.resolve().then(() => loadView(snapshot, config, { env: operationEnv })).then(model => {
197
+ if (view !== current) return;
198
+ current.status = 'ready'; current.model = model; publish(state, { display: true }); refreshEvidence(request.id);
199
+ }, error => {
200
+ if (view !== current) return;
201
+ current.status = 'unavailable';
202
+ current.reason = error?.code === 'CHANGE_VIEW_UNAVAILABLE' ? error.message : 'The changed files could not be loaded for display.';
203
+ publish(state, { display: true });
204
+ });
205
+ }
206
+ function refreshEvidence(id) {
207
+ const current = view;
208
+ if (current?.status !== 'ready' || current.id !== id || !current.store) return;
209
+ current.store.read(id).then(async session => {
210
+ const question = session.questions?.[session.index];
211
+ if (!question || session.status === 'passed') return;
212
+ const key = `${id}:${question.id}`;
213
+ if (evidenceKey === key) return;
214
+ evidenceKey = key;
215
+ const refs = await current.model.evidenceFor(question, session.review).catch(() => []);
216
+ if (view !== current || evidenceKey !== key) return;
217
+ evidence = { checkpoint: id, questionId: question.id, refs };
218
+ publish(state, { display: true });
219
+ }).catch(() => {});
220
+ }
170
221
  async function present({ request, env: operationEnv = env, signal: requestSignal = signal } = {}) {
171
222
  if (offLimits(operationEnv)) return { status: 'unavailable', reason: 'noninteractive', id: request?.id };
172
223
  if (closed || active) return { status: 'unavailable', id: request?.id };
@@ -202,6 +253,7 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
202
253
  const session = await store.read(request.id), config = await readConfig(request.repoRoot, operationEnv);
203
254
  if (session.snapshot.repoRoot !== request.repoRoot) throw new Error('This checkpoint belongs to another repository.');
204
255
  await validate(session.snapshot, config);
256
+ startView(request, session.snapshot, config, operationEnv, store);
205
257
  let visible = store.public(session);
206
258
  publish({ ...publicFields(visible), repoRoot: request.repoRoot, phase: 'working', message: 'Preparing your understanding check…' });
207
259
  await ensureServer(operationEnv);
@@ -229,13 +281,27 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
229
281
  if (!visible) return skip ? skipped() : { status: 'pending', id: request.id };
230
282
  while (visible.status !== 'passed') {
231
283
  localSignal.throwIfAborted();
232
- showQuestion(visible);
284
+ showQuestion(visible); refreshEvidence(request.id);
233
285
  const action = await waitForAction({ ...publicFields(visible), repoRoot: request.repoRoot, phase: 'question', ...(visible.browserError ? { error: visible.browserError } : {}) }, ['answer', 'learn', 'dispute']);
234
286
  try {
235
287
  await validate(session.snapshot, config);
236
- visible = await store.answer(request.id, action.action === 'learn' ? 'I do not know. Explain this concept and give me a different scenario to apply it to.' : action.response.trim(), config, {
288
+ // Type the grader's feedback into the conversation as it streams.
289
+ // Only the first streamed feedback is shown; the saved result replaces it.
290
+ let draft, frozen = false, lastPublish = 0;
291
+ const onText = raw => {
292
+ if (frozen) return;
293
+ const field = partialField(raw, 'feedback');
294
+ if (!field?.text) return;
295
+ if (!draft) { say({ role: 'sphinx', kind: 'feedback', streaming: true, text: field.text }); draft = transcript.at(-1).id; }
296
+ else transcript = transcript.map(item => item.id === draft ? { ...item, text: field.text } : item);
297
+ if (field.complete) frozen = true;
298
+ const now = Date.now();
299
+ if (field.complete || now - lastPublish > 60) { lastPublish = now; publish(state); }
300
+ };
301
+ const dropDraft = () => { if (draft) transcript = transcript.filter(item => item.id !== draft); };
302
+ visible = await withTextStream(onText, () => store.answer(request.id, action.action === 'learn' ? 'I do not know. Explain this concept and give me a different scenario to apply it to.' : action.response.trim(), config, {
237
303
  learn: action.action === 'learn', dispute: action.action === 'dispute', expectedQuestionId: action.questionId, signal: localSignal,
238
- });
304
+ })).finally(dropDraft);
239
305
  settle('graded');
240
306
  if (visible.feedback) say({ role: 'sphinx', kind: 'feedback', text: visible.feedback });
241
307
  } catch (error) {
@@ -249,7 +315,7 @@ export function createBrowserInterview({ env = process.env, signal, timeoutMs =
249
315
  }
250
316
  localSignal.throwIfAborted(); await validate(session.snapshot, config);
251
317
  finalStatus = 'passed';
252
- publish({ ...publicFields(visible), repoRoot: request.repoRoot, phase: 'passed', message: 'Understanding check complete. Return to the original Git output for the operation’s result.' });
318
+ publish({ ...publicFields(visible), repoRoot: request.repoRoot, phase: 'passed', message: 'Understanding check passed for this checkpoint. It records your answers to these questions; it is not a review of every changed line or a claim that the code is correct. Return to the original Git output for the operation’s result.' });
253
319
  return { status: 'passed', id: request.id };
254
320
  } catch (error) {
255
321
  settle('not-graded');