@visual-z/xeno 0.4.0 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # xeno changelog
2
2
 
3
+ ## 0.5.0 — 2026-10-09
4
+
5
+ - Place the Control Panel in a persistent right sidebar, with a drawer on narrow terminals. Preserve the composer, drafts, attachments, scrolling and input focus.
6
+ - Save confirmed model and thinking choices for later launches, and restore the exact available model from recent sessions.
7
+ - Add explicit `--yolo` / `-y` runs across interactive, print and control sessions. The flag applies only to the current process; tool selection, file checks and cancellation still apply.
8
+ - Request the exact required verification command after shell-only changes instead of ending without the declared acceptance receipt.
9
+ - Preserve saved tool selections when reopening sessions, including extension-enabled sessions, while respecting explicit CLI filters and current approval rules.
10
+ - Keep raw terminal input continuous during startup handoff so Enter executes early commands correctly; restore the original terminal mode even if the renderer fails to mount.
11
+ - Prefer automatic Session version checks after reading files, while retaining explicit version receipts and conflict rejection. Keep the ten-tool coding default after separate live trials did not support reducing it.
12
+
3
13
  ## 0.4.0 — 2026-10-09
4
14
 
5
15
  - Add the goal, evidence progress and Context Lens workbench through `/control`. Keep execution outcomes separate from verified acceptance and explain the sources retained in the actual model context.
package/README.md CHANGED
@@ -17,7 +17,7 @@ is ever summarized by a model, and nothing is ever rewritten in place.
17
17
  messages[] frozen system prefix + handoff + this node's events
18
18
  ```
19
19
 
20
- Version **0.4.0**. The on-disk session format will still change; `0.x` is that
20
+ Version **0.5.0**. The on-disk session format will still change; `0.x` is that
21
21
  statement. Published as `@visual-z/xeno`; the license is `UNLICENSED` (readable and installable, no reuse granted).
22
22
 
23
23
  ## Install
@@ -107,7 +107,7 @@ xeno [-m <model>] [--provider <name>] [--api-key <key>]
107
107
  [--tools names] [--exclude-tools names] [--no-tools]
108
108
  [--tool-profile coding|readonly|research|data|full]
109
109
  [--acceptance-file <contract.json>]
110
- [--allow-tools names] [--approve|--no-approve]
110
+ [--allow-tools names] [--yolo|-y] [--approve|--no-approve]
111
111
  [--theme <name|path>] [--use-theme <name>] [--no-themes]
112
112
  [--tui-mode regular|fullscreen]
113
113
  [--skill <path>] [--no-skills|-ns]
@@ -265,9 +265,10 @@ mode `0600`.
265
265
  ## Isolation
266
266
 
267
267
  This is not a sandbox. Tools run with the permissions of the user who
268
- started xeno. `read` / `write` / `edit` stay inside the workspace root;
269
- `bash` does not — it asks before it runs so you can see the command. That
270
- ask is the last look at what is about to happen, **not a security
268
+ started xeno. `read` / `write` / `edit` stay inside the workspace root.
269
+ Shell entry points (`exec_command`, `environment`, `bash`, `target`) ask
270
+ before they run so you can see the command. That ask is the last look at
271
+ what is about to happen, **not a security
271
272
  boundary**.
272
273
 
273
274
  For untrusted repositories, generated code you do not intend to watch, or
@@ -279,17 +280,43 @@ Project trust (`/trust`, `--approve`) only decides whether to load
279
280
  It does not change what tools can do. `AGENTS.md` is loaded whether the
280
281
  project is trusted or not.
281
282
 
282
- New sessions default to the nine-tool `coding` profile: read, grep, find,
283
- write, edit, exec_command, write_stdin, history and note. `readonly`,
283
+ New sessions default to the ten-tool `coding` profile: read, grep, find,
284
+ write, edit, environment, exec_command, write_stdin, history and note. `readonly`,
284
285
  `research` (with branch/resolve), `data` (with query) and `full` choose other
285
286
  builtin defaults. Extensions remain available; explicit `--tools` overrides
286
- the profile and exclusions apply last. Resumed sessions keep their frozen tools.
287
+ the profile and exclusions apply last. Default session resume keeps the frozen
288
+ tool selection; explicit tool flags apply at a resource checkpoint. `/reload`
289
+ reselects tools using the current settings.
290
+ Tool selection does not bypass permission checks; restoring a tool does not
291
+ restore approvals from the previous process.
292
+
293
+ After reading an existing file, normally omit `read_handle` and `expected_sha256`
294
+ when writing or editing it: Session checks the latest observed file version.
295
+ Pass an exact receipt only to select a specific observation; a stale version
296
+ still requires a fresh read. Without any prior observation, omission provides
297
+ no stale-version protection.
287
298
 
288
299
  `--tools` / `--exclude-tools` / `--no-tools` remove tools from the schema
289
300
  the model sees. `--allow-tools exec_command` lets commands run without asking, which
290
301
  is what a CI job that has no tty has to pass explicitly. Without a tty,
291
302
  an `ask` is a deny that names that flag.
292
303
 
304
+ `--yolo` (or `-y`) allows every available tool without an approval prompt for
305
+ the current process. It works in interactive, print, and app-server modes;
306
+ resumed sessions use the current invocation's choice. Tool exclusions,
307
+ read-only branch capabilities, input checks and file path limits still apply.
308
+ YOLO does not enable project trust or get saved in settings or session history.
309
+ Without the flag, the next launch uses the default approval rules again.
310
+
311
+ ```bash
312
+ bun run agent --yolo # interactive session from this checkout
313
+ bun run agent --yolo -c # continue with approvals skipped
314
+ bun run agent --yolo -p "run tests" # explicit approval-free automation
315
+ ```
316
+
317
+ The installed CLI accepts `xeno --yolo` in version 0.5.0 and later. To
318
+ allow only host commands, use `--allow-tools exec_command,environment`.
319
+
293
320
  ## Workspace data
294
321
 
295
322
  Opening a session does not snapshot the working directory. File tools save the
@@ -70,7 +70,9 @@ bun run agent --control-socket /tmp/xeno-control-private/control.sock
70
70
 
71
71
  ## TUI 与终端状态
72
72
 
73
- 常驻 Goal 区展示原始目标、验收状态、执行状态和观察/重试数。`/control` 打开同一会话的面板:Tab 切 Goal / Progress / Context Lens,左右键切上下文来源/省略事件/handoff 恢复引用,上下键翻阅,R 刷新,I 取消,A 选择当前声明的人验条件。远程开始的普通 turn 会更新当前 TUI,并可在面板取消。TUI 输入的 steering 在同一 turn 的原有边界消费,followup 等原 turn 释放后执行;远程取消也把未发送内容归还编辑器。既有隔离 skill / shell 保留各自执行入口,当前协议不新增远程 skill/shell 方法。
73
+ 常驻 Goal 区展示原始目标、验收状态、执行状态和观察/重试数。`/control` 在右侧栏打开同一会话的面板,保留底部输入框和草稿;窄窗口改为消息区内的右侧抽屉,尺寸不足时提示放大。F6 切换面板与编辑器焦点,Esc 回到编辑器并保留面板,Q 关闭并恢复状态侧栏。面板有焦点时,Tab 切 Goal / Progress / Context Lens,左右键切上下文来源/省略事件/handoff 恢复引用,上下键或 PageUp/PageDown 翻阅,R 刷新,I 取消,A 选择当前声明的人验条件;其它文字输入会回到原编辑器。长目标和证据换行显示,缩放保留页签、滚动和草稿。
74
+
75
+ 远程开始的普通 turn 会更新当前 TUI,并可在面板取消。TUI 输入的 steering 在同一 turn 的原有边界消费,followup 等原 turn 释放后执行;远程取消也把未发送内容归还编辑器。既有隔离 skill / shell 保留各自执行入口,当前协议不新增远程 skill/shell 方法。
74
76
 
75
77
  无验收合同保持 `unverified`;模型结束 `model_end` 不等于目标 `verified`。Progress 的观察、产物、验收、新诊断和原样重试均带日志来源;数量不是完成百分比。Context Lens 复用实际 dispatch 的投影,只过滤检查视图,不修改 frozen prefix、handoff 或消息。
76
78
 
package/docs/providers.md CHANGED
@@ -15,6 +15,10 @@ xeno 内置 Pi 0.99.1 和 OpenCode 所用 Models.dev 的固定源并集,另加
15
15
 
16
16
  `/model` 只显示已接入且凭据可用的模型,最近使用的排在最前。保存位置与手写配置相同:key 在 `~/.xeno/auth.json`(0600),端点和启用的模型在 `~/.xeno/models.json` 的 `providers.<id>`。
17
17
 
18
+ 确认模型或 thinking 后会自动保存到 `~/.xeno/settings.json`,下次新会话沿用,不需要额外按 Ctrl+S。模型菜单、`/model <provider/model>`、命令面板与模型循环共用这一行为;thinking 菜单、`/thinking <level>` 与快捷键也会保存。保存失败会提示本次已生效但下次启动不能保证沿用,不会切到另一个供应商。
19
+
20
+ 显式启动参数只覆盖本次运行;`--continue` / `--resume` 沿用会话日志里的模型与 thinking,恢复和扩展程序化切换不会改写个人默认。未保存默认模型的旧配置先尝试可认证的最近选择,再按目录兜底;明确的配置层级顺序保持不变。
21
+
18
22
  ## Grok 官方账户
19
23
 
20
24
  输入 `/provider grok`,xeno 打开官方确认网页并显示 user code;在浏览器批准后,当前会话直接取得凭据。Esc 取消会中止轮询,取消后返回的 token 不会写入。过期时自动刷新,刷新响应未轮换 refresh token 时保留原值;失败会明确要求重新登录。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visual-z/xeno",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "Segmented append-only agent log with pure context and handoff projections",
@@ -0,0 +1,20 @@
1
+ /** User intent owns personal defaults; CLI overrides and restored/extension state only own a session. */
2
+ import { mergeSettings, type Settings } from "../core/settings.ts";
3
+ import { writeUserSettings } from "../store/resources.ts";
4
+ import { describeError } from "./render.ts";
5
+ import type { App } from "./startup.ts";
6
+
7
+ export type PreferenceOrigin = "user" | "session";
8
+
9
+ export async function commitUserSettings(app: App, patch: Settings): Promise<boolean> {
10
+ const ui = app.ui;
11
+ if (!ui) return false;
12
+ ui.settings = mergeSettings({ builtin: ui.settings, user: patch });
13
+ try { await writeUserSettings(patch); }
14
+ catch (error) {
15
+ app.io.say(app.io.palette.yellow(` The change is active in this session, but could not save settings for the next startup: ${describeError(error)}`));
16
+ return false;
17
+ }
18
+ ui.userOverrides = mergeSettings({ builtin: ui.userOverrides, user: patch });
19
+ return true;
20
+ }
package/src/apps/shell.ts CHANGED
@@ -58,14 +58,15 @@ import { registerSessionRoot } from "../store/session-browser.ts";
58
58
  import { createSession, findSessionByPrefix, listSessions, truncationNote, sessionsDir } from "../store/sessions.ts";
59
59
  import { exportSessionArchive, importSessionArchive } from "../store/session-archive.ts";
60
60
  import { resolveTuiMode } from "../core/tui-mode.ts";
61
- import { recordRecent } from "../core/model-picker.ts";
61
+ import { modelPreferencePatch, thinkingPreferencePatch } from "../core/model-preferences.ts";
62
62
  import { flattenTree, treeFromPoints, LEFT_BRANCH_MARK, EMPTY_TREE_MESSAGE, type TreeNode, type NodeParent, type TreePoint } from "../core/session-tree.ts";
63
63
  import { cycle } from "../core/cycle.ts";
64
64
  import { THINKING_LEVELS, asThinkingLevel as asEffort, supportedThinkingLevels, clampThinkingLevel } from "../core/thinking.ts";
65
65
  import { modelKey } from "../core/session-position.ts";
66
66
  import { defaultKeymap, listBindings } from "../core/keymap.ts";
67
- import { settingPatch, mergeSettings } from "../core/settings.ts";
68
- import { writeUserSettings, resolveTheme } from "../store/resources.ts";
67
+ import { settingPatch } from "../core/settings.ts";
68
+ import { resolveTheme } from "../store/resources.ts";
69
+ import { commitUserSettings, type PreferenceOrigin } from "./preferences.ts";
69
70
  import { colorEnabled } from "./render.ts";
70
71
  import { createStyle, IDENTITY } from "../tui/style.ts";
71
72
  import { lastAssistantText, toHtml } from "../core/export.ts";
@@ -144,9 +145,9 @@ async function bindExtensions(app: App, tui?: Ui): Promise<void> {
144
145
  },
145
146
  setModel: async model => {
146
147
  if (typeof model !== "object" || !model || !("provider" in model) || !("id" in model)) return false;
147
- await switchByPattern(app, String(model.provider) + "/" + String(model.id)); return app.providerConfig.spec.id === model.id && app.providerConfig.spec.provider === model.provider;
148
+ await switchByPattern(app, String(model.provider) + "/" + String(model.id), "session"); return app.providerConfig.spec.id === model.id && app.providerConfig.spec.provider === model.provider;
148
149
  },
149
- setThinking: async level => { await applyThinkingCommand(app, level, tui ?? null); },
150
+ setThinking: async level => { await applyThinkingCommand(app, level, tui ?? null, "session"); },
150
151
  exec: (command, args, options) => app.session.runExtensionCommand(command, args, options),
151
152
  });
152
153
  if (!initializedExtensions.has(registry)) {
@@ -822,7 +823,8 @@ async function commitSetting(app: App, field: string, value: string, tui: Ui | n
822
823
  }
823
824
  if (key === "thinking") {
824
825
  const effort = asEffort(value);
825
- if (!effort || !await applyThinking(app, effort, tui)) return;
826
+ if (effort) await applyThinking(app, effort, tui);
827
+ return;
826
828
  }
827
829
  const parsed = settingPatch(key === "status" ? "statusBar" : field, value);
828
830
  if (parsed.diagnostics.length) { app.io.say(C.yellow(parsed.diagnostics.join("\n"))); return; }
@@ -833,12 +835,12 @@ async function commitSetting(app: App, field: string, value: string, tui: Ui | n
833
835
  app.ui.theme = theme;
834
836
  app.ui.style = colorEnabled() ? createStyle(theme.colors) : IDENTITY;
835
837
  }
836
- await writeUserSettings(parsed.settings);
837
- app.ui.settings = mergeSettings({ builtin: app.ui.settings, user: parsed.settings });
838
- app.ui.userOverrides = mergeSettings({ builtin: app.ui.userOverrides, user: parsed.settings });
839
- if (key === "enableskillcommands") { await reloadAppResources(app); await tui?.refreshResources(); }
840
- if (tui) tui.toast(`${field} · ${value}`, "success");
841
- else app.io.say(C.dim(` ${field} ${value}`));
838
+ const saved = await commitUserSettings(app, parsed.settings);
839
+ if (saved && key === "enableskillcommands") { await reloadAppResources(app); await tui?.refreshResources(); }
840
+ if (saved) {
841
+ if (tui) tui.toast(`${field} · ${value}`, "success");
842
+ else app.io.say(C.dim(` ${field} ${value}`));
843
+ }
842
844
  tui?.applyConfiguration();
843
845
  if (key === "theme") tui?.followTheme();
844
846
  tui?.redraw();
@@ -856,7 +858,7 @@ function effortSupported(spec: ModelSpec, level: Effort): boolean {
856
858
  * must not cut: a cut drops the prefix cache, and thinking blocks stay
857
859
  * valid because the model did not change.
858
860
  */
859
- async function applyThinking(app: App, level: Effort, tui: Ui | null): Promise<boolean> {
861
+ async function applyThinking(app: App, level: Effort, tui: Ui | null, origin: PreferenceOrigin = "user"): Promise<boolean> {
860
862
  const C = app.io.palette;
861
863
  if (!effortSupported(app.providerConfig.spec, level)) {
862
864
  app.io.say(
@@ -874,19 +876,20 @@ async function applyThinking(app: App, level: Effort, tui: Ui | null): Promise<b
874
876
  if (app.ui !== undefined) {
875
877
  app.ui.settings = { ...app.ui.settings, thinking: level };
876
878
  }
879
+ if (origin === "user") await commitUserSettings(app, thinkingPreferencePatch(app.providerConfig.spec, level));
877
880
  app.io.say(C.dim(` thinking ${level}`));
878
881
  tui?.redraw();
879
882
  return true;
880
883
  }
881
884
 
882
- async function applyThinkingCommand(app: App, level: string | undefined, tui: Ui | null): Promise<Outcome> {
885
+ async function applyThinkingCommand(app: App, level: string | undefined, tui: Ui | null, origin: PreferenceOrigin = "user"): Promise<Outcome> {
883
886
  if (level !== undefined) {
884
887
  const effort = asEffort(level);
885
888
  if (effort === undefined) {
886
889
  app.io.say(app.io.palette.yellow(" thinking must be " + THINKING_LEVELS.join("|")));
887
890
  return "continue";
888
891
  }
889
- await applyThinking(app, effort, tui);
892
+ await applyThinking(app, effort, tui, origin);
890
893
  return "continue";
891
894
  }
892
895
  if (tui) {
@@ -1486,7 +1489,7 @@ async function adoptCredential(app: App, ready: readonly ModelSpec[]): Promise<v
1486
1489
  const got = app.credentialOf(spec);
1487
1490
  if (!got.ok) return;
1488
1491
  if (app.credential.ok || app.session.state.handoff !== null || app.session.state.events.some((e) => e.t === "assistant")) {
1489
- await switchToModel(app, spec);
1492
+ await switchToModel(app, spec, undefined, "session");
1490
1493
  return;
1491
1494
  }
1492
1495
  const nextConfig: ProviderConfig = {
@@ -1511,7 +1514,7 @@ async function adoptCredential(app: App, ready: readonly ModelSpec[]): Promise<v
1511
1514
  app.io.say(C.green(` now ${spec.name}`) + C.dim(` ${spec.id}`));
1512
1515
  }
1513
1516
 
1514
- async function switchByPattern(app: App, pattern: string): Promise<void> {
1517
+ async function switchByPattern(app: App, pattern: string, origin: PreferenceOrigin = "user"): Promise<void> {
1515
1518
  const C = app.io.palette;
1516
1519
  const found = scopedModels([pattern], app.catalog, true), selected = found.entries[0];
1517
1520
  if (!selected) {
@@ -1519,16 +1522,11 @@ async function switchByPattern(app: App, pattern: string): Promise<void> {
1519
1522
  app.io.say(C.red(` ${diagnostic.ok ? found.diagnostics.join("; ") : diagnostic.reason}`));
1520
1523
  return;
1521
1524
  }
1522
- await switchToModel(app, selected.model, selected.thinking);
1525
+ await switchToModel(app, selected.model, selected.thinking, origin);
1523
1526
  }
1524
1527
 
1525
- export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort): Promise<void> {
1528
+ export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort, origin: PreferenceOrigin = "user"): Promise<void> {
1526
1529
  const C = app.io.palette;
1527
- if (spec.id === app.providerConfig.spec.id && spec.provider === app.providerConfig.spec.provider) {
1528
- if (thinking) await applyThinking(app, clampThinkingLevel(spec, thinking), null);
1529
- app.io.say(C.dim(` already on ${spec.id}`));
1530
- return;
1531
- }
1532
1530
  const got = app.credentialOf(spec);
1533
1531
  if (!got.ok) {
1534
1532
  app.io.say(C.red(`\n ${got.reason}\n`));
@@ -1537,6 +1535,12 @@ export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort
1537
1535
  }
1538
1536
  return;
1539
1537
  }
1538
+ if (spec.id === app.providerConfig.spec.id && spec.provider === app.providerConfig.spec.provider) {
1539
+ if (thinking) await applyThinking(app, clampThinkingLevel(spec, thinking), null, "session");
1540
+ if (origin === "user") await rememberModelSelection(app, spec, thinking);
1541
+ app.io.say(C.dim(` already on ${spec.id}`));
1542
+ return;
1543
+ }
1540
1544
  const nextConfig: ProviderConfig = {
1541
1545
  spec,
1542
1546
  maxTokens: outputTokenBudget(spec),
@@ -1554,15 +1558,15 @@ export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort
1554
1558
  app.providerConfig = nextConfig;
1555
1559
  app.credential = got;
1556
1560
  app.io.say(C.green(` now ${spec.name}`) + C.dim(` ${spec.id}`));
1557
- await rememberRecentModel(app, spec);
1561
+ if (origin === "user") await rememberModelSelection(app, spec, thinking);
1558
1562
  }
1559
1563
 
1560
- /** Recency feeds the picker's first section; a write failure only costs ordering. */
1561
- async function rememberRecentModel(app: App, spec: ModelSpec): Promise<void> {
1564
+ /** N.8 must accept the checkpoint before a user choice can own the next startup. */
1565
+ async function rememberModelSelection(app: App, spec: ModelSpec, explicitThinking: Effort | undefined): Promise<void> {
1562
1566
  if (!app.ui) return;
1563
- const recentModels = recordRecent(app.ui.settings.recentModels ?? [], modelKey(spec));
1564
- app.ui.settings = { ...app.ui.settings, recentModels };
1565
- try { await writeUserSettings({ recentModels }); } catch { /* ordering only */ }
1567
+ const effort = app.providerConfig.effort;
1568
+ const patch = modelPreferencePatch(spec, app.ui.settings.recentModels ?? [], effort);
1569
+ await commitUserSettings(app, explicitThinking === undefined ? patch : { ...patch, ...thinkingPreferencePatch(spec, effort) });
1566
1570
  }
1567
1571
 
1568
1572
  function sessionDirOf(app: App): string | undefined {
@@ -7,7 +7,7 @@
7
7
  type StartupStdin = Pick<NodeJS.ReadStream, "isTTY" | "isRaw" | "setRawMode" | "setEncoding" | "on" | "off" | "resume" | "pause" | "unshift">;
8
8
  export interface StartupInput {
9
9
  phase(name: string): void;
10
- /** Pause, restore the shell's mode and replay held bytes to the next input owner. */
10
+ /** Pause and replay held bytes while keeping raw mode for the next input owner. */
11
11
  handoff(): void;
12
12
  /** Final cleanup discards unsubmitted input instead of leaking it to the shell. */
13
13
  restore(): void;
@@ -45,9 +45,14 @@ export function createStartupInput(opts: {
45
45
  stdin.off("data", receive);
46
46
  signals.off("SIGINT", interrupt); signals.off("SIGTERM", terminate); signals.off("SIGHUP", hangup);
47
47
  pasting = false; partial = "";
48
+ };
49
+ const restore = () => {
50
+ if (closed) return;
51
+ closed = true; release(); pending = [];
52
+ // D20: ownership transfer must not expose a cooked-mode gap that turns CR into LF.
53
+ // Final cleanup also restores termios when the next renderer never mounted.
48
54
  stdin.setRawMode(wasRaw);
49
55
  };
50
- const restore = () => { closed = true; release(); pending = []; };
51
56
  const cancel = (code: number) => {
52
57
  if (!active) return;
53
58
  restore();
@@ -177,6 +177,8 @@ export interface Args {
177
177
  /** `--no-builtin-tools`. False when the flag was not passed. */
178
178
  noBuiltinTools: boolean;
179
179
  allowTools: string[];
180
+ /** `--yolo` / `-y`: skip tool approval for this process only. */
181
+ yolo: boolean;
180
182
  /** `--approve` true, `--no-approve` false. Omitted when neither. */
181
183
  approve?: boolean;
182
184
  configCommand?: boolean;
@@ -271,6 +273,7 @@ const KNOWN_FLAGS: readonly string[] = [
271
273
  "--no-tools", "-nt",
272
274
  "--no-builtin-tools",
273
275
  "--allow-tools",
276
+ "--yolo", "-y",
274
277
  "--approve", "-a",
275
278
  "--no-approve", "-na",
276
279
  "--theme",
@@ -343,7 +346,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
343
346
  model: "", provider: "", apiKey: "", listModels: false, modelsCommand: false, modelsRefresh: false, fresh: false,
344
347
  noContextFiles: false, appendSystemPrompt: [],
345
348
  systemPrompt: null,
346
- excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [],
349
+ excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [], yolo: false,
347
350
  print: false,
348
351
  theme: "",
349
352
  thinking: "",
@@ -428,6 +431,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
428
431
  else if (a === "--no-tools" || a === "-nt") args.noTools = true;
429
432
  else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
430
433
  else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
434
+ else if (a === "--yolo" || a === "-y") args.yolo = true;
431
435
  else if (a === "--approve" || a === "-a") args.approve = true;
432
436
  else if (a === "--no-approve" || a === "-na") args.approve = false;
433
437
  else if (a === "--theme") {
@@ -853,10 +857,14 @@ export function toolSelectionOf(args: Args) {
853
857
  * the provider bills, and an extra field there would look like a tool
854
858
  * argument.
855
859
  */
856
- export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
860
+ export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = [], builtinDefaults?: readonly string[]) {
857
861
  const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
858
- const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) && (!tool.optional || args.tools?.includes(tool.name))).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
859
- const selected = selectTools(available, toolSelectionOf(args));
862
+ // O.7/D6: a cold app open uses saved capabilities as defaults, never today's profile.
863
+ // Explicit selection still wins; extension loading and current exclusions remain live.
864
+ const retained = builtinDefaults !== undefined && args.tools === undefined && args.toolProfile === undefined ? new Set(builtinDefaults) : undefined;
865
+ const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) &&
866
+ (retained === undefined ? !tool.optional || args.tools?.includes(tool.name) : retained.has(tool.name))).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
867
+ const selected = selectTools(available, { ...toolSelectionOf(args), ...(retained ? { profile: "full" as const } : {}) });
860
868
  const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
861
869
  .map(({ name, description, input_schema }) => ({ name, description, input_schema }));
862
870
  const deferredNames = new Set(deferred.map(tool => tool.name));
@@ -909,7 +917,7 @@ export function createSessionFor(opts: {
909
917
  ...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
910
918
  allow: names,
911
919
  deferredTools: deferred,
912
- rules: rulesWithAllowTools(args.allowTools),
920
+ rules: rulesWithAllowTools(args.allowTools, { yolo: args.yolo }),
913
921
  sessionRules,
914
922
  askSlot,
915
923
  ...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
@@ -1097,10 +1105,19 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1097
1105
  opts.startupPhase?.("Opening session history");
1098
1106
  await session.start();
1099
1107
  resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
1100
- if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
1101
- typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
1102
- await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions, session.config.frozen.tools) });
1108
+ const restoredTools = sessionToolCatalog(opts.args, resources.extensions, session.config.frozen.tools,
1109
+ priorCursor === null ? undefined : declaredToolNames(session.config.frozen.tools));
1110
+ const refreshResources = resources.extensions.loaded.length > 0 || session.config.frozen.tools.some(tool =>
1111
+ typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length);
1112
+ const explicitTools = opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
1113
+ opts.args.noBuiltinTools || opts.args.excludeTools.length > 0;
1114
+ if (refreshResources || (priorCursor !== null && explicitTools)) {
1115
+ await session.reloadResources({ system: refreshResources ? resources.system : session.config.frozen.system, tools: restoredTools.schemas });
1103
1116
  }
1117
+ // Admission follows this app's selected declaration; current process approval rules stay intact.
1118
+ // Keep this out of Session.start(): an SDK caller can intentionally supply a narrower allowlist.
1119
+ session.config.tools.allow = declaredToolNames(session.config.frozen.tools);
1120
+ session.config.tools.deferredTools = restoredTools.deferred;
1104
1121
  if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
1105
1122
  await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
1106
1123
  { sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
@@ -1455,7 +1472,7 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1455
1472
  " [--acceptance-file contract.json]\n" +
1456
1473
  " [--control-socket /private/directory/control.sock]\n" +
1457
1474
  " [--no-builtin-tools]\n" +
1458
- " [--allow-tools names] [--approve|--no-approve]\n" +
1475
+ " [--allow-tools names] [--yolo|-y] [--approve|--no-approve]\n" +
1459
1476
  " [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
1460
1477
  " [--skill <path>] [--no-skills]\n" +
1461
1478
  " [--prompt-template <path>] [--no-prompt-templates]\n" +
@@ -1481,7 +1498,10 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1481
1498
  " --tools is a strict allowlist; excluded tools leave the schema.\n" +
1482
1499
  " New sessions use coding (10 tools); full exposes default built-ins.\n" +
1483
1500
  " --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
1484
- " bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
1501
+ " Shell entry points (exec_command, environment, bash, target) ask before they run.\n" +
1502
+ " --allow-tools exec_command skips asks for that tool for this process.\n" +
1503
+ " --yolo (-y) skips all tool approvals for this process, including resumed sessions.\n" +
1504
+ " Tool exclusions and path limits still apply; YOLO does not change project trust.\n" +
1485
1505
  " --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
1486
1506
  " --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
1487
1507
  " --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +