@visual-z/xeno 0.8.4 → 0.9.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 (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,111 +0,0 @@
1
- /** O.1: own the process group, preserve partial observations, and drain before returning. */
2
- import { captureCommandObservation } from "./observations.ts";
3
- import { configDir } from "./config.ts";
4
- import { xenoBinDir } from "./tool-binaries.ts";
5
- import { commandArgv, commandEnvironment, COMMAND_TERMINATION_GRACE_MS } from "../core/command-contract.ts";
6
- import { ToolFault, NOT_STARTED, failureDiagnostic } from "../core/tool-failure.ts";
7
-
8
- interface CommandCaptureInput {
9
- cwd: string;
10
- outputStore: string;
11
- maxOutputBytes: number;
12
- timeoutMs: number;
13
- signal?: AbortSignal;
14
- onOutput?: (chunk: Uint8Array) => void;
15
- /** Bounded probes never transfer background children to the caller. */
16
- cleanupOnExit?: boolean;
17
- /** Stop capture after the crossing chunk; command cancellation still owns process cleanup. */
18
- outputLimitBytes?: number;
19
- }
20
-
21
- export async function observeCommand(input: CommandCaptureInput & { command: string }) {
22
- if (input.signal?.aborted) {
23
- const empty = () => new ReadableStream<Uint8Array>({ start: controller => controller.close() });
24
- const captured = await captureCommandObservation(input.outputStore, empty(), empty(), Promise.resolve(130), input.maxOutputBytes);
25
- return { ...captured, wallMs: 0, stopped: "cancelled" as const, cleanup: undefined };
26
- }
27
- return executeCaptured(input, commandArgv(input.command), false);
28
- }
29
-
30
- /** Native host API: argv is never interpreted by a shell and each pipe is recoverable. */
31
- export async function observeArgvCommand(input: CommandCaptureInput & { argv: readonly string[] }) {
32
- if (!Array.isArray(input.argv) || input.argv.length === 0 || !input.argv[0] ||
33
- input.argv.some(value => typeof value !== "string" || value.includes("\0"))) {
34
- throw new ToolFault("invalid_input", "native argv must contain a program and literal string arguments without NUL", NOT_STARTED);
35
- }
36
- if (input.signal?.aborted) throw new ToolFault("cancelled", "native command was cancelled before it started", NOT_STARTED);
37
- const captured = await executeCaptured(input, [...input.argv], true);
38
- if (captured.channels.length !== 2) throw new Error("missing native stdout/stderr artifacts");
39
- return { ...captured, stdout: captured.channels[0]!, stderr: captured.channels[1]! };
40
- }
41
-
42
- async function executeCaptured(input: CommandCaptureInput, argv: string[], native: boolean) {
43
- const started = performance.now();
44
- const proc = (() => {
45
- try { return Bun.spawn(argv, {
46
- cwd: input.cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe", detached: true,
47
- // Preserve the trusted native API's inherited environment; shell tools
48
- // retain their explicit noninteractive policy and login-shell startup.
49
- env: native ? process.env : commandEnvironment(process.env, xenoBinDir(configDir())),
50
- }); } catch (error) {
51
- const os = (error as { code?: string }).code;
52
- throw new ToolFault(os === "ENOENT" || os === "ENOTDIR" ? "environment_missing" : os === "EACCES" || os === "EPERM" ? "permission_denied" : "execution_failed", String(error), NOT_STARTED);
53
- }
54
- })();
55
- let stopped: "cancelled" | "timeout" | undefined;
56
- let escalation: ReturnType<typeof setTimeout> | undefined;
57
- let timer: ReturnType<typeof setTimeout> | undefined;
58
- let exited = false, cleaned = false;
59
- let cleanupError: unknown;
60
- const kill = (signal: "SIGTERM" | "SIGKILL") => {
61
- try {
62
- // POSIX detached children lead their own group; never signal the caller's group.
63
- if (process.platform === "win32") proc.kill(signal);
64
- else process.kill(-proc.pid, signal);
65
- } catch (error) {
66
- if ((error as { code?: string }).code !== "ESRCH") throw error;
67
- }
68
- };
69
- const cleanup = () => {
70
- if (cleaned) return;
71
- cleaned = true;
72
- try { kill("SIGKILL"); } catch (error) { cleanupError = error; }
73
- };
74
- const stop = (reason: "cancelled" | "timeout") => {
75
- if (exited || stopped !== undefined) return;
76
- stopped = reason;
77
- kill("SIGTERM");
78
- escalation = setTimeout(cleanup, COMMAND_TERMINATION_GRACE_MS);
79
- };
80
- const exit = proc.exited.then(code => {
81
- exited = true;
82
- clearTimeout(timer); clearTimeout(escalation);
83
- // Clean once at the observed exit, never after slow archival when a PID
84
- // might have been reused. Legacy successful bash background starts remain.
85
- if (native || input.cleanupOnExit || stopped !== undefined) cleanup();
86
- return code;
87
- });
88
- const abort = () => stop("cancelled");
89
- input.signal?.addEventListener("abort", abort, { once: true });
90
- timer = setTimeout(() => stop("timeout"), input.timeoutMs);
91
- if (input.signal?.aborted) abort();
92
- try {
93
- const captured = await captureCommandObservation(input.outputStore, proc.stdout, proc.stderr,
94
- exit, input.maxOutputBytes, input.onOutput, native, input.outputLimitBytes);
95
- // Parent exit and archived output remain facts even if group cleanup
96
- // cannot be confirmed. This is execution uncertainty, not user denial.
97
- const cleanupDiagnostic = cleanupError === undefined ? undefined : failureDiagnostic("unknown_effect",
98
- `process-group cleanup was not confirmed: ${String(cleanupError)}`, { execution: "started", effects: "possible" });
99
- return { ...captured, wallMs: performance.now() - started, ...(stopped ? { stopped } : {}), cleanup: cleanupDiagnostic };
100
- } catch (error) {
101
- cleanup();
102
- await proc.exited;
103
- throw error;
104
- } finally {
105
- clearTimeout(timer);
106
- clearTimeout(escalation);
107
- input.signal?.removeEventListener("abort", abort);
108
- // A descendant can close stdio yet ignore SIGTERM; completing capture is not proof it stopped.
109
- if (stopped !== undefined) cleanup();
110
- }
111
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * The user's configuration, which lives with the user and not with the project.
3
- *
4
- * `~/.xeno/` rather than the workspace's `.xeno/`. The split is not cosmetic:
5
- * the log and the blob store belong to one piece of work and should travel
6
- * with it, while a credential belongs to a person. A key written into the
7
- * workspace would be committed, would be seen by the snapshot scan that backs
8
- * `/back`, and would ship with the project.
9
- *
10
- * This layer only reads and writes files. It knows nothing about providers,
11
- * priority, or what a key means -- that is `runtime/auth.ts`, and keeping the
12
- * two apart is what lets the priority rules be tested without a filesystem.
13
- */
14
-
15
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
16
- import { homedir } from "node:os";
17
- import { join } from "node:path";
18
-
19
- export function configDir(): string {
20
- // XENO_CONFIG_DIR exists for tests and for anyone running two configurations
21
- // side by side. Read here rather than threaded through every caller, because
22
- // this is the one place that turns "the user" into a path.
23
- return process.env["XENO_CONFIG_DIR"] ?? join(homedir(), ".xeno");
24
- }
25
-
26
- export function authPath(): string {
27
- return join(configDir(), "auth.json");
28
- }
29
-
30
- export function settingsPath(): string {
31
- return join(configDir(), "settings.json");
32
- }
33
-
34
- /**
35
- * Reads a JSON config, treating "absent" and "unreadable" as empty.
36
- *
37
- * A missing config file is the normal first-run state, not an error. A
38
- * corrupt one is reported to the caller as absent plus a reason, because
39
- * silently starting with default settings after someone hand-edited a file
40
- * is how a typo becomes an hour of confusion.
41
- */
42
- export async function readJsonFile<T>(
43
- path: string,
44
- ): Promise<{ value: T | null; error: string | null }> {
45
- let text: string;
46
- try {
47
- text = await readFile(path, "utf8");
48
- } catch {
49
- return { value: null, error: null };
50
- }
51
- try {
52
- return { value: JSON.parse(text) as T, error: null };
53
- } catch (err) {
54
- return { value: null, error: `${path} is not valid JSON: ${(err as Error).message}` };
55
- }
56
- }
57
-
58
- /**
59
- * Writes a config file, 0600 for the one that holds secrets.
60
- *
61
- * `chmod` after the write rather than a `mode` on the open, because an
62
- * existing file keeps its old permissions when reopened -- so a file created
63
- * before this rule existed would stay world-readable and nothing would say so.
64
- */
65
- export async function writeJsonFile(path: string, value: unknown, secret: boolean): Promise<void> {
66
- await mkdir(configDir(), { recursive: true });
67
- await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
68
- if (secret) await chmod(path, 0o600);
69
- }
@@ -1,436 +0,0 @@
1
- /**
2
- * Project context files (AGENTS.md and friends) and the system prompt they
3
- * join.
4
- *
5
- * Two halves, kept apart because they have different reasons to change:
6
- *
7
- * DISCOVERY is I/O. It walks directories, reads bytes, skips what it
8
- * cannot, and truncates what does not fit. It runs once at process
9
- * start.
10
- *
11
- * ASSEMBLY is a pure function of those bytes. Same files in, same
12
- * string out. That is what makes the ordering rules testable without
13
- * a filesystem, and what makes `frozen.system` a value rather than a
14
- * live view of disk.
15
- *
16
- * Re-reading these files at a node boundary would change `system` and
17
- * invalidate the prefix cache for the whole conversation (D6). They are
18
- * frozen at startup; an explicit reload checkpoints a changed prefix.
19
- */
20
-
21
- import { readFile, stat, lstat } from "node:fs/promises";
22
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
23
- import { assertNever } from "../core/exhaustive.ts";
24
- import { blobAddress } from "./blobs.ts";
25
- import { gitBytes } from "./workspace-observation.ts";
26
-
27
- /**
28
- * Per-file cap. Assumed, not measured: a rule file larger than this is a
29
- * document someone should split, not a prompt prefix. Hitting it truncates
30
- * that file and names it in the prompt; the omitted rules are simply not
31
- * followed, which is the failure mode the notice exists to make visible.
32
- */
33
- export const MAX_FILE_BYTES = 64 * 1024;
34
-
35
- /**
36
- * Cap on the sum of loaded context files. Assumed, not measured: 256KB of
37
- * instructions already crowds out the task. Hitting it truncates the file
38
- * that overflowed (and names it); later files may be omitted, so a huge
39
- * farther file can starve a nearer one.
40
- */
41
- export const MAX_TOTAL_BYTES = 256 * 1024;
42
-
43
- /**
44
- * Directories walked from cwd toward the workspace root, including cwd.
45
- * Assumed, not measured: a 32-level nest is not a project layout, it is a
46
- * pathologically generated tree. Hitting it drops the farther (workspace)
47
- * files -- the walk starts at cwd so nearer rules are kept.
48
- */
49
- export const MAX_WALK_DEPTH = 32;
50
-
51
- const AGENTS_CANDIDATES = ["AGENTS.override.md", "AGENTS.md", "CLAUDE.md"] as const;
52
-
53
- export type ContextRole = "system" | "agents" | "append";
54
-
55
- export interface LoadedFile {
56
- path: string;
57
- content: string;
58
- /** Original size in UTF-8 bytes, before any truncation. */
59
- bytes: number;
60
- truncated: boolean;
61
- role: ContextRole;
62
- }
63
-
64
- export type ContextNotice =
65
- | { kind: "loaded"; path: string; truncated: boolean }
66
- | { kind: "skipped"; path: string; reason: string };
67
-
68
- export interface LoadedContext {
69
- /** Replacement for the builtin system prompt, if any. */
70
- system: LoadedFile | null;
71
- /** AGENTS.md chain, far (global) to near (cwd). */
72
- agents: LoadedFile[];
73
- /** APPEND_SYSTEM.md then `--append-system-prompt`, in append order. */
74
- appends: LoadedFile[];
75
- notices: ContextNotice[];
76
- }
77
-
78
- export interface LoadOptions {
79
- workspace: string;
80
- cwd: string;
81
- /** `~/.xeno` or `XENO_CONFIG_DIR`. Injected so tests never touch the real home. */
82
- configDir: string;
83
- noContextFiles: boolean;
84
- appendSystemPrompt: readonly string[];
85
- /** Raw `--system-prompt` value. Omitted when the flag was not passed. */
86
- systemPrompt?: string;
87
- }
88
-
89
- class Budget {
90
- remaining = MAX_TOTAL_BYTES;
91
-
92
- fit(text: string, bytes: number): { content: string; truncated: boolean } {
93
- const cap = Math.min(MAX_FILE_BYTES, this.remaining);
94
- if (bytes <= cap) {
95
- this.remaining -= bytes;
96
- return { content: text, truncated: false };
97
- }
98
- const content = truncateUtf8(text, cap);
99
- this.remaining = Math.max(0, this.remaining - Buffer.byteLength(content, "utf8"));
100
- return { content, truncated: true };
101
- }
102
- }
103
-
104
- /**
105
- * Walk from cwd up to the workspace root, nearest first, then reverse so
106
- * assembly sees farther files first. cwd outside the workspace contributes
107
- * only the workspace itself -- the session is rooted there, and climbing
108
- * into unrelated trees would load someone else's rules.
109
- */
110
- function projectDirectories(workspace: string, cwd: string): string[] {
111
- const root = resolve(workspace);
112
- const start = resolve(cwd);
113
- if (!isInside(start, root)) return [root];
114
-
115
- const fromCwd: string[] = [];
116
- let dir = start;
117
- for (let i = 0; i < MAX_WALK_DEPTH; i++) {
118
- fromCwd.push(dir);
119
- if (dir === root) break;
120
- const parent = dirname(dir);
121
- if (parent === dir) break;
122
- dir = parent;
123
- }
124
- return fromCwd.reverse();
125
- }
126
-
127
- function isInside(child: string, parent: string): boolean {
128
- const rel = relative(parent, child);
129
- return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
130
- }
131
-
132
- function truncateUtf8(text: string, maxBytes: number): string {
133
- if (maxBytes <= 0) return "";
134
- const buf = Buffer.from(text, "utf8");
135
- if (buf.byteLength <= maxBytes) return text;
136
- let end = maxBytes;
137
- // Do not split a multibyte sequence: a dangling continuation byte is
138
- // not valid UTF-8, and the model would see a replacement character in
139
- // the middle of a rule.
140
- while (end > 0 && (buf[end]! & 0xc0) === 0x80) end--;
141
- return buf.subarray(0, end).toString("utf8");
142
- }
143
-
144
- type ReadResult =
145
- | { status: "missing" }
146
- | { status: "ok"; text: string; bytes: number }
147
- | { status: "skip"; reason: string };
148
-
149
- async function tryRead(path: string): Promise<ReadResult> {
150
- let buf: Buffer;
151
- try {
152
- buf = await readFile(path);
153
- } catch (err) {
154
- const code = (err as { code?: string }).code;
155
- if (code === "ENOENT") return { status: "missing" };
156
- if (code === "EACCES") return { status: "skip", reason: "permission denied" };
157
- if (code === "EISDIR") return { status: "skip", reason: "is a directory" };
158
- const message = err instanceof Error ? err.message : String(err);
159
- return { status: "skip", reason: message };
160
- }
161
- try {
162
- const text = new TextDecoder("utf-8", { fatal: true }).decode(buf);
163
- return { status: "ok", text, bytes: buf.byteLength };
164
- } catch {
165
- return { status: "skip", reason: "not valid UTF-8" };
166
- }
167
- }
168
-
169
- function absorbText(
170
- path: string,
171
- text: string,
172
- role: ContextRole,
173
- bytes: number,
174
- budget: Budget,
175
- notices: ContextNotice[],
176
- ): LoadedFile {
177
- const fitted = budget.fit(text, bytes);
178
- const file: LoadedFile = {
179
- path,
180
- content: fitted.content,
181
- bytes,
182
- truncated: fitted.truncated,
183
- role,
184
- };
185
- notices.push({ kind: "loaded", path, truncated: file.truncated });
186
- return file;
187
- }
188
-
189
- async function absorbFile(
190
- path: string,
191
- role: ContextRole,
192
- budget: Budget,
193
- notices: ContextNotice[],
194
- ): Promise<LoadedFile | null> {
195
- const got = await tryRead(path);
196
- if (got.status === "missing") return null;
197
- if (got.status === "skip") {
198
- notices.push({ kind: "skipped", path, reason: got.reason });
199
- return null;
200
- }
201
- return absorbText(path, got.text, role, got.bytes, budget, notices);
202
- }
203
-
204
- /**
205
- * `--system-prompt` is text, or a file if the argument names one that
206
- * exists. An existing path that cannot be read is skipped (and reported)
207
- * rather than silently becoming the path string as the prompt.
208
- */
209
- async function absorbSystemArg(
210
- value: string,
211
- cwd: string,
212
- budget: Budget,
213
- notices: ContextNotice[],
214
- ): Promise<LoadedFile | null> {
215
- const candidate = resolve(cwd, value);
216
- let file = false;
217
- try {
218
- file = (await stat(candidate)).isFile();
219
- } catch {
220
- file = false;
221
- }
222
- if (file) return absorbFile(candidate, "system", budget, notices);
223
- return absorbText("--system-prompt", value, "system", Buffer.byteLength(value, "utf8"), budget, notices);
224
- }
225
-
226
- /**
227
- * One file per directory: override, else AGENTS.md, else CLAUDE.md.
228
- *
229
- * An unreadable chosen file does not fall through to the next name -- the
230
- * directory had a preference, it just could not be honoured. A name that
231
- * is missing or is a directory is not a preference, so the next candidate
232
- * is tried.
233
- */
234
- async function absorbAgentsAt(
235
- dir: string,
236
- budget: Budget,
237
- notices: ContextNotice[],
238
- ): Promise<LoadedFile | null> {
239
- for (const name of AGENTS_CANDIDATES) {
240
- const path = join(dir, name);
241
- const got = await tryRead(path);
242
- if (got.status === "missing") continue;
243
- if (got.status === "skip") {
244
- if (got.reason === "is a directory") continue;
245
- notices.push({ kind: "skipped", path, reason: got.reason });
246
- return null;
247
- }
248
- return absorbText(path, got.text, "agents", got.bytes, budget, notices);
249
- }
250
- return null;
251
- }
252
-
253
- export async function loadContext(opts: LoadOptions): Promise<LoadedContext> {
254
- const budget = new Budget();
255
- const notices: ContextNotice[] = [];
256
- const agents: LoadedFile[] = [];
257
- const appends: LoadedFile[] = [];
258
-
259
- let system: LoadedFile | null = null;
260
- if (opts.systemPrompt !== undefined) {
261
- // The flag is an explicit choice. A missing/unreadable file does not
262
- // fall through to SYSTEM.md -- that would make a typo in the path
263
- // silently restore the project prompt.
264
- system = await absorbSystemArg(opts.systemPrompt, opts.cwd, budget, notices);
265
- } else {
266
- system =
267
- (await absorbFile(join(opts.workspace, ".xeno", "SYSTEM.md"), "system", budget, notices)) ??
268
- (await absorbFile(join(opts.configDir, "SYSTEM.md"), "system", budget, notices));
269
- }
270
-
271
- if (!opts.noContextFiles) {
272
- const globalAgents = await absorbFile(join(opts.configDir, "AGENTS.md"), "agents", budget, notices);
273
- if (globalAgents) agents.push(globalAgents);
274
- for (const dir of projectDirectories(opts.workspace, opts.cwd)) {
275
- const file = await absorbAgentsAt(dir, budget, notices);
276
- if (file) agents.push(file);
277
- }
278
- }
279
-
280
- const globalAppend = await absorbFile(join(opts.configDir, "APPEND_SYSTEM.md"), "append", budget, notices);
281
- if (globalAppend) appends.push(globalAppend);
282
- const projectAppend = await absorbFile(
283
- join(opts.workspace, ".xeno", "APPEND_SYSTEM.md"),
284
- "append",
285
- budget,
286
- notices,
287
- );
288
- if (projectAppend) appends.push(projectAppend);
289
- for (const text of opts.appendSystemPrompt) {
290
- appends.push(
291
- absorbText(
292
- "--append-system-prompt",
293
- text,
294
- "append",
295
- Buffer.byteLength(text, "utf8"),
296
- budget,
297
- notices,
298
- ),
299
- );
300
- }
301
-
302
- return { system, agents, appends, notices };
303
- }
304
-
305
- const CONTEXT_INTRO =
306
- "# Project context\n\n" +
307
- "These files were loaded at startup. Later files are closer to the working " +
308
- "directory and take precedence.";
309
-
310
- function truncationLine(file: LoadedFile): string {
311
- const included = Buffer.byteLength(file.content, "utf8");
312
- const omitted = Math.max(0, file.bytes - included);
313
- return `[truncated: ${file.path} exceeded a size limit; ${omitted} bytes omitted]`;
314
- }
315
-
316
- function escapeAttr(value: string): string {
317
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
318
- }
319
-
320
- function renderFile(file: LoadedFile): string {
321
- const inner = file.truncated ? `${file.content}\n${truncationLine(file)}` : file.content;
322
- return `<context_file path="${escapeAttr(file.path)}">\n${inner}\n</context_file>`;
323
- }
324
-
325
- /**
326
- * File contents in, prompt string out. The path of every loaded file is in
327
- * the result so the model can cite which rule it is following, and a
328
- * truncated file is named so a silent cut cannot hide.
329
- */
330
- export function assembleSystemPrompt(builtin: string, loaded: LoadedContext): string {
331
- const parts: string[] = [];
332
- if (loaded.system !== null) {
333
- parts.push(loaded.system.content);
334
- if (loaded.system.truncated) parts.push(truncationLine(loaded.system));
335
- } else {
336
- parts.push(builtin);
337
- }
338
-
339
- const files = [...loaded.agents, ...loaded.appends];
340
- if (files.length > 0) {
341
- parts.push(CONTEXT_INTRO);
342
- for (const file of files) parts.push(renderFile(file));
343
- }
344
- return parts.join("\n\n");
345
- }
346
-
347
- export function formatNotice(notice: ContextNotice): string {
348
- switch (notice.kind) {
349
- case "loaded":
350
- return notice.truncated
351
- ? `context ${notice.path} (truncated)`
352
- : `context ${notice.path}`;
353
- case "skipped":
354
- return `context skipped ${notice.path} (${notice.reason})`;
355
- default:
356
- return assertNever(notice, "formatNotice");
357
- }
358
- }
359
-
360
- /** Startup uses the actual repository instruction chain; loadContext remains explicit/compatible. */
361
- export async function resolveContextWorkspace(cwd: string): Promise<string> {
362
- try {
363
- const bytes = await gitBytes(cwd, ["--no-pager", "rev-parse", "--show-prefix"]);
364
- const prefix = new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/\n$/, "");
365
- // Git canonicalizes macOS /var to /private/var. Ascending the known prefix
366
- // keeps root and cwd in the same spelling, preserving the instruction chain.
367
- let root = resolve(cwd);
368
- const levels = prefix.split("/").filter(Boolean).length;
369
- for (let level = 0; level < levels; level++) root = dirname(root);
370
- return root;
371
- } catch { return resolve(cwd); }
372
- }
373
-
374
- export interface TargetInstruction extends LoadedFile { scope: string; key: string }
375
- export interface TargetInstructionOptions {
376
- workspace: string;
377
- paths: readonly string[];
378
- /** Digests from frozen startup files and durable instruction events, never model claims. */
379
- known?: ReadonlySet<string>;
380
- noContextFiles?: boolean;
381
- /** Admission runs before instruction bytes are captured. */
382
- canRead?: (path: string) => Promise<boolean>;
383
- }
384
- export const instructionKey = (file: Pick<LoadedFile, "path" | "content">): string =>
385
- `${resolve(file.path)}:${blobAddress(file.content)}`;
386
-
387
- /** Target paths are explicit. No recursive scan, sibling instruction loading or prefix surgery. */
388
- export async function discoverTargetInstructions(options: TargetInstructionOptions): Promise<{ files: TargetInstruction[]; notices: ContextNotice[] }> {
389
- if (options.noContextFiles) return { files: [], notices: [] };
390
- const root = resolve(options.workspace), directories = new Set<string>();
391
- for (const path of options.paths) {
392
- const absolute = resolve(root, path);
393
- // No workspace scope applies outside it, and another project's AGENTS.md
394
- // is not this workspace's authority. Whether the target may be touched at
395
- // all is the path rules' decision; throwing here pre-empted it with an
396
- // internal step's name (outside-path-instructions card).
397
- if (!isInside(absolute, root)) continue;
398
- for (const directory of projectDirectories(root, dirname(absolute))) directories.add(directory);
399
- }
400
- const ordered = [...directories].sort((a, b) => a.split(sep).length - b.split(sep).length || a.localeCompare(b));
401
- const files: TargetInstruction[] = [], notices: ContextNotice[] = [], budget = new Budget();
402
- for (const directory of ordered) {
403
- // A target directory's symlink must not import another project's ambient authority.
404
- let safe = true, current = root;
405
- for (const part of relative(root, directory).split(sep).filter(Boolean)) {
406
- current = join(current, part);
407
- try { const info = await lstat(current); if (info.isSymbolicLink() || !info.isDirectory()) { safe = false; break; } }
408
- catch (error) { if ((error as { code?: string }).code !== "ENOENT") throw error; }
409
- }
410
- if (!safe) { notices.push({ kind: "skipped", path: directory, reason: "instruction scope crosses a symlink or non-directory" }); continue; }
411
- for (const name of AGENTS_CANDIDATES) {
412
- const path = join(directory, name);
413
- try {
414
- const info = await lstat(path);
415
- if (info.isDirectory()) continue;
416
- if (info.isSymbolicLink() || !info.isFile()) { notices.push({ kind: "skipped", path, reason: "instruction file must be ordinary, not a symlink" }); break; }
417
- } catch (error) { if ((error as { code?: string }).code === "ENOENT") continue; throw error; }
418
- if (options.canRead && !await options.canRead(path)) { notices.push({ kind: "skipped", path, reason: "instruction read permission is unavailable" }); break; }
419
- const got = await tryRead(path);
420
- if (got.status !== "ok") { if (got.status === "skip") notices.push({ kind: "skipped", path, reason: got.reason }); break; }
421
- const file = absorbText(path, got.text, "agents", got.bytes, budget, notices), key = instructionKey(file);
422
- if (!options.known?.has(key)) files.push({ ...file, scope: directory, key });
423
- break;
424
- }
425
- }
426
- return { files, notices };
427
- }
428
-
429
- /** Append as a durable in-context event; closer scope overrides only files under that directory. */
430
- export function renderTargetInstructions(files: readonly TargetInstruction[]): string {
431
- if (!files.length) return "";
432
- return "# Instructions discovered for the current file targets\n\n" +
433
- "Apply each file only within its directory scope. Deeper directories take precedence. " +
434
- "This observed version supersedes earlier versions at the same path; other scopes remain unchanged.\n\n" +
435
- files.map(file => `<instruction_scope directory="${escapeAttr(file.scope)}" key="${escapeAttr(file.key)}">\n${renderFile(file)}\n</instruction_scope>`).join("\n\n");
436
- }