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.
- package/README.md +106 -273
- package/docs/architecture.md +16 -0
- package/docs/configuration.md +241 -0
- package/docs/integrations.md +135 -0
- package/docs/logo.svg +3 -0
- package/package.json +1 -1
- package/src/browser-interview.js +74 -8
- package/src/browser-page.js +287 -32
- package/src/change-view.js +314 -0
- package/src/cli.js +57 -20
- package/src/config.js +57 -5
- package/src/evaluator.js +31 -5
- package/src/help.js +5 -4
- package/src/install.js +3 -1
- package/src/logo.js +6 -0
- package/src/mcp.js +2 -1
- package/src/openrouter.js +65 -35
- package/src/overrides.js +16 -5
- package/src/preparation.js +19 -3
- package/src/prompts.js +25 -1
- package/src/review-workspace.js +13 -10
- package/src/reviewer.js +9 -4
- package/src/routing.js +3 -1
- package/src/sessions.js +10 -1
- package/src/stream.js +59 -0
- package/src/terminal.js +16 -14
- package/src/version.js +4 -0
|
@@ -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.
|
|
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": {
|
package/src/browser-interview.js
CHANGED
|
@@ -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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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');
|