@visual-z/xeno 0.8.4 → 0.9.1

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 +19 -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,410 +0,0 @@
1
- /**
2
- * Pure permission rules: which tools the model may see, and whether a
3
- * call is allow / ask / deny.
4
- *
5
- * Approval is the last look at what is about to run, not a boundary.
6
- * Path tools are already confined to the workspace root; host commands
7
- * are not, which is why shell entry points default to ask.
8
- *
9
- * This module must stay free of I/O, clocks, and randomness. The CLI
10
- * compiles flags and session memory into a rule list; this file only
11
- * matches them. There is no command-danger heuristic: a screen that
12
- * misses `find . -delete` is worse than none, because it implies
13
- * someone is checking.
14
- */
15
-
16
- export type PermissionAction = "allow" | "ask" | "deny";
17
-
18
- export interface PermissionRequest {
19
- readonly tool: string;
20
- /** Exact command string for bash; omitted for tools that have none. */
21
- readonly command?: string;
22
- /** Normalized workspace-relative or external absolute file resources. */
23
- readonly paths?: readonly string[];
24
- }
25
-
26
- export interface PermissionRule {
27
- readonly action: PermissionAction;
28
- /** Absent matches every tool. */
29
- readonly tool?: string;
30
- /** Absent matches every command. When set, matches only this exact string. */
31
- readonly command?: string;
32
- /** Literal argv prefix, matched only for a single command without expansion. */
33
- readonly commandPrefix?: readonly string[];
34
- readonly path?: string;
35
- /** Portable path glob: *, ?, and **; no brace or character-class expansion. */
36
- readonly pathGlob?: string;
37
- }
38
-
39
- /**
40
- * Who contributed the tool. `builtin` is the only producer today.
41
- * `extension` is in the union so `--no-builtin-tools` can keep a
42
- * non-builtin tool without a later schema change across layers.
43
- */
44
- export type ToolSource = "builtin" | "extension";
45
-
46
- /** Task capabilities are frozen once at session creation (O.7); not permissions. */
47
- export const TOOL_PROFILES = {
48
- // worktree joined the measured ten (2026-10-11, feat-worktree-tool): without
49
- // it a model needing isolation had no checkout it was allowed to edit.
50
- coding: ["read", "grep", "find", "write", "edit", "environment", "exec_command", "write_stdin", "history", "note", "worktree"],
51
- foundation: ["read", "grep", "find", "write", "edit", "environment", "exec_command", "write_stdin", "history", "note", "worktree", "question", "apply_patch", "diff", "webfetch", "websearch", "format", "lsp"],
52
- readonly: ["read", "grep", "find", "history", "note"],
53
- research: ["read", "grep", "find", "history", "note", "branch", "resolve"],
54
- data: ["read", "grep", "find", "history", "note", "query"],
55
- full: null,
56
- } as const;
57
- export type ToolProfile = keyof typeof TOOL_PROFILES;
58
- export function isToolProfile(value: string): value is ToolProfile {
59
- return Object.hasOwn(TOOL_PROFILES, value);
60
- }
61
-
62
- export interface ToolSelection {
63
- /** Applies only to builtin defaults; explicit only and extension capabilities win. */
64
- readonly profile?: ToolProfile;
65
- /** `--tools`: a strict allowlist. */
66
- readonly only?: readonly string[];
67
- /** `--exclude-tools`: removed after the allowlist (or the full set) is built. */
68
- readonly exclude?: readonly string[];
69
- /** `--no-tools`: start from nothing. Ignored when `only` is set. */
70
- readonly none?: boolean;
71
- /**
72
- * `--no-builtin-tools`. Drops `source === "builtin"` (and untagged tools,
73
- * which are builtin). Ignored when `only` is set: an explicit `--tools`
74
- * names a specific tool and is a stronger statement than "not that category".
75
- */
76
- readonly noBuiltin?: boolean;
77
- }
78
-
79
- /**
80
- * The flag a no-tty refusal names so a pipeline can choose explicitly.
81
- *
82
- * Session memory is not this flag: session memory dies with the process.
83
- * A persisted "always allow" would fire months later when nobody is watching.
84
- */
85
- export const ALLOW_TOOLS_FLAG = "--allow-tools";
86
-
87
- /**
88
- * Product default. Shell entry points ask because their effects are not
89
- * confined to the workspace root; path tools keep their own confinement.
90
- */
91
- export function defaultRules(): readonly PermissionRule[] {
92
- return [
93
- // Templates are ordinary source; actual environment values deserve the
94
- // same last look as commands. Indirect readers also check read resources.
95
- { action: "allow", tool: "read", pathGlob: "**/.env.example" },
96
- { action: "ask", tool: "read", pathGlob: "**/.env" },
97
- { action: "ask", tool: "read", pathGlob: "**/.env.*" },
98
- { action: "ask", tool: "bash" },
99
- // Same reach as bash. write_stdin needs no rule of its own: it can only
100
- // address a session exec_command already started, under this approval.
101
- { action: "ask", tool: "exec_command" },
102
- { action: "ask", tool: "environment" },
103
- { action: "ask", tool: "target" },
104
- { action: "allow" },
105
- ];
106
- }
107
-
108
- /**
109
- * CLI opt-ins compiled to one process-scoped rule list. YOLO permits every
110
- * tool; capability admission and path confinement still apply (D13).
111
- * Neither this flag nor remembered approvals become a persisted policy.
112
- */
113
- export function rulesWithAllowTools(
114
- allowTools: readonly string[],
115
- options: { readonly yolo?: boolean } = {},
116
- ): PermissionRule[] {
117
- if (options.yolo === true) return [{ action: "allow" }];
118
- return [
119
- ...allowTools.map((tool) => ({ action: "allow" as const, tool })),
120
- ...defaultRules(),
121
- ];
122
- }
123
-
124
- export function ruleMatches(rule: PermissionRule, request: PermissionRequest): boolean {
125
- if (rule.tool !== undefined && rule.tool !== request.tool) return false;
126
- if (rule.command !== undefined && rule.command !== request.command) return false;
127
- if (rule.commandPrefix !== undefined) {
128
- const words = request.command === undefined ? null : literalCommandWords(request.command);
129
- if (!words || rule.commandPrefix.length === 0 || rule.commandPrefix.some((word, i) => words[i] !== word)) return false;
130
- }
131
- if (rule.path !== undefined && !request.paths?.some(path => normalizePermissionPath(path) === normalizePermissionPath(rule.path!))) return false;
132
- if (rule.pathGlob !== undefined && !request.paths?.some(path => permissionPathMatches(rule.pathGlob!, path))) return false;
133
- return true;
134
- }
135
-
136
- /**
137
- * First matching rule wins. An empty list allows: the tool tests pass no
138
- * rules because they are testing the tool, not the gate. The CLI always
139
- * supplies the current process's compiled policy.
140
- */
141
- export function decide(
142
- rules: readonly PermissionRule[],
143
- request: PermissionRequest,
144
- ): PermissionAction {
145
- // A multi-file call needs every resource admitted. A broad allow for its
146
- // first path must not conceal a denial on a later file (D13 / R13).
147
- if (request.paths && request.paths.length > 1) {
148
- const actions = request.paths.map(path => decide(rules, { ...request, paths: [path] }));
149
- return actions.includes("deny") ? "deny" : actions.includes("ask") ? "ask" : "allow";
150
- }
151
- for (const rule of rules) {
152
- if (ruleMatches(rule, request)) return rule.action;
153
- }
154
- return "allow";
155
- }
156
-
157
- /** Configured denial is a ceiling; in-memory approval can only resolve asks. */
158
- export function decideWithSessionRules(
159
- configured: readonly PermissionRule[], remembered: readonly PermissionRule[], request: PermissionRequest,
160
- ): PermissionAction {
161
- if (decide(configured, request) === "deny") return "deny";
162
- return decide([...remembered, ...configured], request);
163
- }
164
-
165
- /**
166
- * Where a gate outcome came from, for the durable approval record
167
- * (feat-approval-events). `auto` is an allow by a rule with no selector --
168
- * what `--yolo` compiles to -- so "no approval was required" is derived from
169
- * the rule list itself rather than from a side flag. `none` means no rule
170
- * matched and the empty-list default allowed: no policy was consulted.
171
- */
172
- export type DecisionSource = "session_rule" | "config" | "auto" | "none";
173
- export interface DecisionExplanation {
174
- readonly action: PermissionAction;
175
- readonly source: DecisionSource;
176
- readonly rule?: PermissionRule;
177
- }
178
-
179
- /** Mirrors decide() exactly, but keeps the index of the rule that produced the action. */
180
- function explainList(rules: readonly PermissionRule[], request: PermissionRequest): { action: PermissionAction; index?: number } {
181
- if (request.paths && request.paths.length > 1) {
182
- const each = request.paths.map(path => explainList(rules, { ...request, paths: [path] }));
183
- const actions = each.map(item => item.action);
184
- const action: PermissionAction = actions.includes("deny") ? "deny" : actions.includes("ask") ? "ask" : "allow";
185
- return each.find(item => item.action === action)!;
186
- }
187
- const index = rules.findIndex(rule => ruleMatches(rule, request));
188
- return index < 0 ? { action: "allow" } : { action: rules[index]!.action, index };
189
- }
190
-
191
- /** decideWithSessionRules plus the rule that decided; the action is always identical (property-tested). */
192
- export function explainDecision(
193
- configured: readonly PermissionRule[], remembered: readonly PermissionRule[], request: PermissionRequest,
194
- ): DecisionExplanation {
195
- const ceiling = explainList(configured, request);
196
- if (ceiling.action === "deny") return { action: "deny", source: "config", rule: configured[ceiling.index!]! };
197
- const combined = [...remembered, ...configured];
198
- const found = explainList(combined, request);
199
- if (found.index === undefined) return { action: found.action, source: "none" };
200
- const rule = combined[found.index]!;
201
- const unconditional = rule.tool === undefined && rule.command === undefined && rule.commandPrefix === undefined &&
202
- rule.path === undefined && rule.pathGlob === undefined;
203
- return { action: found.action, rule,
204
- source: found.index < remembered.length ? "session_rule" : unconditional && rule.action === "allow" ? "auto" : "config" };
205
- }
206
-
207
- /**
208
- * A call needs a decision when the product default would not silently allow
209
- * it, or when the effective policy did not. Calls outside both -- confined
210
- * reads and edits under the default -- had nobody's judgement to record.
211
- */
212
- export function approvalNeeded(request: PermissionRequest, effective: PermissionAction): boolean {
213
- return effective !== "allow" || decide(defaultRules(), request) !== "allow";
214
- }
215
-
216
- /** Stable one-line rule text for the log and the export; field order is fixed. */
217
- export function describeRule(rule: PermissionRule): string {
218
- return [rule.action, rule.tool ?? "*",
219
- ...(rule.command !== undefined ? [`command=${JSON.stringify(rule.command)}`] : []),
220
- ...(rule.commandPrefix !== undefined ? [`commandPrefix=${JSON.stringify(rule.commandPrefix.join(" "))}`] : []),
221
- ...(rule.path !== undefined ? [`path=${JSON.stringify(rule.path)}`] : []),
222
- ...(rule.pathGlob !== undefined ? [`pathGlob=${JSON.stringify(rule.pathGlob)}`] : [])].join(" ");
223
- }
224
-
225
- /** Lexical normalization is pure; the store adds canonical symlink resources. */
226
- export function normalizePermissionPath(path: string): string {
227
- const absolute = path.startsWith("/");
228
- const parts: string[] = [];
229
- for (const part of path.split("/")) {
230
- if (!part || part === ".") continue;
231
- if (part === ".." && parts.length > 0 && parts.at(-1) !== "..") parts.pop();
232
- else if (part === ".." && absolute) continue;
233
- else parts.push(part);
234
- }
235
- return (absolute ? "/" : "") + parts.join("/") || ".";
236
- }
237
-
238
- /** Dynamic programming avoids exponential regexp backtracking on model input. */
239
- export function permissionPathMatches(pattern: string, path: string): boolean {
240
- const glob = normalizePermissionPath(pattern), value = normalizePermissionPath(path);
241
- let row = Array<boolean>(value.length + 1).fill(false); row[0] = true;
242
- for (let i = 0; i < glob.length; i++) {
243
- const char = glob[i]!;
244
- const next = Array<boolean>(value.length + 1).fill(false);
245
- if (char === "*") {
246
- const deep = glob[i + 1] === "*";
247
- if (deep) i++;
248
- // **/ also matches zero directories, as in **/.env at the root.
249
- const directories = deep && glob[i + 1] === "/";
250
- if (directories) i++;
251
- next[0] = row[0]!;
252
- for (let j = 1; j <= value.length; j++) {
253
- next[j] = row[j]! || ((deep || value[j - 1] !== "/") && next[j - 1]!);
254
- if (directories && value[j - 1] !== "/") next[j] = row[j]!;
255
- }
256
- // A globstar followed by slash may consume complete directories only.
257
- if (directories) {
258
- let reachable = row[0]!;
259
- for (let j = 1; j <= value.length; j++) {
260
- reachable = reachable || row[j]!;
261
- next[j] = row[j]! || (value[j - 1] === "/" && reachable);
262
- }
263
- }
264
- } else {
265
- for (let j = 1; j <= value.length; j++) next[j] = row[j - 1]! &&
266
- (char === "?" ? value[j - 1] !== "/" : char === value[j - 1]);
267
- }
268
- row = next;
269
- }
270
- return row[value.length]!;
271
- }
272
-
273
- /**
274
- * This is a prefix matcher, never a command-danger classifier. Unknown shell
275
- * syntax fails to match and therefore retains the full-command ask (D13).
276
- */
277
- export function literalCommandWords(command: string): string[] | null {
278
- const words: string[] = [];
279
- let word = "", started = false, quote: "'" | '"' | null = null;
280
- for (let i = 0; i < command.length; i++) {
281
- const char = command[i]!;
282
- if (char === "\n" || char === "\r" || char === "\0") return null;
283
- if (quote === "'") {
284
- if (char === "'") quote = null; else word += char;
285
- continue;
286
- }
287
- if (char === "\\") {
288
- const escaped = command[++i];
289
- if (escaped === undefined || escaped === "\n" || escaped === "\r") return null;
290
- // Bash double quotes retain backslash before ordinary characters.
291
- word += quote === '"' && !['$', '`', '"', "\\"].includes(escaped) ? `\\${escaped}` : escaped;
292
- started = true; continue;
293
- }
294
- if (quote === '"') {
295
- if (char === '"') quote = null;
296
- else if (char === "$" || char === "`") return null;
297
- else word += char;
298
- continue;
299
- }
300
- if (char === "'" || char === '"') { quote = char; started = true; continue; }
301
- if (char === " " || char === "\t") { if (started) words.push(word); word = ""; started = false; continue; }
302
- if (";|&<>($`*?[]{}~#".includes(char)) return null;
303
- word += char; started = true;
304
- }
305
- if (quote !== null) return null;
306
- if (started) words.push(word);
307
- return words.length ? words : null;
308
- }
309
-
310
- /** Unknown config keys are errors: a typo must not silently weaken a policy. */
311
- export function parsePermissionRule(value: unknown): PermissionRule {
312
- if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("permission rule must be an object");
313
- const rule = value as Record<string, unknown>;
314
- if (Object.keys(rule).some(key => !["action", "tool", "command", "commandPrefix", "path", "pathGlob"].includes(key))) throw Error("unknown permission rule field");
315
- if (!["allow", "ask", "deny"].includes(String(rule["action"]))) throw Error("permission rule action must be allow, ask or deny");
316
- for (const key of ["tool", "command", "path", "pathGlob"]) {
317
- if (rule[key] !== undefined && (typeof rule[key] !== "string" || !String(rule[key]).trim() || String(rule[key]).includes("\0"))) throw Error(`permission ${key} must be a nonempty string`);
318
- }
319
- if (rule["path"] !== undefined && rule["pathGlob"] !== undefined) throw Error("permission path and pathGlob are mutually exclusive");
320
- if (typeof rule["pathGlob"] === "string" && /[\[\]{}]/.test(rule["pathGlob"])) throw Error("permission pathGlob supports only *, ? and **");
321
- const prefix = rule["commandPrefix"];
322
- if (prefix !== undefined && (!Array.isArray(prefix) || prefix.length === 0 || prefix.some(word => typeof word !== "string" || !word || /[\r\n\0]/.test(word)))) throw Error("permission commandPrefix must contain literal argv tokens");
323
- if (prefix !== undefined && rule["command"] !== undefined) throw Error("permission command and commandPrefix are mutually exclusive");
324
- return structuredClone(value) as PermissionRule;
325
- }
326
-
327
- /**
328
- * Comma-separated flag values. Empty tokens are dropped so `--tools a,,b`
329
- * does not invent a blank name.
330
- */
331
- export function parseToolList(raw: string): string[] {
332
- return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
333
- }
334
-
335
- /**
336
- * Names the model will see. An excluded tool has to vanish here -- leaving
337
- * it in the schema and failing at call time teaches the model to retry
338
- * the same call and burn tokens.
339
- *
340
- * `--tools` is a strict allowlist. `--no-tools` starts from nothing, then
341
- * `--tools` can add names back (pi's combination). `--exclude-tools` is
342
- * applied last.
343
- */
344
- export function selectToolNames(
345
- available: readonly string[],
346
- selection: ToolSelection,
347
- ): string[] {
348
- let names: string[];
349
- if (selection.only !== undefined) {
350
- const allow = new Set(selection.only);
351
- names = available.filter((n) => allow.has(n));
352
- } else if (selection.none === true) {
353
- names = [];
354
- } else {
355
- names = [...available];
356
- }
357
- if (selection.exclude !== undefined) {
358
- const ex = new Set(selection.exclude);
359
- names = names.filter((n) => !ex.has(n));
360
- }
361
- return names;
362
- }
363
-
364
- export function selectTools<T extends { name: string; source?: ToolSource }>(
365
- tools: readonly T[],
366
- selection: ToolSelection,
367
- ): T[] {
368
- const names = new Set(selectToolNames(tools.map((t) => t.name), selection));
369
- let selected = tools.filter((t) => names.has(t.name));
370
- const profile = selection.profile === undefined ? null : TOOL_PROFILES[selection.profile];
371
- if (profile !== null && selection.only === undefined) {
372
- const defaults = new Set<string>(profile);
373
- selected = selected.filter(t => t.source === "extension" || defaults.has(t.name));
374
- }
375
- // `--tools` already named specific tools, so the category filter does
376
- // not get a second veto. Untagged tools are builtin: that is today's
377
- // only producer, and treating absence as "keep" would make the flag a
378
- // no-op until every schema is recut.
379
- if (selection.noBuiltin === true && selection.only === undefined) {
380
- selected = selected.filter((t) => (t.source ?? "builtin") !== "builtin");
381
- }
382
- return selected;
383
- }
384
-
385
- export function permissionRequest(name: string, input: unknown): PermissionRequest {
386
- if (typeof input !== "object" || input === null) {
387
- return { tool: name };
388
- }
389
- const fields = input as { readonly [k: string]: unknown };
390
- const command = ["bash", "exec_command", "environment"].includes(name) ? fields[name === "exec_command" ? "cmd" : "command"] : undefined;
391
- const paths = typeof fields["path"] === "string" ? [fields["path"]]
392
- : Array.isArray(fields["paths"]) && fields["paths"].every(path => typeof path === "string") ? fields["paths"] as string[] : undefined;
393
- return { tool: name, ...(typeof command === "string" ? { command } : {}),
394
- ...(paths ? { paths: paths.map(normalizePermissionPath) } : {}) };
395
- }
396
-
397
- /**
398
- * Why this ask became a deny: nobody could be asked. Names the flag that
399
- * would have allowed the call without asking, so a CI author can choose.
400
- */
401
- export function askWithoutTtyMessage(request: PermissionRequest): string {
402
- return (
403
- `refused: ${request.tool} requires approval and there is no tty to ask. ` +
404
- `Pass ${ALLOW_TOOLS_FLAG} ${request.tool} to allow it without asking.`
405
- );
406
- }
407
-
408
- export function deniedMessage(request: PermissionRequest): string {
409
- return `refused: ${request.tool} was denied`;
410
- }
@@ -1,105 +0,0 @@
1
- /** R7 / D8 / D11: an observed process and a live control capability are distinct facts. */
2
- import type { ArtifactRef, LogEvent } from "./events.ts";
3
- import { isObservationArtifact } from "./observation-view.ts";
4
- import { isCommandArtifactReceipt, type CommandArtifactReceipt } from "./command-artifacts.ts";
5
-
6
- export type ProcessSource = { host: string; workspace: string; cwd: string; runtime: string };
7
- export type ProcessReceipt = {
8
- kind: "process";
9
- version: 1;
10
- session_id: number;
11
- state: "running" | "exited" | "cancelled" | "timeout" | "unknown";
12
- capability: "live" | "closed" | "lost" | "expired" | "unknown";
13
- /** Legacy running/exit fields keep their meaning; null explicitly means no observation. */
14
- running: boolean | null;
15
- exit: number | null;
16
- source: ProcessSource | null;
17
- output?: ArtifactRef;
18
- full_output?: ArtifactRef;
19
- /** Half-open raw stream byte offsets covered by output, not preview character positions. */
20
- cursor?: { unit: "bytes"; start: number; end: number };
21
- replayed: boolean;
22
- /** Complete means stream EOF was observed; full_output always means all captured bytes. */
23
- capture?: "complete" | "partial";
24
- /** Same one-shot command observation, not a new scan at each poll. */
25
- artifacts?: CommandArtifactReceipt;
26
- origin?: { node: number; event: string };
27
- };
28
-
29
- const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
30
- const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
31
- export function isProcessReceipt(value: unknown): value is ProcessReceipt {
32
- if (!object(value) || value["kind"] !== "process" || value["version"] !== 1 || !natural(value["session_id"]) || value["session_id"] < 1 ||
33
- !["running", "exited", "cancelled", "timeout", "unknown"].includes(String(value["state"])) ||
34
- !["live", "closed", "lost", "expired", "unknown"].includes(String(value["capability"])) || typeof value["replayed"] !== "boolean") return false;
35
- if (value["state"] === "running") {
36
- if (value["running"] !== true || value["exit"] !== null || !["live", "lost"].includes(String(value["capability"]))) return false;
37
- } else if (value["state"] === "unknown") {
38
- if (value["running"] !== null || value["exit"] !== null || value["capability"] !== "unknown") return false;
39
- } else if (value["running"] !== false || !Number.isInteger(value["exit"]) || !["closed", "expired"].includes(String(value["capability"]))) return false;
40
- const source = value["source"];
41
- if (["lost", "expired"].includes(String(value["capability"])) && value["replayed"] !== true) return false;
42
- if (value["capture"] !== undefined && !["complete", "partial"].includes(String(value["capture"]))) return false;
43
- const artifacts = value["artifacts"];
44
- if (artifacts !== undefined && (!isCommandArtifactReceipt(artifacts) || artifacts.exit !== value["exit"] ||
45
- value["state"] === "unknown" || !object(value["source"]) || !["host", "workspace", "cwd", "runtime"].every(key =>
46
- (artifacts.source as Record<string, string>)[key] === (value["source"] as Record<string, unknown>)[key]))) return false;
47
- if (source !== null && (!object(source) || !["host", "workspace", "cwd", "runtime"].every(key => typeof source[key] === "string" && source[key] !== ""))) return false;
48
- for (const name of ["output", "full_output"]) if (value[name] !== undefined && !isObservationArtifact(value[name])) return false;
49
- const cursor = value["cursor"];
50
- if (cursor !== undefined && (!object(cursor) || cursor["unit"] !== "bytes" || !natural(cursor["start"]) || !natural(cursor["end"]) ||
51
- cursor["end"] < cursor["start"] || !isObservationArtifact(value["output"]) || value["output"].bytes !== cursor["end"] - cursor["start"])) return false;
52
- if (isObservationArtifact(value["full_output"]) && object(cursor) && typeof cursor["end"] === "number" && value["full_output"].bytes !== cursor["end"]) return false;
53
- const origin = value["origin"];
54
- return origin === undefined || object(origin) && natural(origin["node"]) && typeof origin["event"] === "string" && origin["event"] !== "";
55
- }
56
-
57
- export function processReceipt(input: { session: number; source: ProcessSource; exit: number | null; stopped?: "cancelled" | "timeout";
58
- output: ArtifactRef; fullOutput?: ArtifactRef; start: number; end: number; capture?: "complete" | "partial"; replayed?: boolean; artifacts?: CommandArtifactReceipt }): ProcessReceipt {
59
- const state = input.exit === null ? "running" : input.stopped ?? "exited";
60
- return { kind: "process", version: 1, session_id: input.session, source: input.source, state,
61
- capability: input.exit === null ? "live" : "closed", running: input.exit === null, exit: input.exit,
62
- output: input.output, ...(input.fullOutput ? { full_output: input.fullOutput } : {}),
63
- cursor: { unit: "bytes", start: input.start, end: input.end }, replayed: input.replayed === true,
64
- ...(input.artifacts ? { artifacts: input.artifacts } : {}),
65
- ...(input.capture ? { capture: input.capture } : {}) };
66
- }
67
-
68
- /** Missing live state never turns a historical running observation into a fabricated exit. */
69
- export function recoverProcessReceipt(receipt: ProcessReceipt, origin: { node: number; event: string }): ProcessReceipt {
70
- return { ...receipt, capability: receipt.state === "running" ? "lost" : receipt.state === "unknown" ? "unknown" : "expired", replayed: true, origin };
71
- }
72
-
73
- /** Original results, not a bounded handoff or replayed receipt, own the observation time and evidence. */
74
- export function latestProcessReceipt(nodes: readonly { node: number; events: readonly LogEvent[] }[], session: number):
75
- { receipt: ProcessReceipt; origin: { node: number; event: string } } | undefined {
76
- for (let n = nodes.length - 1; n >= 0; n--) {
77
- const part = nodes[n]!;
78
- for (let i = part.events.length - 1; i >= 0; i--) {
79
- const event = part.events[i]!, origin = { node: part.node, event: event.id };
80
- if (event.t === "process_exit" && event.session === session) return { origin, receipt: event.receipt ?? {
81
- kind: "process", version: 1, session_id: session, state: "exited", capability: "closed", source: null,
82
- running: false, exit: event.exit, output: event.output, replayed: false,
83
- } };
84
- if (event.t !== "tool_result" || !object(event.data) || event.data["session_id"] !== session) continue;
85
- if (isProcessReceipt(event.data)) {
86
- if (!event.data.replayed) return { receipt: event.data, origin };
87
- continue;
88
- }
89
- const data = event.data;
90
- if (typeof data["running"] !== "boolean" || data["running"] === false && !Number.isInteger(data["exit"])) continue;
91
- return { origin, receipt: { kind: "process", version: 1, session_id: session, source: null,
92
- state: data["running"] ? "running" : "exited", capability: data["running"] ? "live" : "closed",
93
- running: data["running"], exit: data["running"] ? null : data["exit"] as number,
94
- ...(event.content_ref ? { output: event.content_ref } : {}), replayed: false } };
95
- }
96
- }
97
- return undefined;
98
- }
99
-
100
- /** Disposable cache bounds are supplied by runtime; pruning never changes the durable receipts. */
101
- export function retainProcessReceipts<T extends { endedAt: number }>(entries: readonly T[], now: number, maxAgeMs: number, maxCount: number): T[] {
102
- if (!Number.isFinite(now) || !Number.isFinite(maxAgeMs) || maxAgeMs < 0 || !Number.isSafeInteger(maxCount) || maxCount < 0) throw new Error("invalid receipt retention policy");
103
- return entries.filter(entry => entry.endedAt <= now && now - entry.endedAt < maxAgeMs)
104
- .sort((a, b) => b.endedAt - a.endedAt).slice(0, maxCount);
105
- }
@@ -1,37 +0,0 @@
1
- /** O.14: observed process facts and present capabilities are different projections. */
2
- import type { ArtifactRef, LogEvent } from "./events.ts";
3
- import type { BranchRecord } from "./handoff.ts";
4
- import { isProcessReceipt } from "./process-receipt.ts";
5
-
6
- export type ProcessObservation = {
7
- session: number; node: number; event: string; observed: "running" | "exited";
8
- command?: string; exit?: number; output?: ArtifactRef;
9
- }
10
- export function deriveProcesses(prev: readonly ProcessObservation[], events: readonly LogEvent[], node: number): ProcessObservation[] {
11
- const state = new Map(prev.map(item => [item.session, item]));
12
- const commands = new Map<string, string>();
13
- for (const event of events) {
14
- if (event.t === "tool_invocation" && event.name === "exec_command" && typeof event.input === "object" && event.input !== null &&
15
- !Array.isArray(event.input) && "cmd" in event.input && typeof event.input["cmd"] === "string") commands.set(event.call_id, event.input["cmd"]);
16
- if (event.t === "assistant") for (const call of event.blocks) {
17
- if (call.type === "tool_use" && call.name === "exec_command" && typeof call.input === "object" && call.input !== null && "cmd" in call.input && typeof call.input.cmd === "string") commands.set(call.call_id, call.input.cmd);
18
- }
19
- if (event.t === "process_exit") state.set(event.session, { session: event.session, node, event: event.id, observed: "exited", command: event.command, exit: event.exit, output: event.output });
20
- if (event.t !== "tool_result" || !event.data || typeof event.data !== "object" || Array.isArray(event.data)) continue;
21
- const data = event.data as { readonly [key: string]: unknown };
22
- if (isProcessReceipt(data) && data.replayed) continue;
23
- if (typeof data["session_id"] !== "number" || !Number.isSafeInteger(data["session_id"]) || data["session_id"] < 1 || typeof data["running"] !== "boolean") continue;
24
- if (data["running"] === false && !Number.isInteger(data["exit"])) continue;
25
- const before = state.get(data["session_id"]);
26
- const command = event.effect?.kind === "command" ? event.effect.cmd : commands.get(event.call_id) ?? before?.command;
27
- state.set(data["session_id"], { session: data["session_id"], node, event: event.id, observed: data["running"] ? "running" : "exited",
28
- ...(command ? { command } : {}), ...(typeof data["exit"] === "number" ? { exit: data["exit"] } : {}), ...(event.content_ref ? { output: event.content_ref } : {}) });
29
- }
30
- return [...state.values()].sort((a, b) => a.session - b.session);
31
- }
32
- export function processStatus(item: ProcessObservation, handle: "running" | "ended_unobserved" | undefined): "running" | "ended_unobserved" | "unknown" | "exited" {
33
- return item.observed === "exited" ? "exited" : handle ?? "unknown";
34
- }
35
- export function branchStatus(item: BranchRecord, handle: "running" | "ready" | undefined): "running" | "ready" | "unknown" | "joined" | "resolved" {
36
- return item.resolution !== null ? "resolved" : item.handoff_ref ? "joined" : handle ?? "unknown";
37
- }
@@ -1,40 +0,0 @@
1
- /** OSC 7501 revision 0.2: program state, never a model's claim of goal completion (Q24/D11). */
2
- import type { ExecutionOutcome } from "./outcome.ts";
3
-
4
- export type ProgramStatus = { state: "idle" | "working" | "done" | "blocked" | "error" | "clear";
5
- kind?: "permission" | "question" | "auth"; msg?: string };
6
- /** S.1: what a finished turn's ledger says, in one line; the status itself stays "done". */
7
- const FINISHED: Readonly<Record<string, string>> = {
8
- verified: "Goal verified against its contract",
9
- checked: "Turn finished; changes passed a fresh check",
10
- unchecked: "Turn finished; changes not checked",
11
- needs_work: "Turn finished; declared checks incomplete",
12
- not_applicable: "Turn finished",
13
- };
14
-
15
- export function programStatus(input: { running: boolean; waiting?: "permission" | "question" | "auth" | null;
16
- outcome?: ExecutionOutcome; goal?: string }): ProgramStatus {
17
- if (input.waiting) return { state: "blocked", kind: input.waiting, msg: input.waiting === "auth" ? "Sign in to continue" : input.waiting === "permission" ? "Tool approval required" : "User input required" };
18
- if (input.running) return { state: "working", msg: "Agent is working" };
19
- if (input.outcome?.status === "cancelled") return { state: "idle", msg: "Turn cancelled" };
20
- if (input.outcome?.status === "failed") return { state: "error", msg: "Turn failed; inspect the session" };
21
- if (input.outcome?.status === "stopped") return { state: "blocked", kind: "question", msg: "Turn stopped; review evidence before continuing" };
22
- if (input.goal === "awaiting_human") return { state: "blocked", kind: "question", msg: "Goal awaits user review" };
23
- if (input.outcome?.reason === "model_end") return { state: "done", msg: FINISHED[input.goal ?? "not_applicable"] ?? FINISHED["not_applicable"]! };
24
- return { state: "idle", msg: "Ready for a request" };
25
- }
26
- export const PROGRAM_STATUS_QUERY = "\x1b]7501;?\x1b\\";
27
- export function programStatusReply(sequence: string): boolean { return /^\x1b\]7501;\?[^\x00-\x1f\x7f]*(?:\x07|\x1b\\)$/.test(sequence); }
28
-
29
- export function encodeProgramStatus(status: ProgramStatus, id: string): string {
30
- if (id.length > 128 || id.split("/").length > 8 || !id.split("/").every(part => /^[A-Za-z0-9_.+-]{1,32}$/.test(part))) throw Error("invalid program status id");
31
- let message = "", size = 0;
32
- // Protocol's decoded UTF-8 cap. Remove controls before encoding; never split a code point.
33
- for (const point of (status.msg ?? "").replace(/[\x00-\x1f\x7f-\x9f\p{Cf}]/gu, " ")) {
34
- const bytes = new TextEncoder().encode(point); if (size + bytes.length > 2048) break;
35
- message += point; size += bytes.length;
36
- }
37
- const encoded = btoa(Array.from(new TextEncoder().encode(message), byte => String.fromCharCode(byte)).join(""));
38
- return `\x1b]7501;state=${status.state}:app=xeno:id=${id}` +
39
- (status.state === "blocked" && status.kind ? `:kind=${status.kind}` : "") + (message ? `:msg=${encoded}` : "") + "\x1b\\";
40
- }