@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,488 +0,0 @@
1
- /**
2
- * What this request's executed commands did outside the workspace
3
- * (feat-external-effects-summary).
4
- *
5
- * The permission design deliberately has no command-danger classifier
6
- * (permissions.ts): a screen that misses something implies someone is
7
- * checking. This is the opposite stance -- a statement after the fact, shown
8
- * to the person when a request ends, never a gate. Its failure mode decides
9
- * its shape: a missed item is one line fewer; a wrong item teaches the person
10
- * to ignore the line. So everything here prefers silence to a guess:
11
- * unparseable commands are skipped whole, and a path containing a variable or
12
- * a command substitution is never resolved.
13
- *
14
- * Only executed commands count: the input is `tool_result` receipts with a
15
- * `command` effect, which a denied or cancelled call never has. Nothing here
16
- * is stored; the list is recomputed from the log whenever it is shown (D8).
17
- *
18
- * Pure: the home and temporary directories are parameters, never read here.
19
- */
20
- import type { LogEvent } from "./events.ts";
21
- import { normalizePermissionPath } from "./permissions.ts";
22
-
23
- export type ExternalCategory = "write" | "process" | "system" | "network" | "push";
24
- export interface ExternalEffect {
25
- readonly category: ExternalCategory;
26
- /** The simple command that did it, truncated for display. */
27
- readonly command: string;
28
- /** The receipt that ran it. */
29
- readonly event: string;
30
- /** The resolved outside path, for writes. */
31
- readonly path?: string;
32
- }
33
- export interface ExternalOptions {
34
- /** Expands `~` and `$HOME`. Absent: such paths are dynamic and skipped. */
35
- readonly home?: string;
36
- /** Extra temporary roots (the platform's os.tmpdir()); /tmp and /var/tmp are always temporary. */
37
- readonly temp?: readonly string[];
38
- }
39
-
40
- /** Display bound for one item; long heredoc commands would otherwise fill the line. */
41
- const SNIPPET_CHARS = 60;
42
- /** Assumed bound on nested `sh -c` analysis; deeper nesting is skipped, not guessed. */
43
- const MAX_NESTING = 3;
44
- const ALWAYS_TEMPORARY = ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", "/dev"];
45
-
46
- // ---------------------------------------------------------------------------
47
- // Lexer: a deliberately small subset of POSIX shell. Anything outside it makes
48
- // the whole command unparseable, which means "report nothing".
49
- // ---------------------------------------------------------------------------
50
-
51
- /** A word with its literal value, or null when part of it is only known at run time. */
52
- type Word = { text: string; value: string | null; quoted: boolean };
53
- type Token =
54
- | { kind: "word"; word: Word }
55
- | { kind: "op"; op: string }
56
- | { kind: "redirect"; op: string; fd: string };
57
-
58
- const OPERATORS = ["&&", "||", ";;", "|&", ";", "|", "&", "(", ")", "\n"];
59
-
60
- /** Tokens, or null when the command does not fit the subset (unclosed quote, unterminated heredoc). */
61
- function lex(input: string, home: string | undefined): Token[] | null {
62
- const tokens: Token[] = [];
63
- const heredocs: { delimiter: string; strip: boolean }[] = [];
64
- let i = 0;
65
- const at = (offset = 0) => input[i + offset] ?? "";
66
- while (i < input.length) {
67
- const char = at();
68
- if (char === "\n") {
69
- tokens.push({ kind: "op", op: "\n" }); i++;
70
- // Heredoc bodies are data, never commands: skip them line by line.
71
- for (const doc of heredocs.splice(0)) {
72
- for (;;) {
73
- if (i >= input.length) return null;
74
- const end = input.indexOf("\n", i);
75
- const line = input.slice(i, end < 0 ? input.length : end);
76
- i = end < 0 ? input.length : end + 1;
77
- if ((doc.strip ? line.replace(/^\t+/, "") : line) === doc.delimiter) break;
78
- }
79
- }
80
- continue;
81
- }
82
- if (char === " " || char === "\t") { i++; continue; }
83
- if (char === "\\" && at(1) === "\n") { i += 2; continue; }
84
- if (char === "#") { while (i < input.length && at() !== "\n") i++; continue; }
85
- // Redirections, optionally with a leading descriptor (2>, &>).
86
- const redirect = /^(\d*|&)(>>|>\||>&|>|<<<|<<-|<<|<>|<&|<)/.exec(input.slice(i));
87
- if (redirect && (redirect[1] !== "&" || redirect[2]!.startsWith(">"))) {
88
- const op = redirect[2]!;
89
- i += redirect[0].length;
90
- if (op === "<<" || op === "<<-") {
91
- while (at() === " " || at() === "\t") i++;
92
- const word = readWord();
93
- if (word === null || word.text === "") return null;
94
- heredocs.push({ delimiter: word.text, strip: op === "<<-" });
95
- continue;
96
- }
97
- tokens.push({ kind: "redirect", op, fd: redirect[1]! });
98
- continue;
99
- }
100
- const operator = OPERATORS.find(candidate => input.startsWith(candidate, i));
101
- if (operator) { tokens.push({ kind: "op", op: operator }); i += operator.length; continue; }
102
- const word = readWord();
103
- if (word === null) return null;
104
- tokens.push({ kind: "word", word });
105
- }
106
- if (heredocs.length) return null;
107
- return tokens;
108
-
109
- /** One word; `text` is the unquoted spelling (heredoc delimiters, display), `value` the resolved literal. */
110
- function readWord(): Word | null {
111
- let text = "", value: string | null = "", quoted = false;
112
- const start = i;
113
- const dynamic = () => { value = null; };
114
- while (i < input.length) {
115
- const char = at();
116
- if (" \t\n;&|()<>".includes(char)) break;
117
- if (char === "'") {
118
- const end = input.indexOf("'", i + 1);
119
- if (end < 0) return null;
120
- const part = input.slice(i + 1, end);
121
- text += part; if (value !== null) value += part; quoted = true; i = end + 1; continue;
122
- }
123
- if (char === '"') {
124
- i++; quoted = true;
125
- for (;;) {
126
- if (i >= input.length) return null;
127
- const inner = at();
128
- if (inner === '"') { i++; break; }
129
- if (inner === "\\" && '$`"\\\n'.includes(at(1))) { text += at(1); if (value !== null) value += at(1); i += 2; continue; }
130
- if (inner === "$" || inner === "`") {
131
- const expanded = expansion(true);
132
- if (expanded === undefined) return null;
133
- text += expanded.text; if (expanded.value === null) dynamic(); else if (value !== null) value += expanded.value;
134
- continue;
135
- }
136
- text += inner; if (value !== null) value += inner; i++;
137
- }
138
- continue;
139
- }
140
- if (char === "\\") {
141
- if (i + 1 >= input.length) return null;
142
- text += at(1); if (value !== null) value += at(1); i += 2; continue;
143
- }
144
- if (char === "$" || char === "`") {
145
- const expanded = expansion(false);
146
- if (expanded === undefined) return null;
147
- text += expanded.text; if (expanded.value === null) dynamic(); else if (value !== null) value += expanded.value;
148
- continue;
149
- }
150
- if (char === "~" && i === start) {
151
- // Only a bare leading ~ or ~/ is the home directory; ~user is not guessed.
152
- const next = at(1);
153
- if (next === "" || next === "/" || " \t\n;&|()<>".includes(next)) {
154
- text += "~"; if (home === undefined) dynamic(); else if (value !== null) value += home; i++; continue;
155
- }
156
- text += char; dynamic(); i++; continue;
157
- }
158
- if ("*?[{".includes(char)) { text += char; dynamic(); i++; continue; }
159
- text += char; if (value !== null) value += char; i++;
160
- }
161
- return { text, value, quoted };
162
- }
163
-
164
- /** `$HOME` / `${HOME}` expand; every other expansion makes the word dynamic. undefined: unparseable. */
165
- function expansion(inDouble: boolean): { text: string; value: string | null } | undefined {
166
- const start = i;
167
- if (at() === "`") {
168
- const end = input.indexOf("`", i + 1);
169
- if (end < 0) return undefined;
170
- i = end + 1; return { text: input.slice(start, i), value: null };
171
- }
172
- // $(...) / $((...)): balanced parentheses, quotes inside are skipped coarsely.
173
- if (at(1) === "(") {
174
- let depth = 0;
175
- for (; i < input.length; i++) {
176
- const char = at();
177
- if (char === "'" && !inDouble) { const end = input.indexOf("'", i + 1); if (end < 0) return undefined; i = end; continue; }
178
- if (char === "(") depth++;
179
- if (char === ")" && --depth === 0) { i++; return { text: input.slice(start, i), value: null }; }
180
- }
181
- return undefined;
182
- }
183
- if (at(1) === "{") {
184
- const end = input.indexOf("}", i);
185
- if (end < 0) return undefined;
186
- const name = input.slice(i + 2, end);
187
- i = end + 1;
188
- return { text: input.slice(start, i), value: name === "HOME" && home !== undefined ? home : null };
189
- }
190
- const name = /^[A-Za-z_][A-Za-z0-9_]*|^[0-9@*#?$!-]/.exec(input.slice(i + 1))?.[0];
191
- if (!name) { i++; return { text: "$", value: "$" }; }
192
- i += 1 + name.length;
193
- return { text: input.slice(start, i), value: name === "HOME" && home !== undefined ? home : null };
194
- }
195
- }
196
-
197
- // ---------------------------------------------------------------------------
198
- // Commands
199
- // ---------------------------------------------------------------------------
200
-
201
- type Simple = { words: Word[]; redirects: { op: string; fd: string; target: Word | null }[]; text: string };
202
- type Context = { root: string; options: ExternalOptions; event: string; out: ExternalEffect[]; depth: number };
203
-
204
- const WRITE_COMMANDS = new Set(["cp", "mv", "ln", "rm", "rmdir", "mkdir", "touch", "chmod", "chown", "chgrp", "tee", "install", "rsync", "truncate"]);
205
- /** Words that run the rest of the line as a command; their own options are skipped. */
206
- const PREFIXES = new Set(["sudo", "doas", "env", "command", "builtin", "exec", "nohup", "time", "nice", "xargs"]);
207
-
208
- function inside(path: string, root: string): boolean {
209
- return path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`);
210
- }
211
-
212
- function absolute(path: string, cwd: string): string {
213
- return normalizePermissionPath(path.startsWith("/") ? path : `${cwd}/${path}`);
214
- }
215
-
216
- function snippet(text: string): string {
217
- const line = text.replace(/\s+/g, " ").trim();
218
- return line.length > SNIPPET_CHARS ? `${line.slice(0, SNIPPET_CHARS - 1)}…` : line;
219
- }
220
-
221
- /** The outside path a literal target resolves to, or null (inside, temporary, or dynamic). */
222
- function outside(target: Word | null, cwd: string | null, ctx: Context): string | null {
223
- if (!target || target.value === null || target.value === "" || cwd === null) return null;
224
- // `-` is stdout/stdin for tee and friends.
225
- if (target.value === "-") return null;
226
- const path = absolute(target.value, cwd);
227
- const root = normalizePermissionPath(ctx.root);
228
- if (inside(path, root)) return null;
229
- for (const temp of [...ALWAYS_TEMPORARY, ...ctx.options.temp ?? []]) if (inside(path, normalizePermissionPath(temp))) return null;
230
- return path;
231
- }
232
-
233
- /** Splits tokens into simple commands; `cd` state is threaded across `&&`, `;` and newlines, restored by `)`. */
234
- function analyse(tokens: Token[], cwd: string, ctx: Context): void {
235
- const stack: (string | null)[] = [];
236
- let current: string | null = cwd;
237
- let simple: Simple = { words: [], redirects: [], text: "" };
238
- let pendingRedirect: { op: string; fd: string } | null = null;
239
- const flush = () => {
240
- if (pendingRedirect) { simple.redirects.push({ ...pendingRedirect, target: null }); pendingRedirect = null; }
241
- if (simple.words.length || simple.redirects.length) current = command(simple, current, ctx);
242
- simple = { words: [], redirects: [], text: "" };
243
- };
244
- for (const token of tokens) {
245
- if (token.kind === "op") {
246
- flush();
247
- if (token.op === "(") stack.push(current);
248
- if (token.op === ")") current = stack.length ? stack.pop()! : current;
249
- continue;
250
- }
251
- if (token.kind === "redirect") {
252
- if (pendingRedirect) simple.redirects.push({ ...pendingRedirect, target: null });
253
- pendingRedirect = { op: token.op, fd: token.fd };
254
- simple.text += `${simple.text ? " " : ""}${token.fd}${token.op}`;
255
- continue;
256
- }
257
- simple.text += `${simple.text && !pendingRedirect ? " " : ""}${token.word.text}`;
258
- if (pendingRedirect) { simple.redirects.push({ ...pendingRedirect, target: token.word }); pendingRedirect = null; continue; }
259
- simple.words.push(token.word);
260
- }
261
- flush();
262
- }
263
-
264
- /** One simple command; returns the working directory after it (null: unknown, so later paths are not guessed). */
265
- function command(simple: Simple, cwd: string | null, ctx: Context): string | null {
266
- const add = (category: ExternalCategory, path?: string) => {
267
- if (ctx.out.some(item => item.event === ctx.event && item.category === category && item.path === path && item.command === snippet(simple.text))) return;
268
- ctx.out.push({ category, command: snippet(simple.text), event: ctx.event, ...(path === undefined ? {} : { path }) });
269
- };
270
- for (const redirect of simple.redirects) {
271
- if (!redirect.op.startsWith(">") && redirect.op !== "<>") continue;
272
- // >&2 duplicates a descriptor; it names no file.
273
- if (redirect.op === ">&" && redirect.target?.value !== null && /^\d+$|^-$/.test(redirect.target?.value ?? "")) continue;
274
- const path = outside(redirect.target, cwd, ctx);
275
- if (path) add("write", path);
276
- }
277
- let words = simple.words;
278
- // Leading assignments (FOO=bar cmd) and wrappers (sudo, env -i) are not the command.
279
- for (;;) {
280
- while (words.length && words[0]!.value !== null && !words[0]!.quoted && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]!.value!)) words = words.slice(1);
281
- const head = words[0]?.value;
282
- if (head === undefined || head === null || !PREFIXES.has(baseName(head))) break;
283
- words = words.slice(1);
284
- while (words.length && (words[0]!.value ?? "").startsWith("-")) words = words.slice(1);
285
- }
286
- if (!words.length) return cwd;
287
- const name = words[0]!.value;
288
- if (name === null) return cwd;
289
- const base = baseName(name);
290
- const args = words.slice(1);
291
- const values = args.map(word => word.value);
292
-
293
- switch (base) {
294
- case "cd": case "pushd": {
295
- const target = args.find(word => !(word.value ?? "").startsWith("-") || word.value === "-");
296
- if (!target) return ctx.options.home ?? null;
297
- if (target.value === null || target.value === "-" || cwd === null) return null;
298
- return absolute(target.value, cwd);
299
- }
300
- case "popd": return null;
301
- case "sh": case "bash": case "zsh": case "dash": {
302
- const flag = args.findIndex(word => /^-[a-z]*c[a-z]*$/.test(word.value ?? ""));
303
- const script = flag >= 0 ? args[flag + 1] : undefined;
304
- if (script?.value && ctx.depth < MAX_NESTING) {
305
- const tokens = lex(script.value, ctx.options.home);
306
- if (tokens && cwd !== null) analyse(tokens, cwd, { ...ctx, depth: ctx.depth + 1 });
307
- }
308
- return cwd;
309
- }
310
- case "kill":
311
- // kill -0 probes and kill -l lists; neither changes a process.
312
- if (values.some(value => value === "-0" || value === "-l" || value === "-L" || value === "-s0")) return cwd;
313
- if (args.length) add("process");
314
- return cwd;
315
- case "killall": case "pkill":
316
- if (args.length) add("process");
317
- return cwd;
318
- case "launchctl": {
319
- const sub = values[0];
320
- if (sub && ["load", "unload", "bootstrap", "bootout", "kickstart", "kill", "start", "stop", "enable", "disable", "remove", "submit", "setenv", "unsetenv"].includes(sub)) add("system");
321
- return cwd;
322
- }
323
- case "systemctl": {
324
- const sub = values.find(value => value !== null && !value.startsWith("-"));
325
- if (sub && ["start", "stop", "restart", "reload", "enable", "disable", "mask", "unmask", "kill", "daemon-reload", "set-property", "edit", "isolate", "try-restart", "reload-or-restart"].includes(sub)) add("system");
326
- return cwd;
327
- }
328
- case "defaults": {
329
- const sub = values.find(value => value !== null && !value.startsWith("-"));
330
- if (sub && ["write", "delete", "import", "rename"].includes(sub)) add("system");
331
- return cwd;
332
- }
333
- case "brew": {
334
- const sub = values.find(value => value !== null && !value.startsWith("-"));
335
- if (sub && ["install", "upgrade", "uninstall", "reinstall", "remove", "rm", "link", "unlink", "tap", "untap"].includes(sub)) add("system");
336
- return cwd;
337
- }
338
- case "npm": case "pnpm": case "yarn": case "bun": {
339
- const global = values.some(value => value === "-g" || value === "--global" || value === "--location=global");
340
- const sub = values.find(value => value !== null && !value.startsWith("-"));
341
- if (global && sub && ["install", "i", "add", "uninstall", "remove", "rm", "un", "update", "upgrade", "link"].includes(sub)) add("system");
342
- if (base === "yarn" && sub === "global") add("system");
343
- return cwd;
344
- }
345
- case "pip": case "pip3": {
346
- const sub = values.find(value => value !== null && !value.startsWith("-"));
347
- if (sub === "install" || sub === "uninstall") add("system");
348
- return cwd;
349
- }
350
- case "python": case "python3": {
351
- const module = values.indexOf("-m");
352
- if (module >= 0 && /^pip3?$/.test(values[module + 1] ?? "") && ["install", "uninstall"].includes(values[module + 2] ?? "")) add("system");
353
- return cwd;
354
- }
355
- case "curl":
356
- if (curlMutates(values)) add("network");
357
- return cwd;
358
- case "wget":
359
- if (values.some(value => value !== null && /^--(post-data|post-file|body-data|body-file|method)(=|$)/.test(value))) add("network");
360
- return cwd;
361
- case "git": {
362
- // Global options before the subcommand: -C dir, -c k=v, --git-dir=...
363
- let index = 0;
364
- while (index < values.length && (values[index] ?? "").startsWith("-")) index += ["-C", "-c"].includes(values[index] ?? "") ? 2 : 1;
365
- if (values[index] === "push" && !values.slice(index + 1).some(value => value === "--dry-run" || value === "-n")) add("push");
366
- return cwd;
367
- }
368
- }
369
- // A dynamic operand could be any path; writeTargets judges only literal ones.
370
- if (WRITE_COMMANDS.has(base)) for (const path of writeTargets(base, args, cwd, ctx)) add("write", path);
371
- return cwd;
372
- }
373
-
374
- function baseName(path: string): string {
375
- return path.slice(path.lastIndexOf("/") + 1);
376
- }
377
-
378
- /** Paths the command writes, among its literal operands, resolved and outside. */
379
- function writeTargets(base: string, args: Word[], cwd: string | null, ctx: Context): string[] {
380
- const operands: Word[] = [];
381
- let targetDirectory: Word | null = null;
382
- let options = true;
383
- for (let index = 0; index < args.length; index++) {
384
- const word = args[index]!;
385
- const value = word.value;
386
- if (options && value === "--") { options = false; continue; }
387
- if (options && value !== null && value.startsWith("-") && value !== "-") {
388
- if ((base === "cp" || base === "mv" || base === "ln" || base === "install") && (value === "-t" || value === "--target-directory")) { targetDirectory = args[++index] ?? null; continue; }
389
- if (value.startsWith("--target-directory=")) { targetDirectory = { ...word, value: value.slice("--target-directory=".length) }; continue; }
390
- // Options that take a separate value; the value is not an operand.
391
- if ((base === "chmod" || base === "chown" || base === "chgrp") && value === "--reference") { index++; continue; }
392
- if (base === "install" && ["-m", "-o", "-g"].includes(value)) { index++; continue; }
393
- if (base === "truncate" && (value === "-s" || value === "-r")) { index++; continue; }
394
- if (base === "mkdir" && value === "-m") { index++; continue; }
395
- continue;
396
- }
397
- operands.push(word);
398
- }
399
- let targets: Word[];
400
- switch (base) {
401
- case "cp": case "mv": case "ln": case "install": case "rsync":
402
- // The destination is the last operand (or -t); sources are read, not written.
403
- targets = targetDirectory ? [targetDirectory] : operands.length >= 2 ? [operands.at(-1)!] : [];
404
- // mv also removes its sources.
405
- if (base === "mv") targets = [...targets, ...operands.slice(0, targetDirectory ? operands.length : -1)];
406
- break;
407
- case "chmod": case "chown": case "chgrp":
408
- // The first operand is the mode or owner.
409
- targets = operands.slice(1);
410
- break;
411
- default:
412
- targets = operands;
413
- }
414
- // rsync destinations may be remote (host:path); those are network, not local writes.
415
- if (base === "rsync") targets = targets.filter(word => word.value !== null && !/^[^/]*:/.test(word.value));
416
- return targets.map(word => outside(word, cwd, ctx)).filter((path): path is string => path !== null);
417
- }
418
-
419
- function curlMutates(values: readonly (string | null)[]): boolean {
420
- let get = false, data = false, method: string | null = null;
421
- for (let index = 0; index < values.length; index++) {
422
- const value = values[index];
423
- if (value === null || value === undefined) continue;
424
- if (value === "-G" || value === "--get") get = true;
425
- else if (value === "-X" || value === "--request") method = (values[++index] ?? "").toUpperCase();
426
- else if (value.startsWith("--request=")) method = value.slice("--request=".length).toUpperCase();
427
- else if (/^-X./.test(value)) method = value.slice(2).toUpperCase();
428
- else if (/^--(data|data-raw|data-binary|data-urlencode|data-ascii|json)(=|$)/.test(value) || /^-d/.test(value)) data = true;
429
- else if (/^--(form|form-string|upload-file)(=|$)/.test(value) || /^-[FT]/.test(value)) return true;
430
- else if (/^-[a-zA-Z]+$/.test(value) && !value.startsWith("--")) {
431
- // Bundled short flags: -sSd ... or -sX POST.
432
- const flags = value.slice(1);
433
- if (flags.includes("G")) get = true;
434
- if (/[FT]/.test(flags)) return true;
435
- if (flags.endsWith("X")) method = (values[++index] ?? "").toUpperCase();
436
- else if (flags.endsWith("d")) data = true;
437
- }
438
- }
439
- if (method !== null) return method !== "GET" && method !== "HEAD";
440
- return data && !get;
441
- }
442
-
443
- /** One command string's effects; exported for the property tests and for single-receipt views. */
444
- export function commandExternalEffects(cmd: string, event: string, workspaceRoot: string, options: ExternalOptions = {}, cwd = workspaceRoot): ExternalEffect[] {
445
- const tokens = lex(cmd, options.home);
446
- if (!tokens) return [];
447
- const out: ExternalEffect[] = [];
448
- analyse(tokens, normalizePermissionPath(cwd), { root: workspaceRoot, options, event, out, depth: 0 });
449
- return out;
450
- }
451
-
452
- /**
453
- * The current request's outside effects, in log order. `events` is the
454
- * request's own slice (currentRequestEvents); a workdir on the call is
455
- * honoured when it is a literal relative or absolute path.
456
- */
457
- export function externalEffects(events: readonly LogEvent[], workspaceRoot: string, options: ExternalOptions = {}): ExternalEffect[] {
458
- const workdirs = new Map<string, string>();
459
- for (const event of events) {
460
- if (event.t !== "assistant") continue;
461
- for (const block of event.blocks) {
462
- if (block.type !== "tool_use" || block.input === null || typeof block.input !== "object") continue;
463
- const workdir = (block.input as Record<string, unknown>)["workdir"];
464
- if (typeof workdir === "string" && workdir !== "") workdirs.set(block.call_id, workdir);
465
- }
466
- }
467
- const root = normalizePermissionPath(workspaceRoot);
468
- const out: ExternalEffect[] = [];
469
- for (const event of events) {
470
- if (event.t !== "tool_result" || event.effect?.kind !== "command") continue;
471
- const workdir = workdirs.get(event.call_id);
472
- out.push(...commandExternalEffects(event.effect.cmd, event.id, root, options, workdir ? absolute(workdir, root) : root));
473
- }
474
- return out;
475
- }
476
-
477
- /** Display bound: the line names the first few and counts the rest. */
478
- export const EXTERNAL_SHOWN = 5;
479
-
480
- /** One line for the end of a request, or null when nothing reached outside. */
481
- export function formatExternalEffects(effects: readonly ExternalEffect[], options: { home?: string } = {}): string | null {
482
- if (!effects.length) return null;
483
- const tilde = (path: string) => options.home && inside(path, options.home) ? `~${path.slice(options.home.length)}` : path;
484
- const label = (effect: ExternalEffect) => effect.category === "write" && effect.path ? `write ${tilde(effect.path)}` : effect.command;
485
- const shown = effects.slice(0, EXTERNAL_SHOWN).map(label);
486
- const rest = effects.length - shown.length;
487
- return `Outside the workspace: ${shown.join(" · ")}${rest > 0 ? ` · and ${rest} more` : ""}`;
488
- }
@@ -1,173 +0,0 @@
1
- /** R5 / D8: replayable failure premises; advice never authorizes or automatically retries a tool. */
2
- import type { GuidanceEvent, LogEvent, ToolResultEvent } from "./events.ts";
3
- import type { ToolDiagnostic } from "./tool-contract.ts";
4
- import { canonicalValue } from "./continuation.ts";
5
- import { repairToolInput } from "./tool-input.ts";
6
- import { failureDiagnostic, scopeFailureGuidance } from "./tool-failure.ts";
7
-
8
- /** Reuses the existing three-attempt diagnostic hypothesis, not a measured optimum or a stop threshold. */
9
- export const RECOVERY_FAILURES = 3;
10
- type Call = { name: string; input: unknown };
11
- export interface FailureIdentity {
12
- family: string;
13
- resource: string;
14
- version: string;
15
- code: ToolDiagnostic["code"];
16
- execution: NonNullable<ToolDiagnostic["execution"]>;
17
- effects: NonNullable<ToolDiagnostic["effects"]>;
18
- premise: string;
19
- }
20
- interface FailureRun {
21
- identity: FailureIdentity;
22
- key: string;
23
- count: number;
24
- sources: readonly string[];
25
- notified: boolean;
26
- }
27
- export interface RecoveryState {
28
- calls: ReadonlyMap<string, Call>;
29
- versions: ReadonlyMap<string, string>;
30
- failures: ReadonlyMap<string, FailureRun>;
31
- change: number;
32
- }
33
- export function emptyRecoveryState(): RecoveryState {
34
- return { calls: new Map(), versions: new Map(), failures: new Map(), change: 0 };
35
- }
36
- const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
37
- const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
38
- const fileTools = ["read", "write", "edit", "grep", "find", "ls"];
39
- const family = (name: string): string => name === "bash" || name === "exec_command" ? "command"
40
- : name === "edit" || name === "write" ? "file_change" : name;
41
-
42
- function target(call: Call, identify: (path: string) => string): { family: string; resource: string; input: Record<string, unknown> } {
43
- const repaired = repairToolInput(call.name, call.input), input = record(repaired) ? repaired : { value: repaired };
44
- const kind = family(call.name);
45
- return { family: kind, input, resource: fileTools.includes(call.name) && typeof input["path"] === "string" ? identify(input["path"])
46
- : kind === "command" ? identify(typeof input["workdir"] === "string" ? input["workdir"] : ".")
47
- : call.name === "write_stdin" ? `session:${String(input["session_id"])}` : call.name };
48
- }
49
-
50
- function groupKey(call: Call, result: ToolResultEvent, identify: (path: string) => string): string {
51
- const context = target(call, identify);
52
- return canonicalValue([context.family, context.resource, context.family === "command"
53
- ? result.effect?.kind === "command" ? result.effect.cmd.trim() : String(context.input["command"] ?? context.input["cmd"] ?? "").trim() : null]);
54
- }
55
-
56
- /** Only recorded versions and structured failures are facts; stderr words never identify an error. */
57
- export function failureIdentity(call: Call, result: ToolResultEvent, state: RecoveryState,
58
- identify: (path: string) => string): FailureIdentity | null {
59
- const diagnostic = result.diagnostic;
60
- if (result.ok || !diagnostic || diagnostic.execution === undefined || diagnostic.effects === undefined) return null;
61
- const context = target(call, identify), data = record(result.data) ? result.data : {};
62
- const current = data["sha256"];
63
- const effect = result.effect;
64
- const known = state.versions.get(context.resource);
65
- const version = diagnostic.code === "permission_denied" ? "unknown" : current === null ? "absent" : digest(current) ? current
66
- : effect?.kind === "command" && effect.workspace_observed !== false && digest(effect.workspace_sha256) ? effect.workspace_sha256
67
- : digest(known) ? known : "unknown";
68
- const input = context.input;
69
- let premise: unknown;
70
- if (diagnostic.code === "version_mismatch") {
71
- const precondition = record(data["precondition"]) ? data["precondition"] : {};
72
- premise = { expected: precondition["expected"] === null ? "absent"
73
- : precondition["expected"] ?? input["expected_sha256"] ?? input["read_handle"] ?? "unknown" };
74
- } else if (diagnostic.code === "permission_denied") {
75
- // Replacement wording and requested shell timeouts cannot change a denied capability.
76
- premise = { operation: context.family === "command" ? effect?.kind === "command" ? effect.cmd.trim()
77
- : String(input["command"] ?? input["cmd"] ?? "").trim() : context.family };
78
- } else if (context.family === "file_change" && ["not_found", "ambiguous_edit"].includes(diagnostic.code)) {
79
- const edits = Array.isArray(input["edits"]) ? input["edits"] : [input];
80
- premise = edits.map(edit => record(edit) ? { old_string: edit["old_string"], replace_all: edit["replace_all"] === true } : edit);
81
- } else if (context.family === "command") {
82
- // Shared launch contract equates bash/exec_command; arbitrary shell strings are not parsed as equivalent.
83
- premise = { command: effect?.kind === "command" ? effect.cmd.trim() : String(input["command"] ?? input["cmd"] ?? "").trim(),
84
- exit: effect?.kind === "command" ? effect.exit : null,
85
- timeout: input["timeout"] ?? null, tty: input["tty"] === true, change: state.change };
86
- } else {
87
- const normalized = { ...input };
88
- if (fileTools.includes(call.name) && typeof normalized["path"] === "string") normalized["path"] = context.resource;
89
- premise = normalized;
90
- }
91
- return { family: context.family, resource: context.resource, version, code: diagnostic.code,
92
- execution: diagnostic.execution, effects: diagnostic.effects, premise: canonicalValue(premise) };
93
- }
94
-
95
- function changedPremises(state: RecoveryState, changed: boolean, resource?: string, version?: string): RecoveryState {
96
- return { ...state, change: state.change + Number(changed), failures: new Map([...state.failures].filter(([, run]) =>
97
- run.identity.code === "permission_denied" || !(changed && run.identity.family === "command" ||
98
- resource !== undefined && version !== undefined && run.identity.resource === resource && run.identity.version !== version))) };
99
- }
100
-
101
- /** Incremental disposable projection; folding the original ancestry yields exactly the same state. */
102
- export function advanceRecovery(state: RecoveryState, events: readonly LogEvent[], identify: (path: string) => string): RecoveryState {
103
- let current = state;
104
- for (const event of events) {
105
- if (event.t === "user") { current = { ...current, failures: new Map() }; continue; }
106
- if (event.t === "assistant") {
107
- const calls = new Map(current.calls);
108
- for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, { name: block.name, input: block.input });
109
- current = { ...current, calls }; continue;
110
- }
111
- if (event.t === "tool_invocation") {
112
- if (event.input !== undefined) current = { ...current, calls: new Map(current.calls).set(event.call_id, { name: event.name, input: event.input }) };
113
- continue;
114
- }
115
- if (event.t === "guidance" && event.reason === "churn") {
116
- current = { ...current, failures: new Map([...current.failures].map(([group, run]) =>
117
- [group, event.sources.includes(run.sources[0]!) ? { ...run, notified: true } : run])) }; continue;
118
- }
119
- if (event.t === "workspace_observation" && event.changed) {
120
- current = changedPremises(current, true); continue;
121
- }
122
- if (event.t === "checkpoint" && event.phase === "applied") {
123
- for (const [path, change] of Object.entries(event.changes))
124
- current = changedPremises(current, change.before?.sha256 !== change.after?.sha256 || change.before?.mode !== change.after?.mode,
125
- identify(path), change.after === null ? "absent" : change.after.sha256);
126
- continue;
127
- }
128
- if (event.t !== "tool_result") continue;
129
- const call = current.calls.get(event.call_id), calls = new Map(current.calls); calls.delete(event.call_id);
130
- current = { ...current, calls };
131
- const effect = event.effect;
132
- if (effect?.kind === "edit" || effect?.kind === "read") {
133
- const resource = identify(effect.path), previous = current.versions.get(resource);
134
- const version = effect.kind === "edit" ? effect.sha256_after : effect.sha256;
135
- const changed = effect.kind === "edit" ? effect.sha256_before !== effect.sha256_after : digest(previous) && previous !== version;
136
- current = changedPremises({ ...current, versions: new Map(current.versions).set(resource, version) }, changed,
137
- resource, digest(version) ? version : undefined);
138
- }
139
- if (!call) continue;
140
- const group = groupKey(call, event, identify);
141
- if (event.ok) {
142
- const failures = new Map(current.failures); failures.delete(group);
143
- current = { ...current, failures }; continue;
144
- }
145
- const identity = failureIdentity(call, event, current, identify);
146
- if (!identity) continue;
147
- const key = canonicalValue(identity), previous = current.failures.get(group);
148
- const run: FailureRun = previous?.key === key ? { ...previous, count: previous.count + 1,
149
- sources: [...previous.sources.slice(0, 2), event.id] }
150
- : { identity, key, count: 1, sources: [event.id], notified: false };
151
- current = { ...current, failures: new Map(current.failures).set(group, run) };
152
- }
153
- return current;
154
- }
155
-
156
- export function recoveryReview(state: RecoveryState, tools?: readonly string[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
157
- for (const run of state.failures.values()) {
158
- if (run.count < RECOVERY_FAILURES || run.notified) continue;
159
- const identity = run.identity;
160
- // Recompute trusted instructions; never promote arbitrary tool error text to harness instructions.
161
- const diagnostic = failureDiagnostic(identity.code, "", { execution: identity.execution, effects: identity.effects });
162
- const policy = tools === undefined ? diagnostic : scopeFailureGuidance(diagnostic, tools);
163
- return { reason: "churn", sources: run.sources,
164
- text: `Harness: ${run.count} attempts share an unchanged failure premise in the recorded facts. ` +
165
- `Tool family ${JSON.stringify(identity.family)}; resource label (data) ${JSON.stringify(identity.resource.slice(0, 160))}; ` +
166
- `code ${identity.code}; version ${identity.version}; execution ${identity.execution}; effects ${identity.effects}. ` +
167
- `${policy.next} ` + (policy.retry === "after_inspection" ? "Inspect actual state for partial effects before any replay. " : "") +
168
- "Identify which relevant premise changed before another attempt; changing replacement wording is not a new premise. " +
169
- (identity.version === "unknown" ? "The resource version is unknown; this notice does not prove external state is unchanged. " : "") +
170
- "This is recovery advice, not a task-failure verdict or authorization to replay. Original result references: " + run.sources.join(", ") + "." };
171
- }
172
- return null;
173
- }