@visual-z/xeno 0.2.1 → 0.4.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.
Files changed (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,7 +1,8 @@
1
- /** Startup owns raw input until a Pi screen can own it (D19). */
2
- import { StdinBuffer } from "../vendor/pi-tui/stdin-buffer.ts";
3
- import { isKeyRelease, matchesKey } from "../vendor/pi-tui/keys.ts";
4
- import { resolveEscapeTimeoutMs } from "../vendor/pi-tui/terminal.ts";
1
+ /**
2
+ * Startup owns raw input until the renderer can (D20). Keystrokes typed while
3
+ * configuration loads are held as original bytes and handed to the next owner;
4
+ * only Ctrl+C outside a bracketed paste is interpreted here.
5
+ */
5
6
 
6
7
  type StartupStdin = Pick<NodeJS.ReadStream, "isTTY" | "isRaw" | "setRawMode" | "setEncoding" | "on" | "off" | "resume" | "pause" | "unshift">;
7
8
  export interface StartupInput {
@@ -31,7 +32,9 @@ export function createStartupInput(opts: {
31
32
  let active = false, closed = false, announced = false;
32
33
  let phase = "Reading configuration";
33
34
  let pending: string[] = [];
34
- let parser: StdinBuffer | undefined;
35
+ let pasting = false;
36
+ /** An escape sequence cut across chunks; joined with the next chunk before matching. */
37
+ let partial = "";
35
38
  let timer: ReturnType<typeof setTimeout> | undefined;
36
39
 
37
40
  const release = () => {
@@ -41,7 +44,7 @@ export function createStartupInput(opts: {
41
44
  stdin.pause();
42
45
  stdin.off("data", receive);
43
46
  signals.off("SIGINT", interrupt); signals.off("SIGTERM", terminate); signals.off("SIGHUP", hangup);
44
- parser?.destroy(); parser = undefined;
47
+ pasting = false; partial = "";
45
48
  stdin.setRawMode(wasRaw);
46
49
  };
47
50
  const restore = () => { closed = true; release(); pending = []; };
@@ -56,14 +59,17 @@ export function createStartupInput(opts: {
56
59
  // Keep original chunks, including partial escapes/pastes, for Pi's own decoder.
57
60
  // The temporary parser only recognizes cancellation outside bracketed paste.
58
61
  pending.push(data);
59
- parser?.process(data);
62
+ const joined = partial + data;
63
+ const cut = trailingPartialEscape(joined);
64
+ const whole = joined.slice(0, joined.length - cut.length);
65
+ partial = cut;
66
+ if (interruptsStartup(whole, pasting)) interrupt();
67
+ pasting = pasteState(whole, pasting);
60
68
  };
61
69
  const announce = () => { if (active) { announced = true; report(` Starting xeno: ${phase} (Ctrl+C to cancel)`); } };
62
70
  const capture = () => {
63
71
  if (closed || active) return;
64
72
  active = true; announced = false;
65
- parser = new StdinBuffer({ escapeTimeout: resolveEscapeTimeoutMs() });
66
- parser.on("data", (data: string) => { if (!isKeyRelease(data) && matchesKey(data, "ctrl+c")) interrupt(); });
67
73
  stdin.setEncoding("utf8");
68
74
  stdin.on("data", receive);
69
75
  signals.on("SIGINT", interrupt); signals.on("SIGTERM", terminate); signals.on("SIGHUP", hangup);
@@ -92,3 +98,33 @@ export function createStartupInput(opts: {
92
98
  },
93
99
  };
94
100
  }
101
+
102
+ /** Bracketed paste state after a chunk: a paste that started and did not end yet. */
103
+ export function pasteState(data: string, pasting: boolean): boolean {
104
+ const start = data.lastIndexOf("\x1b[200~"), end = data.lastIndexOf("\x1b[201~");
105
+ if (start < 0 && end < 0) return pasting;
106
+ return start > end;
107
+ }
108
+
109
+ /** Ctrl+C as legacy ETX or kitty CSI-u (99;5u press, not a :3 release), outside a paste. */
110
+ export function interruptsStartup(data: string, pasting: boolean): boolean {
111
+ let outside = "";
112
+ let inPaste = pasting;
113
+ for (let i = 0; i < data.length;) {
114
+ if (data.startsWith("\x1b[200~", i)) { inPaste = true; i += 6; continue; }
115
+ if (data.startsWith("\x1b[201~", i)) { inPaste = false; i += 6; continue; }
116
+ if (!inPaste) outside += data[i];
117
+ i++;
118
+ }
119
+ return outside.includes("\x03") || /\x1b\[99;5(?::1)?u/.test(outside);
120
+ }
121
+
122
+ /** The unfinished CSI/SS3 at the end of a chunk ("\x1b[99;" waiting for "5u"), or "". */
123
+ export function trailingPartialEscape(data: string): string {
124
+ const at = data.lastIndexOf("\x1b");
125
+ if (at < 0) return "";
126
+ const tail = data.slice(at);
127
+ if (tail === "\x1b") return tail;
128
+ if (/^\x1b\[[0-9;:?]*$/.test(tail) || /^\x1bO$/.test(tail)) return tail;
129
+ return "";
130
+ }
@@ -1,5 +1,6 @@
1
1
  import { release } from "node:os";
2
- import { workingProtocol } from "../core/continuation.ts";
2
+ import { codingWorkingProtocol } from "../core/continuation.ts";
3
+ import { taskGuidance } from "../core/prompt-policy.ts";
3
4
  import { environmentSection, type SessionEnvironment } from "../core/environment.ts";
4
5
  /**
5
6
  * From argv to a session that can run. Credential order, model selection,
@@ -12,8 +13,10 @@ import { mkdir } from "node:fs/promises";
12
13
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
13
14
  import { Session, systemClock } from "../runtime/loop.ts";
14
15
  import { DEFAULT_PROVIDER, type ProviderConfig } from "../runtime/provider.ts";
15
- import { createProvider, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
16
+ import { createProvider, providerSessionId, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
16
17
  import { ExtensionRegistry } from "../runtime/extensions.ts";
18
+ import { DISCOVERY_TOOL_NAME } from "../runtime/tool-discovery.ts";
19
+ import { declaredToolNames } from "../core/tool-discovery.ts";
17
20
  import { CATALOG, outputTokenBudget, type ModelSpec } from "../runtime/model.ts";
18
21
  import { asThinkingLevel, clampThinkingLevel, supportedThinkingLevels } from "../core/thinking.ts";
19
22
  import { modelKey } from "../core/session-position.ts";
@@ -25,10 +28,11 @@ import {
25
28
  import { authPath, configDir, readJsonFile } from "../store/config.ts";
26
29
  import { loadUiResources, loadTextResources, type UiResources } from "../store/resources.ts";
27
30
  import { discoverResources, type ResourceIndex } from "../store/resource-index.ts";
28
- import { runResourceConfig } from "./resource-browser.ts";
31
+ import { pickProjectTrust, pickStartupSession, runResourceConfig } from "./ui/standalone.ts";
29
32
  import { PackageManager, type PackageCommand } from "../runtime/package-manager.ts";
30
- import { pickProjectTrust } from "./project-trust.ts";
31
- import { appendSkillsToSystem } from "../core/skills.ts";
33
+ import { loadAcceptanceContract } from "../store/acceptance.ts";
34
+ import type { AcceptanceContract } from "../core/acceptance.ts";
35
+ import { appendSkillsToSystem, type Skill } from "../core/skills.ts";
32
36
  import { COMMANDS, resolveSlashCatalog, type SlashCatalog } from "./commands.ts";
33
37
  import { createStyle, IDENTITY, type Style } from "../tui/style.ts";
34
38
  import type { Settings } from "../core/settings.ts";
@@ -38,23 +42,24 @@ import { readCursor, writeCursor, type Cursor } from "../store/log.ts";
38
42
  import { resolveLogDir, findSessionByPrefix, listSessions, sessionsDir, type ResolveLogOpts } from "../store/sessions.ts";
39
43
  import { recoverCut } from "../store/recovery.ts";
40
44
  import { planSessionEntry, restoreSessionModel } from "../core/session-entry.ts";
41
- import { pickStartupSession } from "./session-picker.ts";
42
45
  import { loadContext, assembleSystemPrompt, formatNotice, type LoadedContext } from "../store/context-files.ts";
43
46
  import {
44
47
  projectTrust,
45
48
  } from "../store/trust.ts";
46
49
  import { stat } from "node:fs/promises";
47
50
  import {
48
- parseToolList, rulesWithAllowTools, selectTools,
51
+ parseToolList, rulesWithAllowTools, selectTools, isToolProfile,
52
+ type ToolProfile,
49
53
  type PermissionRequest, type PermissionRule,
50
54
  } from "../core/permissions.ts";
51
55
  import {
52
- TOOL_SCHEMAS, DEFAULT_TOOL_CONTEXT, workspaceRoot,
56
+ TOOL_SCHEMAS, TOOL_DEFINITIONS, modelToolSchema, DEFAULT_TOOL_CONTEXT, workspaceRoot,
53
57
  type ApprovalChoice, type ToolContext,
54
58
  } from "../runtime/tools.ts";
55
- import { DEFAULT_BOUNDARY } from "../core/boundary.ts";
56
- import { DEFAULT_CONTEXT_POLICY, isContextPolicy, type ContextPolicy } from "../core/context-policy.ts";
57
- import { windowBudget } from "../core/context-window.ts";
59
+ import { assertNever } from "../core/exhaustive.ts";
60
+ import {
61
+ contextLimits, DEFAULT_CONTEXT_POLICY, isContextPolicy, type ContextLimits, type ContextPolicy,
62
+ } from "../core/context-policy.ts";
58
63
  import { colorEnabled, createPalette, describeError, stdinIsTty, stdoutIsTty, type Palette } from "./render.ts";
59
64
  import { type ActionId, type Keymap } from "../core/keymap.ts";
60
65
  import type { StartupInput } from "./startup-input.ts";
@@ -62,46 +67,16 @@ import type { StartupInput } from "./startup-input.ts";
62
67
  export const SYSTEM_PROMPT = systemPromptFor(TOOL_SCHEMAS.map((tool) => tool.name));
63
68
 
64
69
  /**
65
- * The instructions mention only tools the model can actually see. Naming a
66
- * tool that was excluded from the schema teaches it to call something that
67
- * will never work. The default prompt is left alone when every tool is visible.
70
+ * Tool schemas are the only capability inventory. A branch may inherit this
71
+ * stable policy while narrowing its tools; no parent tool recipe can leak.
68
72
  */
69
73
  export function systemPromptFor(toolNames: readonly string[], env?: SessionEnvironment): string {
70
- const has = (name: string) => toolNames.includes(name);
71
- const toolsLine = toolNames.length === 0
72
- ? "No tools are available in this session."
73
- : `Tools: ${toolNames.join(", ")}.` +
74
- (toolNames.some(name => ["grep", "find", "ls"].includes(name)) ? " Use the available grep/find/ls tools for confined search." : "") +
75
- (has("bash")
76
- ? " Use bash for verification commands and operations the other tools do not cover."
77
- : "");
78
-
79
- // Each line answers a failure class in the Terminal-Bench taxonomy
80
- // (arXiv 2601.11868): premature termination, no verification, step
81
- // repetition. The wording of the first and last follows the Codex
82
- // prompting guide: persist end to end, and end on a change or a blocker.
83
- const work = [
84
- "How to work:",
85
- "- Keep going until the request is fully resolved. When a reasonable assumption lets you proceed, make it and mention it at the end; stop to ask only when blocked on a decision only the user can make.",
86
- "- Understand before changing: read the relevant code, and for a bug, reproduce it first.",
87
- "- Put independent reads and searches in one response; they run in parallel.",
88
- "- After a change, run the project's own tests, build or type check. When a command fails, read the end of its output first; errors and summaries are printed last.",
89
- "- If the same approach fails twice, change the approach instead of retrying it.",
90
- "- A claim without a passing command is not evidence. Finish with what changed and the command that verified it, or the specific blocker.",
91
- ];
92
- if (has("bash")) work.push(
93
- "- Each bash call is a fresh non-interactive shell with no TTY: use workdir or `cd dir && ...` within one call. Never open pagers or interactive editors. " +
94
- "Start servers in the background with output redirected (`nohup cmd > app.log 2>&1 &`), then poll the log or port. Pass a larger timeout for long builds and installs.",
95
- );
96
- if (has("exec_command")) work.push("- For servers, REPLs and programs that prompt for input, use exec_command (tty=true when the program needs a terminal) and continue with write_stdin.");
97
- if (has("edit")) work.push("- Prefer edit to rewriting a whole file; copy old_string from read output without the line numbers.");
98
-
99
74
  return [
100
- "You are a coding agent working in a terminal.",
101
- toolsLine,
75
+ "You are an agent working in a terminal.",
76
+ "The declared tool schemas define available capabilities, input units and execution lifecycles. Do not assume additional tools or infer permission from a tool's visibility.",
102
77
  ...(env === undefined ? [] : [environmentSection(env)]),
103
- work.join("\n"),
104
- workingProtocol(toolNames),
78
+ taskGuidance(),
79
+ codingWorkingProtocol(toolNames),
105
80
  "Be concise. Do not narrate what you are about to do; do it, then say what happened.",
106
81
  ].join("\n\n");
107
82
  }
@@ -128,12 +103,12 @@ export function sessionEnvironment(workspace: string): SessionEnvironment {
128
103
  * Session policy a CLI coding session differs from a benchmark run in.
129
104
  * Benchmarks construct their own SessionConfig and keep the core defaults.
130
105
  */
131
- export function cliSessionConfig(args: Args): { continuationAfter: number; timeBudgetMs?: number } {
106
+ export function cliSessionConfig(args: Args): { continuationAfter: number; workingSet: true; workingSetVersion: 2; guidanceVersion: 1; strictVerification: true; timeBudgetMs?: number } {
132
107
  // Never within a request. A real Kimi K3 run with the reminder at reply six
133
108
  // spent half its requests on notes after the work was already verified
134
109
  // (fix-cli-continuation-noise). The reminder still fires where it earns its
135
110
  // place: a resumed handoff that cannot continue, and boundary preparation.
136
- return { continuationAfter: Infinity, ...(args.timeBudgetMs === undefined ? {} : { timeBudgetMs: args.timeBudgetMs }) };
111
+ return { continuationAfter: Infinity, workingSet: true, workingSetVersion: 2, guidanceVersion: 1, strictVerification: true, ...(args.timeBudgetMs === undefined ? {} : { timeBudgetMs: args.timeBudgetMs }) };
137
112
  }
138
113
 
139
114
  /**
@@ -151,6 +126,7 @@ export type AskSlot = {
151
126
  };
152
127
 
153
128
  export interface Args {
129
+ shellCheckpoints?: boolean;
154
130
  workspace: string;
155
131
  /**
156
132
  * `--state-dir <dir>`: where every xeno write goes -- snapshots, tool
@@ -193,6 +169,9 @@ export interface Args {
193
169
  systemPrompt: string | null;
194
170
  /** `--tools` allowlist. Omitted when the flag was not passed. */
195
171
  tools?: string[];
172
+ /** New sessions default to coding; restored context keeps its frozen schemas. */
173
+ toolProfile?: ToolProfile;
174
+ acceptanceFile?: string;
196
175
  excludeTools: string[];
197
176
  noTools: boolean;
198
177
  /** `--no-builtin-tools`. False when the flag was not passed. */
@@ -245,6 +224,8 @@ export interface Args {
245
224
  sessionName?: string;
246
225
  /** `--mode json`. Empty / omitted when the flag was not passed. */
247
226
  mode?: string;
227
+ /** Explicit local adapter endpoint. An owned mode-0700 directory is required. */
228
+ controlSocket?: string;
248
229
  /** `--verbose`. Expands the startup header's counts into lists. */
249
230
  verbose?: boolean;
250
231
  }
@@ -258,6 +239,7 @@ export interface Args {
258
239
  * parses; it just cannot be offered as a suggestion.
259
240
  */
260
241
  const KNOWN_FLAGS: readonly string[] = [
242
+ "--checkpoint-shell",
261
243
  "--workspace", "-w",
262
244
  "--path-root",
263
245
  "--state-dir",
@@ -283,6 +265,8 @@ const KNOWN_FLAGS: readonly string[] = [
283
265
  "--system-prompt",
284
266
  "--append-system-prompt",
285
267
  "--tools", "-t",
268
+ "--tool-profile",
269
+ "--acceptance-file",
286
270
  "--exclude-tools", "-xt",
287
271
  "--no-tools", "-nt",
288
272
  "--no-builtin-tools",
@@ -298,6 +282,7 @@ const KNOWN_FLAGS: readonly string[] = [
298
282
  "--no-prompt-templates",
299
283
  "--extension", "-e", "--no-extensions", "-ne", "--extension-flag",
300
284
  "--mode",
285
+ "--control-socket",
301
286
  "--verbose",
302
287
  "--print", "-p",
303
288
  "--help", "-h",
@@ -374,7 +359,8 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
374
359
  console.error(`${a} requires a value`);
375
360
  process.exit(1);
376
361
  }
377
- if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
362
+ if (a === "--checkpoint-shell") args.shellCheckpoints = true;
363
+ else if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
378
364
  else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
379
365
  else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
380
366
  else if (a === "--time-budget") {
@@ -429,6 +415,15 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
429
415
  else if (a === "--system-prompt") args.systemPrompt = argv[++i] ?? "";
430
416
  else if (a === "--append-system-prompt") args.appendSystemPrompt.push(argv[++i] ?? "");
431
417
  else if (a === "--tools" || a === "-t") args.tools = parseToolList(argv[++i] ?? "");
418
+ else if (a === "--acceptance-file") {
419
+ const path = argv[++i]; if (!path || path.startsWith("--")) throw new Error("--acceptance-file requires a JSON path");
420
+ args.acceptanceFile = path;
421
+ }
422
+ else if (a === "--tool-profile") {
423
+ const value = argv[++i] ?? "";
424
+ if (!isToolProfile(value)) throw new Error("unknown tool profile; choose coding, readonly, research, data or full");
425
+ args.toolProfile = value;
426
+ }
432
427
  else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
433
428
  else if (a === "--no-tools" || a === "-nt") args.noTools = true;
434
429
  else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
@@ -459,6 +454,11 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
459
454
  (args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[value.slice(0, at)] = value.slice(at + 1) === "true" ? true : value.slice(at + 1) === "false" ? false : value.slice(at + 1);
460
455
  }
461
456
  else if (a === "--mode") args.mode = argv[++i] ?? "";
457
+ else if (a === "--control-socket") {
458
+ const value = argv[++i];
459
+ if (!value || value.startsWith("-")) throw Error("--control-socket requires a path");
460
+ args.controlSocket = resolve(value);
461
+ }
462
462
  else if (a === "--verbose") args.verbose = true;
463
463
  else if (a === "-p" || a === "--print") {
464
464
  args.print = true;
@@ -598,16 +598,17 @@ export function formatStartupHeader(input: {
598
598
  `workspace ${input.workspace}`,
599
599
  `session ${input.session}`,
600
600
  ];
601
- const pushCount = (label: string, items: readonly string[]): void => {
602
- lines.push(`${label.padEnd(10)}${items.length}`);
603
- if (input.verbose) {
601
+ const groups: [string, readonly string[]][] = [["agents", input.agents], ["skills", input.skills], ["prompts", input.prompts],
602
+ ...(input.tools !== undefined ? [["tools", input.tools] as [string, readonly string[]]] : [])];
603
+ if (input.verbose) {
604
+ for (const [label, items] of groups) {
605
+ lines.push(`${label.padEnd(10)}${items.length}`);
604
606
  for (const item of items) lines.push(` ${item}`);
605
607
  }
606
- };
607
- pushCount("agents", input.agents);
608
- pushCount("skills", input.skills);
609
- pushCount("prompts", input.prompts);
610
- if (input.tools !== undefined) pushCount("tools", input.tools);
608
+ } else {
609
+ // Four count rows were a third of the banner; one row says the same.
610
+ lines.push(`loaded ${groups.map(([label, items]) => `${items.length} ${label}`).join(" · ")}`);
611
+ }
611
612
  const keys = formatShortcutLine(input.keymap);
612
613
  if (keys.length > 0) lines.push(`keys ${keys}`);
613
614
  return lines;
@@ -720,7 +721,7 @@ export interface App {
720
721
  blobDir: string;
721
722
  system: string;
722
723
  providerConfig: ProviderConfig;
723
- /** May be `{ ok: false }` in an interactive session waiting on /login. */
724
+ /** May be `{ ok: false }` in an interactive session waiting on /provider. */
724
725
  credential: Resolved;
725
726
  auth: AuthFile;
726
727
  effects: ResolveEffects;
@@ -780,8 +781,8 @@ export function whyNot(spec: ModelSpec, got: Resolved, auth: AuthFile, C: Palett
780
781
  }
781
782
 
782
783
  /**
783
- * /login only exists inside a live session. A one-shot prompt and a pipe
784
- * cannot answer it, so those runs must still refuse -- naming /login there
784
+ * /provider only exists inside a live session. A one-shot prompt and a pipe
785
+ * cannot answer it, so those runs must still refuse -- naming /provider there
785
786
  * is how this file used to deadlock a first run.
786
787
  */
787
788
  /**
@@ -799,7 +800,7 @@ export function isInteractiveSession(args: Args): boolean {
799
800
 
800
801
  /**
801
802
  * Script-facing missing-key text. Environment variables and --api-key are
802
- * the routes that actually work without a tty; /login is not one of them.
803
+ * the routes that actually work without a tty; /provider is not one of them.
803
804
  */
804
805
  export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CATALOG): string {
805
806
  const providers = [...new Set(catalog.map((m) => m.provider))];
@@ -810,13 +811,14 @@ export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CA
810
811
  );
811
812
  }
812
813
 
813
- /** Banner / hint for an interactive session that started without a key. */
814
- export function describeUnauthenticatedSession(catalog: readonly ModelSpec[] = CATALOG): string {
815
- const vars = [...new Set(catalog.map((m) => m.provider))].map((p) => envLabel(p));
816
- return (
817
- `no credential. This session cannot send until one is set. ` +
818
- `Export ${vars.join(" or ")}, or run /login <provider>`
819
- );
814
+ /**
815
+ * Banner / hint for an interactive session that started without a key.
816
+ * Listing every provider's variable was a 20-line wall that buried the one
817
+ * action that works from here; the interactive routes are named instead.
818
+ */
819
+ export function describeUnauthenticatedSession(_catalog: readonly ModelSpec[] = CATALOG): string {
820
+ return "no provider connected yet. Type /provider to connect one — a Claude, ChatGPT, Copilot or Grok account, an API key, a gateway or a local server — " +
821
+ `or export a key such as ${["anthropic", "openai", "google", "deepseek"].map(provider => envLabel(provider).split(" or ")[0]).join(", ")}.`;
820
822
  }
821
823
 
822
824
  export function retryHandler(io: AppIO): (attempt: number, delayMs: number, error: unknown) => void {
@@ -833,6 +835,7 @@ export function retryHandler(io: AppIO): (attempt: number, delayMs: number, erro
833
835
 
834
836
  export function toolSelectionOf(args: Args) {
835
837
  return {
838
+ profile: args.toolProfile ?? "coding",
836
839
  ...(args.noTools ? { none: true as const } : {}),
837
840
  ...(args.tools !== undefined ? { only: args.tools } : {}),
838
841
  ...(args.excludeTools.length > 0 ? { exclude: args.excludeTools } : {}),
@@ -843,25 +846,40 @@ export function toolSelectionOf(args: Args) {
843
846
  /**
844
847
  * Schema assembly: every tool is tagged with a source here so
845
848
  * `--no-builtin-tools` can drop a category without a later cross-layer
846
- * recut. Today the only producer is builtin; an extension would push
847
- * `{ ...schema, source: "extension" }` into this list.
849
+ * recut. Optional extension descriptors remain local until selected; their
850
+ * harness-owned discovery entry belongs to that extension-enabled capability set.
848
851
  *
849
852
  * `source` is stripped before the frozen prefix: the wire schema is what
850
853
  * the provider bills, and an extra field there would look like a tool
851
854
  * argument.
852
855
  */
853
- export function visibleSchemas(args: Args, extensions?: ExtensionRegistry) {
856
+ export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
854
857
  const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
855
- const available = [...TOOL_SCHEMAS.filter(tool => !extensions?.tools.has(tool.name)).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
856
- return selectTools(available, toolSelectionOf(args)).map((tool) => {
857
- const { source, ...schema } = tool;
858
- void source;
859
- return schema;
860
- });
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));
860
+ const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
861
+ .map(({ name, description, input_schema }) => ({ name, description, input_schema }));
862
+ const deferredNames = new Set(deferred.map(tool => tool.name));
863
+ const active = new Set([...declaredToolNames(previous), ...args.tools ?? []]);
864
+ const visible = selected.filter(tool => (!deferredNames.has(tool.name) || active.has(tool.name)) &&
865
+ (tool.name !== DISCOVERY_TOOL_NAME || deferred.length > 0 || args.tools?.includes(DISCOVERY_TOOL_NAME)));
866
+ // A cold open must not reorder an already accepted declaration and invent a checkpoint.
867
+ const order = new Map(declaredToolNames(previous).map((name, index) => [name, index]));
868
+ visible.sort((a, b) => (order.get(a.name) ?? Infinity) - (order.get(b.name) ?? Infinity));
869
+ const builtins = visible.filter(tool => tool.source === "builtin").map(tool => tool.name);
870
+ const schemas = visible.map(tool => tool.source === "builtin" ? modelToolSchema(tool, builtins)
871
+ : { name: tool.name, description: tool.description, input_schema: tool.input_schema });
872
+ // D6: extension authors retain their registration objects and SDK catalog views.
873
+ // Neither may mutate the already frozen provider declaration through an alias.
874
+ return { schemas: JSON.parse(JSON.stringify(schemas)) as typeof schemas,
875
+ deferred: JSON.parse(JSON.stringify(deferred)) as typeof deferred };
876
+ }
877
+ export function visibleSchemas(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
878
+ return sessionToolCatalog(args, extensions, previous).schemas;
861
879
  }
862
880
 
863
881
  export function unknownToolNames(names: readonly string[]): string[] {
864
- const known = new Set(TOOL_SCHEMAS.map((t) => t.name));
882
+ const known = new Set(TOOL_DEFINITIONS.map((t) => t.name));
865
883
  return names.filter((n) => !known.has(n));
866
884
  }
867
885
 
@@ -878,9 +896,11 @@ export function createSessionFor(opts: {
878
896
  oauth?: boolean;
879
897
  effects?: ResolveEffects;
880
898
  extensions?: ExtensionRegistry;
899
+ acceptanceContract?: AcceptanceContract;
900
+ skills?: readonly Skill[];
881
901
  }): Session {
882
902
  const { args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot } = opts;
883
- const schemas = visibleSchemas(args, opts.extensions);
903
+ const { schemas, deferred } = sessionToolCatalog(args, opts.extensions);
884
904
  const names = schemas.map((t) => t.name);
885
905
  const sessionRules: PermissionRule[] = [];
886
906
  const tools: ToolContext = {
@@ -888,6 +908,7 @@ export function createSessionFor(opts: {
888
908
  ...DEFAULT_TOOL_CONTEXT,
889
909
  ...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
890
910
  allow: names,
911
+ deferredTools: deferred,
891
912
  rules: rulesWithAllowTools(args.allowTools),
892
913
  sessionRules,
893
914
  askSlot,
@@ -898,9 +919,12 @@ export function createSessionFor(opts: {
898
919
  logDir,
899
920
  frozen: { system, tools: schemas },
900
921
  blobDir,
922
+ shellCheckpoints: args.shellCheckpoints === true,
901
923
  tools,
902
924
  maxTurns: args.maxTurns,
903
925
  ...cliSessionConfig(args),
926
+ ...(opts.skills ? { skills: opts.skills } : {}),
927
+ ...(opts.acceptanceContract ? { acceptanceContract: opts.acceptanceContract } : {}),
904
928
  // Explicit legacy thresholds still select that experiment unless overridden.
905
929
  contextPolicy: args.contextPolicy ?? (args.trigger > 0 ? "segmented" : DEFAULT_CONTEXT_POLICY),
906
930
  ...(args.trigger > 0
@@ -917,13 +941,13 @@ export function createSessionFor(opts: {
917
941
  : {}),
918
942
  },
919
943
  // Empty key is a placeholder the shell must not send through. Interactive
920
- // startup reaches this path so /login exists; run() refuses until a real
944
+ // startup reaches this path so /provider exists; run() refuses until a real
921
945
  // credential is installed on the session.
922
946
  createProvider(
923
947
  providerConfig,
924
948
  credential.ok ? credential.key : "",
925
949
  retryHandler(io),
926
- { ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
950
+ { sessionId: providerSessionId(logDir), ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
927
951
  ),
928
952
  systemClock(),
929
953
  );
@@ -984,8 +1008,9 @@ export async function loadSessionResources(args: Args, workspace: string, io: Ap
984
1008
  for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
985
1009
  const system = appendSkillsToSystem(assembleSystemPrompt(systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)), loadedContext), text.skills);
986
1010
  const ui = toUiState(loaded, colorEnabled());
1011
+ // Also reached on /resume and /reload: `auto` is resolved once per process by the Tui (apps/shell.ts), not here.
987
1012
  if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
988
- return { loadedContext, slash: { ...slash.catalog, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
1013
+ return { loadedContext, slash: { ...slash.catalog, skillExecution: true, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
989
1014
  }
990
1015
 
991
1016
  /**
@@ -1011,7 +1036,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1011
1036
  : await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
1012
1037
  const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
1013
1038
  const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
1014
- const unknown = unknownToolNames(named).filter(name => !resources.extensions.tools.has(name));
1039
+ const unknown = unknownToolNames(named).filter(name => !resources.extensions.definitions().has(name));
1015
1040
  if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
1016
1041
  const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
1017
1042
  const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
@@ -1054,10 +1079,14 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1054
1079
  const restoring = saved ?? spec;
1055
1080
  const restoringCredential = saved === undefined ? credential : opts.credentialOf(restoring);
1056
1081
  const oauth = (model: ModelSpec, got: Resolved) => got.ok && storedIsOAuth(opts.auth, model.provider) && opts.args.apiKey.length === 0;
1082
+ const acceptanceContract = priorCursor === null
1083
+ ? await loadAcceptanceContract(workspace, resources.trusted, opts.args.acceptanceFile) : undefined;
1057
1084
  const session = createSessionFor({
1058
1085
  ...opts,
1059
1086
  system: resources.system,
1060
1087
  extensions: resources.extensions,
1088
+ skills: [...resources.slash.skills.values()],
1089
+ ...(acceptanceContract ? { acceptanceContract } : {}),
1061
1090
  workspace: priorCursor?.workspace ?? opts.workspace,
1062
1091
  blobDir: priorCursor?.blob_dir ?? (priorCursor?.workspace && priorCursor.workspace !== opts.workspace
1063
1092
  ? statePaths(priorCursor.workspace, opts.args.stateDir).blobs : opts.blobDir),
@@ -1065,15 +1094,16 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1065
1094
  credential: restoringCredential,
1066
1095
  ...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
1067
1096
  });
1068
- opts.startupPhase?.("Preparing workspace snapshot");
1097
+ opts.startupPhase?.("Opening session history");
1069
1098
  await session.start();
1099
+ resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
1070
1100
  if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
1071
1101
  typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
1072
- await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions) });
1102
+ await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions, session.config.frozen.tools) });
1073
1103
  }
1074
1104
  if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
1075
1105
  await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
1076
- { ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
1106
+ { sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
1077
1107
  opts.io.say(opts.io.palette.yellow(` switched ${restoring.id} → ${spec.id} at a model checkpoint`));
1078
1108
  }
1079
1109
  resources.ui.settings = { ...resources.ui.settings, thinking: providerConfig.effort,
@@ -1253,7 +1283,7 @@ export async function startup(args: Args, input?: StartupInput): Promise<App> {
1253
1283
  if (printMode || jsonMode || !stdinIsTty() || !stdoutIsTty()) {
1254
1284
  throw new Error("--resume needs an interactive terminal. Pass -r <id> or --session <id> instead");
1255
1285
  }
1256
- const picked = await withTerminal(handoff => pickStartupSession({ workspace, style: ui.style, say: io.say, keymap: ui.keymap,
1286
+ const picked = await withTerminal(handoff => pickStartupSession({ workspace, theme: ui.theme, say: io.say, keymap: ui.keymap,
1257
1287
  ...(sessionDir === undefined ? {} : { sessionDir }) }, handoff));
1258
1288
  if (picked === null) { io.say(C.dim("No session selected")); process.exit(0); }
1259
1289
  sessionOpts = { explicit: picked };
@@ -1321,6 +1351,21 @@ export async function startup(args: Args, input?: StartupInput): Promise<App> {
1321
1351
  };
1322
1352
  }
1323
1353
 
1354
+ /** The banner's words for the shared limits; the loop acts at exactly these numbers. */
1355
+ export function boundaryDescription(policy: ContextPolicy, limits: ContextLimits): string {
1356
+ switch (limits.kind) {
1357
+ case "stop":
1358
+ case "open":
1359
+ return "continuous replay; stops at model window";
1360
+ case "window":
1361
+ return `windowed replay; prepare ${limits.prepare}, cut ${limits.trigger}, output reserved`;
1362
+ case "segment":
1363
+ return `${policy}${policy === "windowed" ? " (unknown window; fixed fallback)" : ""}; cut ${limits.trigger}, hard ${limits.hard}`;
1364
+ default:
1365
+ return assertNever(limits, "boundaryDescription");
1366
+ }
1367
+ }
1368
+
1324
1369
  function printBanner(
1325
1370
  args: Args,
1326
1371
  io: AppIO,
@@ -1370,13 +1415,14 @@ function printBanner(
1370
1415
  const line = formatNotice(notice);
1371
1416
  io.say(notice.kind === "skipped" ? C.yellow(` ${line}`) : C.dim(` ${line}`));
1372
1417
  }
1373
- const { triggerTokens: trigger, hardTokens: hard } = { ...DEFAULT_BOUNDARY, ...session.config.boundary };
1374
1418
  const policy = session.config.contextPolicy ?? DEFAULT_CONTEXT_POLICY;
1375
- const budget = windowBudget(session.provider.config.spec.contextWindow, session.provider.config.maxTokens);
1376
- const description = policy === "continuous" ? "continuous replay; stops at model window"
1377
- : policy === "windowed" && budget !== null ? `windowed replay; prepare ${budget.prepare}, cut ${budget.trigger}, output reserved`
1378
- : `${policy}${policy === "windowed" ? " (unknown window; fixed fallback)" : ""}; cut ${trigger}, hard ${hard}`;
1379
- io.say(C.dim(`boundary ${description} (estimated tokens)`));
1419
+ const limits = contextLimits({
1420
+ policy,
1421
+ contextWindow: session.provider.config.spec.contextWindow,
1422
+ outputReserve: session.provider.config.maxTokens,
1423
+ ...(session.config.boundary ? { boundary: session.config.boundary } : {}),
1424
+ });
1425
+ io.say(C.dim(`boundary ${boundaryDescription(policy, limits)} (estimated tokens)`));
1380
1426
  if (priorCursor !== null) {
1381
1427
  const events = session.state.events.length;
1382
1428
  io.say(
@@ -1405,6 +1451,9 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1405
1451
  " [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
1406
1452
  " [--no-context-files|-nc]\n" +
1407
1453
  " [--tools names] [--exclude-tools names] [--no-tools]\n" +
1454
+ " [--tool-profile coding|readonly|research|data|full]\n" +
1455
+ " [--acceptance-file contract.json]\n" +
1456
+ " [--control-socket /private/directory/control.sock]\n" +
1408
1457
  " [--no-builtin-tools]\n" +
1409
1458
  " [--allow-tools names] [--approve|--no-approve]\n" +
1410
1459
  " [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
@@ -1413,6 +1462,10 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1413
1462
  " [--extension <path>] [--no-extensions] [--extension-flag name=value]\n" +
1414
1463
  " [--mode text|json] [--tui-mode regular|fullscreen] [--verbose]\n" +
1415
1464
  " [-p <prompt>] [prompt...]\n\n" +
1465
+ " xeno app-server [session/model flags] runs the experimental JSONL control interface.\n" +
1466
+ " xeno app-server schema prints its versioned request schema without startup.\n" +
1467
+ " --control-socket attaches local adapters to the same live session as the TUI.\n" +
1468
+ " /control opens goal, progress and context panels; XENO_PROGRAM_STATUS=auto|1|0 controls terminal status.\n\n" +
1416
1469
  " Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
1417
1470
  " -c continues the newest (or starts one); -r opens the session picker.\n" +
1418
1471
  " --session / -r <id> pick one (unique prefix only). Resume restores its model;\n" +
@@ -1426,10 +1479,12 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1426
1479
  " --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
1427
1480
  " --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
1428
1481
  " --tools is a strict allowlist; excluded tools leave the schema.\n" +
1482
+ " New sessions use coding (10 tools); full exposes default built-ins.\n" +
1429
1483
  " --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
1430
1484
  " bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
1431
1485
  " --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
1432
1486
  " --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
1487
+ " --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +
1433
1488
  " --path-root <dir> lets file tools reach absolute paths under <dir> (e.g. / in a container);\n" +
1434
1489
  " relative paths, fingerprints and /back stay in the workspace, so edits outside it are not rewound.\n" +
1435
1490
  " --approve loads .xeno/settings.json this run; --no-approve skips it.\n" +