@visual-z/xeno 0.2.0 → 0.3.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 (259) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +52 -9
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/cli.ts +10 -11
  8. package/src/apps/commands.ts +38 -5
  9. package/src/apps/completions.ts +73 -19
  10. package/src/apps/image-draft.ts +46 -0
  11. package/src/apps/project-trust.ts +2 -42
  12. package/src/apps/provider-connect.ts +346 -0
  13. package/src/apps/render.ts +22 -47
  14. package/src/apps/resources.ts +5 -2
  15. package/src/apps/shell.ts +298 -1526
  16. package/src/apps/startup-input.ts +130 -0
  17. package/src/apps/startup.ts +143 -115
  18. package/src/apps/ui/app.ts +1383 -0
  19. package/src/apps/ui/extension-host.ts +194 -0
  20. package/src/apps/ui/image-cache.ts +78 -0
  21. package/src/apps/ui/palette.ts +88 -0
  22. package/src/apps/ui/project.ts +127 -0
  23. package/src/apps/ui/standalone.ts +146 -0
  24. package/src/apps/ui/status-model.ts +68 -0
  25. package/src/apps/ui/surface.ts +84 -0
  26. package/src/apps/ui/theme.ts +86 -0
  27. package/src/core/acceptance.ts +107 -0
  28. package/src/core/action-defaults.ts +47 -295
  29. package/src/core/benchmark.ts +2 -2
  30. package/src/core/branch-receipt.ts +24 -0
  31. package/src/core/checkpoints.ts +64 -0
  32. package/src/core/churn.ts +2 -0
  33. package/src/core/command-artifacts.ts +94 -0
  34. package/src/core/command-contract.ts +35 -0
  35. package/src/core/completion-review.ts +21 -5
  36. package/src/core/context-detail.ts +37 -0
  37. package/src/core/context-policy.ts +74 -1
  38. package/src/core/continuation.ts +17 -2
  39. package/src/core/derive.ts +29 -3
  40. package/src/core/draft-images.ts +56 -0
  41. package/src/core/environment.ts +145 -0
  42. package/src/core/evaluation-coverage.ts +74 -0
  43. package/src/core/event-validation.ts +80 -5
  44. package/src/core/events.ts +97 -3
  45. package/src/core/evidence-links.ts +7 -0
  46. package/src/core/evidence.ts +13 -3
  47. package/src/core/execution.ts +8 -2
  48. package/src/core/experiment.ts +133 -0
  49. package/src/core/export.ts +9 -0
  50. package/src/core/failure-recovery.ts +173 -0
  51. package/src/core/file-precondition.ts +72 -0
  52. package/src/core/handoff.ts +65 -4
  53. package/src/core/history-query.ts +38 -2
  54. package/src/core/history.ts +38 -1
  55. package/src/core/integration-health.ts +34 -0
  56. package/src/core/keymap.ts +8 -20
  57. package/src/core/model-picker.ts +64 -0
  58. package/src/core/native-eval.ts +25 -0
  59. package/src/core/note-guidance.ts +34 -0
  60. package/src/core/observation-view.ts +49 -8
  61. package/src/core/permissions.ts +24 -2
  62. package/src/core/process-receipt.ts +105 -0
  63. package/src/core/process-state.ts +37 -0
  64. package/src/core/project.ts +45 -18
  65. package/src/core/prompt-policy.ts +37 -0
  66. package/src/core/query-eval-contract.ts +14 -0
  67. package/src/core/read-observation.ts +82 -0
  68. package/src/core/recovery-corpus.ts +17 -0
  69. package/src/core/recovery-evaluation.ts +60 -0
  70. package/src/core/recovery-sources.ts +74 -0
  71. package/src/core/runtime-target.ts +94 -0
  72. package/src/core/search-observation.ts +97 -0
  73. package/src/core/session-tree.ts +147 -0
  74. package/src/core/settings.ts +26 -10
  75. package/src/core/skills.ts +44 -0
  76. package/src/core/sql-query.ts +211 -0
  77. package/src/core/theme.ts +36 -58
  78. package/src/core/tool-contract.ts +51 -2
  79. package/src/core/tool-discovery.ts +87 -0
  80. package/src/core/tool-failure.ts +60 -0
  81. package/src/core/tool-mode-protocol.ts +25 -0
  82. package/src/core/transcript.ts +19 -4
  83. package/src/core/trial-lifecycle.ts +14 -0
  84. package/src/core/tui-mode.ts +29 -0
  85. package/src/core/update-check.ts +97 -0
  86. package/src/core/version.ts +55 -0
  87. package/src/core/work-state.ts +49 -3
  88. package/src/core/working-set.ts +91 -0
  89. package/src/runtime/auth.ts +2 -2
  90. package/src/runtime/branch-jobs.ts +36 -0
  91. package/src/runtime/clipboard.ts +15 -4
  92. package/src/runtime/command-artifacts.ts +64 -0
  93. package/src/runtime/environment-tools.ts +60 -0
  94. package/src/runtime/evidence-notes.ts +4 -2
  95. package/src/runtime/exec-sessions.ts +206 -61
  96. package/src/runtime/execution-control.ts +1 -0
  97. package/src/runtime/extension-ui.ts +22 -30
  98. package/src/runtime/extensions.ts +95 -16
  99. package/src/runtime/harness-comparison.ts +27 -7
  100. package/src/runtime/harness-eval.ts +460 -0
  101. package/src/runtime/history-cache.ts +57 -0
  102. package/src/runtime/history-tools.ts +62 -12
  103. package/src/runtime/integrations.ts +210 -0
  104. package/src/runtime/loop.ts +1021 -268
  105. package/src/runtime/model-discovery.ts +208 -0
  106. package/src/runtime/native-eval-fixtures.ts +33 -0
  107. package/src/runtime/native-eval.ts +143 -0
  108. package/src/runtime/oauth.ts +1 -1
  109. package/src/runtime/provider-connect.ts +109 -0
  110. package/src/runtime/provider-presets.ts +87 -0
  111. package/src/runtime/query-eval-verifier.ts +28 -0
  112. package/src/runtime/recovery-coverage.ts +71 -0
  113. package/src/runtime/recovery-evidence.ts +76 -0
  114. package/src/runtime/recovery-fixtures.ts +116 -0
  115. package/src/runtime/runtime-target.ts +149 -0
  116. package/src/runtime/tool-broker.ts +137 -0
  117. package/src/runtime/tool-discovery.ts +57 -0
  118. package/src/runtime/tool-mode-eval.ts +146 -0
  119. package/src/runtime/tool-queue.ts +77 -0
  120. package/src/runtime/tools.ts +628 -183
  121. package/src/runtime/trial-control.ts +45 -0
  122. package/src/runtime/update-check.ts +47 -0
  123. package/src/runtime/window-trial.ts +2 -1
  124. package/src/runtime/working-notes.ts +33 -3
  125. package/src/runtime/workspace-lifecycle.ts +97 -0
  126. package/src/store/acceptance.ts +15 -0
  127. package/src/store/checkpoint-history.ts +43 -0
  128. package/src/store/command-artifacts.ts +96 -0
  129. package/src/store/command.ts +65 -37
  130. package/src/store/file-checkpoints.ts +104 -0
  131. package/src/store/file-index.ts +13 -1
  132. package/src/store/git-checkpoints.ts +67 -0
  133. package/src/store/observations.ts +61 -20
  134. package/src/store/process-receipts.ts +34 -0
  135. package/src/store/query-source.ts +13 -8
  136. package/src/store/recovery.ts +2 -3
  137. package/src/store/replay.ts +16 -4
  138. package/src/store/runtime-target.ts +64 -0
  139. package/src/store/search.ts +171 -132
  140. package/src/store/session-archive.ts +4 -0
  141. package/src/store/source-cancellation.ts +6 -0
  142. package/src/store/update-cache.ts +36 -0
  143. package/src/store/workspace-observation.ts +113 -0
  144. package/src/tui/ansi-spans.ts +126 -0
  145. package/src/tui/style.ts +19 -16
  146. package/src/tui/ui/atomic-draft.ts +121 -0
  147. package/src/tui/ui/canvas.ts +96 -0
  148. package/src/tui/ui/colors.ts +70 -0
  149. package/src/tui/ui/composer.ts +199 -0
  150. package/src/tui/ui/dialog.ts +379 -0
  151. package/src/tui/ui/draft.ts +23 -0
  152. package/src/tui/ui/edge.ts +186 -0
  153. package/src/tui/ui/image-size.ts +40 -0
  154. package/src/tui/ui/ink.ts +210 -0
  155. package/src/tui/ui/items.ts +40 -0
  156. package/src/tui/ui/keys.ts +40 -0
  157. package/src/tui/ui/layout.ts +74 -0
  158. package/src/tui/ui/list-model.ts +86 -0
  159. package/src/tui/ui/list-view.ts +172 -0
  160. package/src/tui/ui/mascot.ts +67 -0
  161. package/src/tui/ui/messages.ts +518 -0
  162. package/src/tui/ui/motion.ts +28 -0
  163. package/src/tui/ui/paths.ts +10 -0
  164. package/src/tui/ui/pictures.ts +78 -0
  165. package/src/tui/ui/screen.ts +421 -0
  166. package/src/tui/ui/status.ts +271 -0
  167. package/src/tui/ui/styled.ts +85 -0
  168. package/src/tui/ui/testing.ts +5 -0
  169. package/src/tui/ui/tool-card.ts +150 -0
  170. package/src/apps/extension-ui.ts +0 -330
  171. package/src/apps/resource-browser.ts +0 -75
  172. package/src/apps/session-browser.ts +0 -113
  173. package/src/apps/session-picker.ts +0 -55
  174. package/src/tui/ansi.ts +0 -67
  175. package/src/tui/autocomplete.ts +0 -29
  176. package/src/tui/chat-viewport.ts +0 -47
  177. package/src/tui/complete.ts +0 -87
  178. package/src/tui/editor.ts +0 -940
  179. package/src/tui/host.ts +0 -264
  180. package/src/tui/input.ts +0 -75
  181. package/src/tui/keys.ts +0 -378
  182. package/src/tui/markdown.ts +0 -17
  183. package/src/tui/render.ts +0 -153
  184. package/src/tui/select.ts +0 -258
  185. package/src/tui/settings.ts +0 -44
  186. package/src/tui/statusline.ts +0 -162
  187. package/src/tui/system-theme.ts +0 -700
  188. package/src/tui/terminal.ts +0 -30
  189. package/src/tui/text.ts +0 -52
  190. package/src/tui/toolview.ts +0 -386
  191. package/src/tui/transcript.ts +0 -123
  192. package/src/tui/tree.ts +0 -471
  193. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  194. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  195. package/src/vendor/pi-tui/VENDOR.md +0 -25
  196. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  197. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  198. package/src/vendor/pi-tui/colors.ts +0 -367
  199. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  200. package/src/vendor/pi-tui/components/box.ts +0 -166
  201. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  202. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  203. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  204. package/src/vendor/pi-tui/components/image.ts +0 -127
  205. package/src/vendor/pi-tui/components/input.ts +0 -494
  206. package/src/vendor/pi-tui/components/loader.ts +0 -101
  207. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  208. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  209. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  210. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  211. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  212. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  213. package/src/vendor/pi-tui/components/stack.ts +0 -154
  214. package/src/vendor/pi-tui/components/text.ts +0 -107
  215. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  216. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  217. package/src/vendor/pi-tui/editor-component.ts +0 -74
  218. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  219. package/src/vendor/pi-tui/index.ts +0 -186
  220. package/src/vendor/pi-tui/keybindings.ts +0 -320
  221. package/src/vendor/pi-tui/keys.ts +0 -1401
  222. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  223. package/src/vendor/pi-tui/latex.ts +0 -1506
  224. package/src/vendor/pi-tui/layout-node.ts +0 -51
  225. package/src/vendor/pi-tui/layout.ts +0 -449
  226. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  227. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  228. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  229. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  232. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  233. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  234. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  237. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  238. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  239. package/src/vendor/pi-tui/native/napi.h +0 -91
  240. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  241. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  244. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  245. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  246. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  247. package/src/vendor/pi-tui/native-platform.ts +0 -65
  248. package/src/vendor/pi-tui/oklab.ts +0 -233
  249. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  250. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  251. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  252. package/src/vendor/pi-tui/terminal.ts +0 -554
  253. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  254. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  255. package/src/vendor/pi-tui/tui.ts +0 -1493
  256. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  257. package/src/vendor/pi-tui/utils.ts +0 -1385
  258. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  259. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -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,
@@ -14,6 +15,8 @@ import { Session, systemClock } from "../runtime/loop.ts";
14
15
  import { DEFAULT_PROVIDER, type ProviderConfig } from "../runtime/provider.ts";
15
16
  import { createProvider, 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,69 +42,41 @@ 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";
65
+ import type { StartupInput } from "./startup-input.ts";
60
66
 
61
67
  export const SYSTEM_PROMPT = systemPromptFor(TOOL_SCHEMAS.map((tool) => tool.name));
62
68
 
63
69
  /**
64
- * The instructions mention only tools the model can actually see. Naming a
65
- * tool that was excluded from the schema teaches it to call something that
66
- * 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.
67
72
  */
68
73
  export function systemPromptFor(toolNames: readonly string[], env?: SessionEnvironment): string {
69
- const has = (name: string) => toolNames.includes(name);
70
- const toolsLine = toolNames.length === 0
71
- ? "No tools are available in this session."
72
- : `Tools: ${toolNames.join(", ")}.` +
73
- (toolNames.some(name => ["grep", "find", "ls"].includes(name)) ? " Use the available grep/find/ls tools for confined search." : "") +
74
- (has("bash")
75
- ? " Use bash for verification commands and operations the other tools do not cover."
76
- : "");
77
-
78
- // Each line answers a failure class in the Terminal-Bench taxonomy
79
- // (arXiv 2601.11868): premature termination, no verification, step
80
- // repetition. The wording of the first and last follows the Codex
81
- // prompting guide: persist end to end, and end on a change or a blocker.
82
- const work = [
83
- "How to work:",
84
- "- 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.",
85
- "- Understand before changing: read the relevant code, and for a bug, reproduce it first.",
86
- "- Put independent reads and searches in one response; they run in parallel.",
87
- "- 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.",
88
- "- If the same approach fails twice, change the approach instead of retrying it.",
89
- "- A claim without a passing command is not evidence. Finish with what changed and the command that verified it, or the specific blocker.",
90
- ];
91
- if (has("bash")) work.push(
92
- "- 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. " +
93
- "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.",
94
- );
95
- 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.");
96
- if (has("edit")) work.push("- Prefer edit to rewriting a whole file; copy old_string from read output without the line numbers.");
97
-
98
74
  return [
99
- "You are a coding agent working in a terminal.",
100
- 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.",
101
77
  ...(env === undefined ? [] : [environmentSection(env)]),
102
- work.join("\n"),
103
- workingProtocol(toolNames),
78
+ taskGuidance(),
79
+ codingWorkingProtocol(toolNames),
104
80
  "Be concise. Do not narrate what you are about to do; do it, then say what happened.",
105
81
  ].join("\n\n");
106
82
  }
@@ -127,12 +103,12 @@ export function sessionEnvironment(workspace: string): SessionEnvironment {
127
103
  * Session policy a CLI coding session differs from a benchmark run in.
128
104
  * Benchmarks construct their own SessionConfig and keep the core defaults.
129
105
  */
130
- 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 } {
131
107
  // Never within a request. A real Kimi K3 run with the reminder at reply six
132
108
  // spent half its requests on notes after the work was already verified
133
109
  // (fix-cli-continuation-noise). The reminder still fires where it earns its
134
110
  // place: a resumed handoff that cannot continue, and boundary preparation.
135
- 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 }) };
136
112
  }
137
113
 
138
114
  /**
@@ -150,6 +126,7 @@ export type AskSlot = {
150
126
  };
151
127
 
152
128
  export interface Args {
129
+ shellCheckpoints?: boolean;
153
130
  workspace: string;
154
131
  /**
155
132
  * `--state-dir <dir>`: where every xeno write goes -- snapshots, tool
@@ -192,6 +169,9 @@ export interface Args {
192
169
  systemPrompt: string | null;
193
170
  /** `--tools` allowlist. Omitted when the flag was not passed. */
194
171
  tools?: string[];
172
+ /** New sessions default to coding; restored context keeps its frozen schemas. */
173
+ toolProfile?: ToolProfile;
174
+ acceptanceFile?: string;
195
175
  excludeTools: string[];
196
176
  noTools: boolean;
197
177
  /** `--no-builtin-tools`. False when the flag was not passed. */
@@ -257,6 +237,7 @@ export interface Args {
257
237
  * parses; it just cannot be offered as a suggestion.
258
238
  */
259
239
  const KNOWN_FLAGS: readonly string[] = [
240
+ "--checkpoint-shell",
260
241
  "--workspace", "-w",
261
242
  "--path-root",
262
243
  "--state-dir",
@@ -282,6 +263,8 @@ const KNOWN_FLAGS: readonly string[] = [
282
263
  "--system-prompt",
283
264
  "--append-system-prompt",
284
265
  "--tools", "-t",
266
+ "--tool-profile",
267
+ "--acceptance-file",
285
268
  "--exclude-tools", "-xt",
286
269
  "--no-tools", "-nt",
287
270
  "--no-builtin-tools",
@@ -346,32 +329,6 @@ function rejectUnknownFlag(flag: string): never {
346
329
  process.exit(1);
347
330
  }
348
331
 
349
- /**
350
- * Stop the terminal echoing before startup does anything slow.
351
- *
352
- * Reading auth, scanning context files and printing the header all happen
353
- * before the TUI attaches. In cooked mode the tty echoes whatever is typed
354
- * during that window, so keystrokes land *between* the banner's own lines
355
- * and look like output the program produced. Raw mode is the TUI's anyway;
356
- * taking it at the first instruction just closes the gap.
357
- *
358
- * `stdin` is a parameter so the decision is testable without a real tty.
359
- * Returns an undo so a caller that bails out can put the terminal back.
360
- */
361
- export function silenceTtyEarly(opts: {
362
- interactive: boolean;
363
- stdin?: Pick<NodeJS.ReadStream, "isTTY" | "setRawMode" | "resume">;
364
- }): () => void {
365
- const stdin = opts.stdin ?? process.stdin;
366
- if (!opts.interactive || stdin.isTTY !== true) return () => {};
367
- stdin.setRawMode(true);
368
- // Leave input buffered until ProcessTerminal installs its listener. Resuming
369
- // here drops keystrokes typed while startup or transcript loading awaits I/O.
370
- return () => {
371
- if (stdin.isTTY === true) stdin.setRawMode(false);
372
- };
373
- }
374
-
375
332
  export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
376
333
  let selectedTheme: string | undefined;
377
334
  const args: Args = {
@@ -399,7 +356,8 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
399
356
  console.error(`${a} requires a value`);
400
357
  process.exit(1);
401
358
  }
402
- if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
359
+ if (a === "--checkpoint-shell") args.shellCheckpoints = true;
360
+ else if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
403
361
  else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
404
362
  else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
405
363
  else if (a === "--time-budget") {
@@ -454,6 +412,15 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
454
412
  else if (a === "--system-prompt") args.systemPrompt = argv[++i] ?? "";
455
413
  else if (a === "--append-system-prompt") args.appendSystemPrompt.push(argv[++i] ?? "");
456
414
  else if (a === "--tools" || a === "-t") args.tools = parseToolList(argv[++i] ?? "");
415
+ else if (a === "--acceptance-file") {
416
+ const path = argv[++i]; if (!path || path.startsWith("--")) throw new Error("--acceptance-file requires a JSON path");
417
+ args.acceptanceFile = path;
418
+ }
419
+ else if (a === "--tool-profile") {
420
+ const value = argv[++i] ?? "";
421
+ if (!isToolProfile(value)) throw new Error("unknown tool profile; choose coding, readonly, research, data or full");
422
+ args.toolProfile = value;
423
+ }
457
424
  else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
458
425
  else if (a === "--no-tools" || a === "-nt") args.noTools = true;
459
426
  else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
@@ -623,16 +590,17 @@ export function formatStartupHeader(input: {
623
590
  `workspace ${input.workspace}`,
624
591
  `session ${input.session}`,
625
592
  ];
626
- const pushCount = (label: string, items: readonly string[]): void => {
627
- lines.push(`${label.padEnd(10)}${items.length}`);
628
- if (input.verbose) {
593
+ const groups: [string, readonly string[]][] = [["agents", input.agents], ["skills", input.skills], ["prompts", input.prompts],
594
+ ...(input.tools !== undefined ? [["tools", input.tools] as [string, readonly string[]]] : [])];
595
+ if (input.verbose) {
596
+ for (const [label, items] of groups) {
597
+ lines.push(`${label.padEnd(10)}${items.length}`);
629
598
  for (const item of items) lines.push(` ${item}`);
630
599
  }
631
- };
632
- pushCount("agents", input.agents);
633
- pushCount("skills", input.skills);
634
- pushCount("prompts", input.prompts);
635
- if (input.tools !== undefined) pushCount("tools", input.tools);
600
+ } else {
601
+ // Four count rows were a third of the banner; one row says the same.
602
+ lines.push(`loaded ${groups.map(([label, items]) => `${items.length} ${label}`).join(" · ")}`);
603
+ }
636
604
  const keys = formatShortcutLine(input.keymap);
637
605
  if (keys.length > 0) lines.push(`keys ${keys}`);
638
606
  return lines;
@@ -835,13 +803,14 @@ export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CA
835
803
  );
836
804
  }
837
805
 
838
- /** Banner / hint for an interactive session that started without a key. */
839
- export function describeUnauthenticatedSession(catalog: readonly ModelSpec[] = CATALOG): string {
840
- const vars = [...new Set(catalog.map((m) => m.provider))].map((p) => envLabel(p));
841
- return (
842
- `no credential. This session cannot send until one is set. ` +
843
- `Export ${vars.join(" or ")}, or run /login <provider>`
844
- );
806
+ /**
807
+ * Banner / hint for an interactive session that started without a key.
808
+ * Listing every provider's variable was a 20-line wall that buried the one
809
+ * action that works from here; the interactive routes are named instead.
810
+ */
811
+ export function describeUnauthenticatedSession(_catalog: readonly ModelSpec[] = CATALOG): string {
812
+ return "no provider connected yet. Type /login to connect one — a Claude, ChatGPT or Copilot subscription, an API key, a gateway or a local server — " +
813
+ `or export a key such as ${["anthropic", "openai", "google", "deepseek"].map(provider => envLabel(provider).split(" or ")[0]).join(", ")}.`;
845
814
  }
846
815
 
847
816
  export function retryHandler(io: AppIO): (attempt: number, delayMs: number, error: unknown) => void {
@@ -858,6 +827,7 @@ export function retryHandler(io: AppIO): (attempt: number, delayMs: number, erro
858
827
 
859
828
  export function toolSelectionOf(args: Args) {
860
829
  return {
830
+ profile: args.toolProfile ?? "coding",
861
831
  ...(args.noTools ? { none: true as const } : {}),
862
832
  ...(args.tools !== undefined ? { only: args.tools } : {}),
863
833
  ...(args.excludeTools.length > 0 ? { exclude: args.excludeTools } : {}),
@@ -868,25 +838,40 @@ export function toolSelectionOf(args: Args) {
868
838
  /**
869
839
  * Schema assembly: every tool is tagged with a source here so
870
840
  * `--no-builtin-tools` can drop a category without a later cross-layer
871
- * recut. Today the only producer is builtin; an extension would push
872
- * `{ ...schema, source: "extension" }` into this list.
841
+ * recut. Optional extension descriptors remain local until selected; their
842
+ * harness-owned discovery entry belongs to that extension-enabled capability set.
873
843
  *
874
844
  * `source` is stripped before the frozen prefix: the wire schema is what
875
845
  * the provider bills, and an extra field there would look like a tool
876
846
  * argument.
877
847
  */
878
- export function visibleSchemas(args: Args, extensions?: ExtensionRegistry) {
848
+ export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
879
849
  const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
880
- const available = [...TOOL_SCHEMAS.filter(tool => !extensions?.tools.has(tool.name)).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
881
- return selectTools(available, toolSelectionOf(args)).map((tool) => {
882
- const { source, ...schema } = tool;
883
- void source;
884
- return schema;
885
- });
850
+ 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];
851
+ const selected = selectTools(available, toolSelectionOf(args));
852
+ const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
853
+ .map(({ name, description, input_schema }) => ({ name, description, input_schema }));
854
+ const deferredNames = new Set(deferred.map(tool => tool.name));
855
+ const active = new Set([...declaredToolNames(previous), ...args.tools ?? []]);
856
+ const visible = selected.filter(tool => (!deferredNames.has(tool.name) || active.has(tool.name)) &&
857
+ (tool.name !== DISCOVERY_TOOL_NAME || deferred.length > 0 || args.tools?.includes(DISCOVERY_TOOL_NAME)));
858
+ // A cold open must not reorder an already accepted declaration and invent a checkpoint.
859
+ const order = new Map(declaredToolNames(previous).map((name, index) => [name, index]));
860
+ visible.sort((a, b) => (order.get(a.name) ?? Infinity) - (order.get(b.name) ?? Infinity));
861
+ const builtins = visible.filter(tool => tool.source === "builtin").map(tool => tool.name);
862
+ const schemas = visible.map(tool => tool.source === "builtin" ? modelToolSchema(tool, builtins)
863
+ : { name: tool.name, description: tool.description, input_schema: tool.input_schema });
864
+ // D6: extension authors retain their registration objects and SDK catalog views.
865
+ // Neither may mutate the already frozen provider declaration through an alias.
866
+ return { schemas: JSON.parse(JSON.stringify(schemas)) as typeof schemas,
867
+ deferred: JSON.parse(JSON.stringify(deferred)) as typeof deferred };
868
+ }
869
+ export function visibleSchemas(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
870
+ return sessionToolCatalog(args, extensions, previous).schemas;
886
871
  }
887
872
 
888
873
  export function unknownToolNames(names: readonly string[]): string[] {
889
- const known = new Set(TOOL_SCHEMAS.map((t) => t.name));
874
+ const known = new Set(TOOL_DEFINITIONS.map((t) => t.name));
890
875
  return names.filter((n) => !known.has(n));
891
876
  }
892
877
 
@@ -903,9 +888,11 @@ export function createSessionFor(opts: {
903
888
  oauth?: boolean;
904
889
  effects?: ResolveEffects;
905
890
  extensions?: ExtensionRegistry;
891
+ acceptanceContract?: AcceptanceContract;
892
+ skills?: readonly Skill[];
906
893
  }): Session {
907
894
  const { args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot } = opts;
908
- const schemas = visibleSchemas(args, opts.extensions);
895
+ const { schemas, deferred } = sessionToolCatalog(args, opts.extensions);
909
896
  const names = schemas.map((t) => t.name);
910
897
  const sessionRules: PermissionRule[] = [];
911
898
  const tools: ToolContext = {
@@ -913,6 +900,7 @@ export function createSessionFor(opts: {
913
900
  ...DEFAULT_TOOL_CONTEXT,
914
901
  ...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
915
902
  allow: names,
903
+ deferredTools: deferred,
916
904
  rules: rulesWithAllowTools(args.allowTools),
917
905
  sessionRules,
918
906
  askSlot,
@@ -923,9 +911,12 @@ export function createSessionFor(opts: {
923
911
  logDir,
924
912
  frozen: { system, tools: schemas },
925
913
  blobDir,
914
+ shellCheckpoints: args.shellCheckpoints === true,
926
915
  tools,
927
916
  maxTurns: args.maxTurns,
928
917
  ...cliSessionConfig(args),
918
+ ...(opts.skills ? { skills: opts.skills } : {}),
919
+ ...(opts.acceptanceContract ? { acceptanceContract: opts.acceptanceContract } : {}),
929
920
  // Explicit legacy thresholds still select that experiment unless overridden.
930
921
  contextPolicy: args.contextPolicy ?? (args.trigger > 0 ? "segmented" : DEFAULT_CONTEXT_POLICY),
931
922
  ...(args.trigger > 0
@@ -961,6 +952,7 @@ export function sessionDirFor(args: Args, workspace: string): string | undefined
961
952
  }
962
953
 
963
954
  type OpenSessionOptions = Omit<Parameters<typeof createSessionFor>[0], "providerConfig" | "credential" | "oauth" | "system"> & {
955
+ startupPhase?: (name: string) => void;
964
956
  catalog: readonly ModelSpec[];
965
957
  auth: AuthFile;
966
958
  credentialOf: (spec: ModelSpec) => Resolved;
@@ -1008,8 +1000,9 @@ export async function loadSessionResources(args: Args, workspace: string, io: Ap
1008
1000
  for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
1009
1001
  const system = appendSkillsToSystem(assembleSystemPrompt(systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)), loadedContext), text.skills);
1010
1002
  const ui = toUiState(loaded, colorEnabled());
1003
+ // Also reached on /resume and /reload: `auto` is resolved once per process by the Tui (apps/shell.ts), not here.
1011
1004
  if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
1012
- return { loadedContext, slash: { ...slash.catalog, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
1005
+ return { loadedContext, slash: { ...slash.catalog, skillExecution: true, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
1013
1006
  }
1014
1007
 
1015
1008
  /**
@@ -1021,6 +1014,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1021
1014
  session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
1022
1015
  workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
1023
1016
  }> {
1017
+ opts.startupPhase?.("Reading session history");
1024
1018
  const recorded = await readCursor(opts.logDir);
1025
1019
  const priorCursor = recorded === null ? null : await recoverCut(opts.logDir, recorded);
1026
1020
  const workspace = priorCursor?.workspace ?? opts.workspace;
@@ -1029,11 +1023,12 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1029
1023
  if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
1030
1024
  }
1031
1025
  if (opts.preparedResources && opts.preparedResources.workspace !== workspace) await opts.preparedResources.value.extensions.dispose();
1026
+ opts.startupPhase?.("Loading session resources");
1032
1027
  const resources = opts.preparedResources?.workspace === workspace ? opts.preparedResources.value
1033
1028
  : await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
1034
1029
  const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
1035
1030
  const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
1036
- const unknown = unknownToolNames(named).filter(name => !resources.extensions.tools.has(name));
1031
+ const unknown = unknownToolNames(named).filter(name => !resources.extensions.definitions().has(name));
1037
1032
  if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
1038
1033
  const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
1039
1034
  const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
@@ -1042,6 +1037,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1042
1037
  const explicit = flags.model.length > 0 || flags.provider.length > 0;
1043
1038
  const settings = opts.settings ?? {};
1044
1039
  const preferred = saved ?? opts.fallback?.spec;
1040
+ opts.startupPhase?.("Resolving model credentials");
1045
1041
  const selected = !explicit && preferred !== undefined
1046
1042
  ? { ok: true as const, spec: preferred }
1047
1043
  : selectModel(settings, flags, (spec) => opts.credentialOf(spec).ok, opts.catalog);
@@ -1075,10 +1071,14 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1075
1071
  const restoring = saved ?? spec;
1076
1072
  const restoringCredential = saved === undefined ? credential : opts.credentialOf(restoring);
1077
1073
  const oauth = (model: ModelSpec, got: Resolved) => got.ok && storedIsOAuth(opts.auth, model.provider) && opts.args.apiKey.length === 0;
1074
+ const acceptanceContract = priorCursor === null
1075
+ ? await loadAcceptanceContract(workspace, resources.trusted, opts.args.acceptanceFile) : undefined;
1078
1076
  const session = createSessionFor({
1079
1077
  ...opts,
1080
1078
  system: resources.system,
1081
1079
  extensions: resources.extensions,
1080
+ skills: [...resources.slash.skills.values()],
1081
+ ...(acceptanceContract ? { acceptanceContract } : {}),
1082
1082
  workspace: priorCursor?.workspace ?? opts.workspace,
1083
1083
  blobDir: priorCursor?.blob_dir ?? (priorCursor?.workspace && priorCursor.workspace !== opts.workspace
1084
1084
  ? statePaths(priorCursor.workspace, opts.args.stateDir).blobs : opts.blobDir),
@@ -1086,10 +1086,12 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1086
1086
  credential: restoringCredential,
1087
1087
  ...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
1088
1088
  });
1089
+ opts.startupPhase?.("Opening session history");
1089
1090
  await session.start();
1091
+ resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
1090
1092
  if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
1091
1093
  typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
1092
- await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions) });
1094
+ await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions, session.config.frozen.tools) });
1093
1095
  }
1094
1096
  if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
1095
1097
  await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
@@ -1103,7 +1105,8 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1103
1105
  blobDir: session.config.blobDir ?? opts.blobDir, system: session.config.frozen.system, resources };
1104
1106
  }
1105
1107
 
1106
- export async function startup(args: Args): Promise<App> {
1108
+ export async function startup(args: Args, input?: StartupInput): Promise<App> {
1109
+ const withTerminal = <T>(run: (handoff: () => void) => Promise<T>): Promise<T> => input ? input.runWithTerminal(run) : run(() => {});
1107
1110
  if (args.extensionArgv && (args.listModels || args.modelsCommand || args.configCommand)) rejectUnknownFlag("--" + Object.keys(args.extensionInputs ?? {})[0]);
1108
1111
  const C = createPalette(colorEnabled());
1109
1112
  let printMode = isPrintMode(args);
@@ -1158,6 +1161,7 @@ export async function startup(args: Args): Promise<App> {
1158
1161
  }
1159
1162
 
1160
1163
  // File only. Passing fetch here would make the test of D16 un-runnable.
1164
+ input?.phase("Loading model catalog");
1161
1165
  const loaded = loadCatalog(await readModelSources());
1162
1166
  for (const warning of loaded.warnings) io.say(C.yellow(` ${warning}`));
1163
1167
  const catalog = loaded.models;
@@ -1187,6 +1191,7 @@ export async function startup(args: Args): Promise<App> {
1187
1191
  * It has to happen before model selection so a trusted `.xeno/settings.json`
1188
1192
  * can override the default model. Trust does not affect tools.
1189
1193
  */
1194
+ input?.phase("Opening workspace");
1190
1195
  let workspace: string;
1191
1196
  try {
1192
1197
  workspace = await workspaceRoot(args.workspace);
@@ -1206,6 +1211,7 @@ export async function startup(args: Args): Promise<App> {
1206
1211
  }
1207
1212
  }
1208
1213
 
1214
+ input?.phase("Preparing extensions and project settings");
1209
1215
  await prepareExtensionPackages(args,workspace);
1210
1216
  const trust = await projectTrust(workspace, args.approve);
1211
1217
  let loadProjectSettings = trust.trusted;
@@ -1213,11 +1219,11 @@ export async function startup(args: Args): Promise<App> {
1213
1219
  if (!stdinIsTty() || !stdoutIsTty()) {
1214
1220
  const index = await discoverResources({ ...args, workspace, trusted: loadProjectSettings });
1215
1221
  for (const entry of index.entries) io.say(`${entry.enabled ? "[x]" : "[ ]"} ${entry.kind} ${entry.scope}${entry.package?" · "+entry.package.source:""} ${entry.path}`);
1216
- } else await runResourceConfig({ ...args, workspace, trusted: loadProjectSettings });
1222
+ } else await withTerminal(handoff => runResourceConfig({ ...args, workspace, trusted: loadProjectSettings }, handoff));
1217
1223
  process.exit(0);
1218
1224
  }
1219
1225
  const hasProjectResources = (await Promise.all(["settings.json", "keybindings.json", "skills", "prompts", "themes", "extensions"].map(name => stat(join(workspace, ".xeno", name)).then(() => true, () => false)))).some(Boolean);
1220
- if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await pickProjectTrust(workspace);
1226
+ if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await withTerminal(handoff => pickProjectTrust(workspace, handoff));
1221
1227
  if (hasProjectResources && !loadProjectSettings) io.say(C.dim(" Project resources not loaded (project is not trusted). /trust changes resource trust; tool permissions are separate."));
1222
1228
 
1223
1229
  // Resolve the resource owner before factories run. Parsing flags must not load a different cwd first.
@@ -1231,6 +1237,7 @@ export async function startup(args: Args): Promise<App> {
1231
1237
  else if (args.continueLatest) resourceLog = (await listSessions(workspace, sessionDirFor(args, workspace), Infinity)).sessions.find(session => session.ok)?.path;
1232
1238
  if (resourceLog) resourceWorkspace = (await readCursor(resourceLog))?.workspace ?? workspace;
1233
1239
  }
1240
+ input?.phase("Loading session resources");
1234
1241
  const preparedResources = prepareFlags
1235
1242
  ? { workspace: resourceWorkspace, value: await loadSessionResources(args, resourceWorkspace, io, resourceWorkspace === workspace ? loadProjectSettings : undefined) } : undefined;
1236
1243
  if (args.help) { printCliHelp(preparedResources?.value.extensions.flags); await preparedResources?.value.extensions.dispose(); process.exit(0); }
@@ -1268,8 +1275,8 @@ export async function startup(args: Args): Promise<App> {
1268
1275
  if (printMode || jsonMode || !stdinIsTty() || !stdoutIsTty()) {
1269
1276
  throw new Error("--resume needs an interactive terminal. Pass -r <id> or --session <id> instead");
1270
1277
  }
1271
- const picked = await pickStartupSession({ workspace, style: ui.style, say: io.say, keymap: ui.keymap,
1272
- ...(sessionDir === undefined ? {} : { sessionDir }) });
1278
+ const picked = await withTerminal(handoff => pickStartupSession({ workspace, theme: ui.theme, say: io.say, keymap: ui.keymap,
1279
+ ...(sessionDir === undefined ? {} : { sessionDir }) }, handoff));
1273
1280
  if (picked === null) { io.say(C.dim("No session selected")); process.exit(0); }
1274
1281
  sessionOpts = { explicit: picked };
1275
1282
  }
@@ -1301,6 +1308,7 @@ export async function startup(args: Args): Promise<App> {
1301
1308
  settings: mergedSettings, modelFlags: { model: args.model, provider: args.provider }, projectTrusted: loadProjectSettings,
1302
1309
  ...(preparedResources ? { preparedResources } : {}),
1303
1310
  interactive: isInteractiveSession(args),
1311
+ ...(input ? { startupPhase: input.phase } : {}),
1304
1312
  });
1305
1313
  } catch (err) {
1306
1314
  const detail = err instanceof Error ? err.message : String(err);
@@ -1335,6 +1343,21 @@ export async function startup(args: Args): Promise<App> {
1335
1343
  };
1336
1344
  }
1337
1345
 
1346
+ /** The banner's words for the shared limits; the loop acts at exactly these numbers. */
1347
+ export function boundaryDescription(policy: ContextPolicy, limits: ContextLimits): string {
1348
+ switch (limits.kind) {
1349
+ case "stop":
1350
+ case "open":
1351
+ return "continuous replay; stops at model window";
1352
+ case "window":
1353
+ return `windowed replay; prepare ${limits.prepare}, cut ${limits.trigger}, output reserved`;
1354
+ case "segment":
1355
+ return `${policy}${policy === "windowed" ? " (unknown window; fixed fallback)" : ""}; cut ${limits.trigger}, hard ${limits.hard}`;
1356
+ default:
1357
+ return assertNever(limits, "boundaryDescription");
1358
+ }
1359
+ }
1360
+
1338
1361
  function printBanner(
1339
1362
  args: Args,
1340
1363
  io: AppIO,
@@ -1384,13 +1407,14 @@ function printBanner(
1384
1407
  const line = formatNotice(notice);
1385
1408
  io.say(notice.kind === "skipped" ? C.yellow(` ${line}`) : C.dim(` ${line}`));
1386
1409
  }
1387
- const { triggerTokens: trigger, hardTokens: hard } = { ...DEFAULT_BOUNDARY, ...session.config.boundary };
1388
1410
  const policy = session.config.contextPolicy ?? DEFAULT_CONTEXT_POLICY;
1389
- const budget = windowBudget(session.provider.config.spec.contextWindow, session.provider.config.maxTokens);
1390
- const description = policy === "continuous" ? "continuous replay; stops at model window"
1391
- : policy === "windowed" && budget !== null ? `windowed replay; prepare ${budget.prepare}, cut ${budget.trigger}, output reserved`
1392
- : `${policy}${policy === "windowed" ? " (unknown window; fixed fallback)" : ""}; cut ${trigger}, hard ${hard}`;
1393
- io.say(C.dim(`boundary ${description} (estimated tokens)`));
1411
+ const limits = contextLimits({
1412
+ policy,
1413
+ contextWindow: session.provider.config.spec.contextWindow,
1414
+ outputReserve: session.provider.config.maxTokens,
1415
+ ...(session.config.boundary ? { boundary: session.config.boundary } : {}),
1416
+ });
1417
+ io.say(C.dim(`boundary ${boundaryDescription(policy, limits)} (estimated tokens)`));
1394
1418
  if (priorCursor !== null) {
1395
1419
  const events = session.state.events.length;
1396
1420
  io.say(
@@ -1419,6 +1443,8 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1419
1443
  " [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
1420
1444
  " [--no-context-files|-nc]\n" +
1421
1445
  " [--tools names] [--exclude-tools names] [--no-tools]\n" +
1446
+ " [--tool-profile coding|readonly|research|data|full]\n" +
1447
+ " [--acceptance-file contract.json]\n" +
1422
1448
  " [--no-builtin-tools]\n" +
1423
1449
  " [--allow-tools names] [--approve|--no-approve]\n" +
1424
1450
  " [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
@@ -1440,10 +1466,12 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1440
1466
  " --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
1441
1467
  " --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
1442
1468
  " --tools is a strict allowlist; excluded tools leave the schema.\n" +
1469
+ " New sessions use coding (10 tools); full exposes default built-ins.\n" +
1443
1470
  " --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
1444
1471
  " bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
1445
1472
  " --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
1446
1473
  " --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
1474
+ " --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +
1447
1475
  " --path-root <dir> lets file tools reach absolute paths under <dir> (e.g. / in a container);\n" +
1448
1476
  " relative paths, fingerprints and /back stay in the workspace, so edits outside it are not rewound.\n" +
1449
1477
  " --approve loads .xeno/settings.json this run; --no-approve skips it.\n" +