@visual-z/xeno 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,111 +0,0 @@
1
- /**
2
- * Prompt-template expansion. Pure: the file is read in store.
3
- *
4
- * `{{name}}` is filled with the remainder of `/template rest`. An empty
5
- * remainder keeps the placeholder and names it — emptying it would drop a
6
- * sentence from the prompt with no trace (feat-skills-and-prompts design).
7
- */
8
-
9
- import { parseFrontmatter } from "./frontmatter.ts";
10
-
11
- export interface PromptTemplate {
12
- readonly name: string;
13
- readonly description: string;
14
- readonly argumentHint?: string;
15
- readonly content: string;
16
- readonly filePath: string;
17
- }
18
-
19
- const PLACEHOLDER = /\{\{([A-Za-z_][A-Za-z0-9_-]*)\}\}/g;
20
-
21
- export function expandTemplate(
22
- content: string,
23
- arg: string,
24
- ): { text: string; diagnostics: readonly string[] } {
25
- const names: string[] = [];
26
- const seen = new Set<string>();
27
- for (const match of content.matchAll(PLACEHOLDER)) {
28
- const name = match[1]!;
29
- if (!seen.has(name)) {
30
- seen.add(name);
31
- names.push(name);
32
- }
33
- }
34
- return {
35
- text: substituteArgs(content, parseCommandArgs(arg), arg),
36
- diagnostics: arg.length ? [] : names.map((name) => `unfilled placeholder: ${name}`),
37
- };
38
- }
39
-
40
- /** Ported from pi core/prompt-templates.ts (MIT, 02eed88); quote handling is Pi's, not shell execution. */
41
- export function parseCommandArgs(text: string): string[] {
42
- const args: string[] = [];
43
- let current = "", quote: string | null = null;
44
- for (const char of text) {
45
- if (quote) { if (char === quote) quote = null; else current += char; }
46
- else if (char === '"' || char === "'") quote = char;
47
- else if (/\s/.test(char)) { if (current) { args.push(current); current = ""; } }
48
- else current += char;
49
- }
50
- if (current) args.push(current);
51
- return args;
52
- }
53
-
54
- /** Pi positional/default/slice syntax and legacy named syntax share one pass: inserted text stays literal. */
55
- function substituteArgs(content: string, args: readonly string[], legacy: string): string {
56
- const all = args.join(" ");
57
- return content.replace(/\{\{([A-Za-z_][A-Za-z0-9_-]*)\}\}|\$\{(\d+|ARGUMENTS|@):-([^}]*)\}|\$\{@:(\d+)(?::(\d+))?\}|\$(ARGUMENTS|@|\d+)/g,
58
- (match: string, named: string | undefined, target: string | undefined, fallback: string | undefined,
59
- start: string | undefined, length: string | undefined, simple: string | undefined) => {
60
- if (named) return legacy.length ? legacy : match;
61
- if (target) return (target === "@" || target === "ARGUMENTS" ? all : args[Number(target) - 1]) || fallback || "";
62
- if (start !== undefined) {
63
- const from = Math.max(0, Number(start) - 1);
64
- return args.slice(from, length === undefined ? undefined : from + Number(length)).join(" ");
65
- }
66
- if (simple === "@" || simple === "ARGUMENTS") return all;
67
- return args[Number(simple) - 1] ?? "";
68
- });
69
- }
70
-
71
- export function parsePromptTemplate(input: {
72
- readonly filePath: string;
73
- readonly text: string;
74
- }): { template: PromptTemplate | null; diagnostics: readonly string[] } {
75
- const parsed = parseFrontmatter(input.text);
76
- const diagnostics = [...parsed.diagnostics];
77
- const base = fileName(input.filePath);
78
- const name = base.replace(/\.md$/i, "").toLowerCase();
79
- if (name.length === 0) {
80
- diagnostics.push(`${input.filePath}: prompt template has no name`);
81
- return { template: null, diagnostics };
82
- }
83
-
84
- const descField = parsed.fields["description"];
85
- let description = typeof descField === "string" ? descField.trim() : "";
86
- if (description.length === 0) {
87
- const first = parsed.body.split("\n").find((line) => line.trim().length > 0);
88
- description = first === undefined ? "" : first.trim();
89
- }
90
-
91
- const hintField = parsed.fields["argument-hint"];
92
- const argumentHint = typeof hintField === "string" ? hintField : undefined;
93
-
94
- return {
95
- template: {
96
- name,
97
- description,
98
- content: parsed.body,
99
- filePath: input.filePath,
100
- ...(argumentHint !== undefined ? { argumentHint } : {}),
101
- },
102
- diagnostics,
103
- };
104
- }
105
-
106
- /** Path basename without node:path, so this module stays core-pure. */
107
- function fileName(path: string): string {
108
- const trimmed = path.replace(/[\\/]+$/, "");
109
- const slash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
110
- return slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
111
- }
@@ -1,94 +0,0 @@
1
- /** R13 / D11: an answer is bound to its original active invocation, never guessed. */
2
-
3
- // Product limits keep one interaction reviewable, not measured model limits.
4
- export const MAX_QUESTIONS = 3;
5
- export const MAX_QUESTION_CHOICES = 8;
6
- export interface QuestionChoice { readonly id: string; readonly label: string; readonly description?: string }
7
- export interface QuestionSpec {
8
- readonly id: string;
9
- readonly header: string;
10
- readonly question: string;
11
- readonly options: readonly QuestionChoice[];
12
- readonly multiple: boolean;
13
- readonly custom: boolean;
14
- }
15
- export interface QuestionRequest {
16
- readonly request_id: string;
17
- readonly turn_id: string;
18
- readonly call_id: string;
19
- readonly questions: readonly QuestionSpec[];
20
- }
21
- export interface QuestionAnswer { readonly id: string; readonly choices: readonly string[]; readonly text?: string }
22
- export interface QuestionReply {
23
- readonly request_id: string;
24
- readonly turn_id: string;
25
- readonly answers: readonly QuestionAnswer[];
26
- }
27
- function object(value: unknown, label: string, fields: readonly string[]): Record<string, unknown> {
28
- if (!value || typeof value !== "object" || Array.isArray(value)) throw Error(`${label} must be an object`);
29
- const result = value as Record<string, unknown>;
30
- if (Object.keys(result).some(key => !fields.includes(key))) throw Error(`${label} contains an unknown field`);
31
- return result;
32
- }
33
- function string(value: unknown, label: string, max = 2000): string {
34
- if (typeof value !== "string" || !value.trim() || value.length > max || /[\0\x1b]/.test(value)) throw Error(`${label} must be nonempty plain text (max ${max} characters)`);
35
- return value;
36
- }
37
- function identifier(value: unknown, label: string): string {
38
- const id = string(value, label, 64);
39
- if (!/^[a-z][a-z0-9_-]*$/.test(id)) throw Error(`${label} must be a stable lowercase identifier`);
40
- return id;
41
- }
42
-
43
- export function parseQuestions(value: unknown): QuestionSpec[] {
44
- const input = object(value, "question input", ["questions"]);
45
- const questions = input["questions"];
46
- if (!Array.isArray(questions) || questions.length < 1 || questions.length > MAX_QUESTIONS) throw Error(`question requires 1..${MAX_QUESTIONS} questions`);
47
- const parsed = questions.map(value => {
48
- const item = object(value, "question", ["id", "header", "question", "options", "multiple", "custom"]);
49
- const options = item["options"] ?? [];
50
- if (!Array.isArray(options) || options.length > MAX_QUESTION_CHOICES) throw Error(`question options must contain at most ${MAX_QUESTION_CHOICES} choices`);
51
- const choices = options.map(value => {
52
- const choice = object(value, "question option", ["id", "label", "description"]);
53
- return { id: identifier(choice["id"], "option id"), label: string(choice["label"], "option label", 120),
54
- ...(choice["description"] !== undefined ? { description: string(choice["description"], "option description", 600) } : {}) };
55
- });
56
- if (new Set(choices.map(choice => choice.id)).size !== choices.length) throw Error("question option ids must be unique");
57
- if (new Set(choices.map(choice => choice.label)).size !== choices.length) throw Error("question option labels must be unique");
58
- for (const key of ["multiple", "custom"]) if (item[key] !== undefined && typeof item[key] !== "boolean") throw Error(`question ${key} must be boolean`);
59
- const multiple = item["multiple"] === true, custom = item["custom"] !== false;
60
- if (!choices.length && !custom) throw Error("question must offer choices or custom text");
61
- return { id: identifier(item["id"], "question id"), header: string(item["header"], "question header", 40),
62
- question: string(item["question"], "question text"), options: choices, multiple, custom };
63
- });
64
- if (new Set(parsed.map(question => question.id)).size !== parsed.length) throw Error("question ids must be unique");
65
- return parsed;
66
- }
67
-
68
- export function questionRequest(turnId: string, callId: string, questions: readonly QuestionSpec[]): QuestionRequest {
69
- if (!turnId || !callId || /[\r\n\0]/.test(turnId + callId)) throw Error("question requires a trusted active turn and call identity");
70
- // Tuple encoding avoids slash/delimiter collisions in opaque runtime IDs.
71
- return { request_id: JSON.stringify([turnId, callId]), turn_id: turnId, call_id: callId, questions: structuredClone(questions) };
72
- }
73
-
74
- export function validateQuestionReply(request: QuestionRequest, value: unknown): QuestionReply {
75
- const reply = object(value, "question reply", ["request_id", "turn_id", "answers"]);
76
- if (reply["request_id"] !== request.request_id || reply["turn_id"] !== request.turn_id) throw Error("question reply belongs to a different invocation or turn");
77
- const answers = reply["answers"];
78
- if (!Array.isArray(answers) || answers.length !== request.questions.length) throw Error("question reply must answer every question exactly once");
79
- const parsed = answers.map(value => {
80
- const answer = object(value, "question answer", ["id", "choices", "text"]);
81
- const id = identifier(answer["id"], "answer id"), question = request.questions.find(question => question.id === id);
82
- if (!question) throw Error("question reply contains an unknown question id");
83
- const choices = answer["choices"];
84
- if (!Array.isArray(choices) || choices.some(choice => typeof choice !== "string" || !question.options.some(option => option.id === choice)) || new Set(choices).size !== choices.length) throw Error("question reply contains unknown or duplicate choices");
85
- const text = answer["text"] === undefined ? undefined : string(answer["text"], "answer text", 10000);
86
- if (text !== undefined && !question.custom) throw Error("custom text is not permitted for this question");
87
- const count = choices.length + (text === undefined ? 0 : 1);
88
- if (count === 0 || (!question.multiple && count !== 1)) throw Error("question reply requires one answer, or multiple only when enabled");
89
- return { id, choices: [...choices] as string[], ...(text !== undefined ? { text } : {}) };
90
- });
91
- if (new Set(parsed.map(answer => answer.id)).size !== parsed.length) throw Error("question reply contains duplicate question ids");
92
- return { request_id: request.request_id, turn_id: request.turn_id,
93
- answers: request.questions.map(question => parsed.find(answer => answer.id === question.id)!) };
94
- }
package/src/core/queue.ts DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * When a typed message may enter the log.
3
- *
4
- * The queue itself is UI state: a line the user may still retract. The log
5
- * is what happened. nextDelivery is the only decision — it says which
6
- * queued line is legal to deliver at this phase, or none. The caller
7
- * records the user event; this file never does.
8
- *
9
- * after_tools is the first legal moment to insert a user message, for the
10
- * same pairing reason switchModel already refuses a half-round: a tool_use
11
- * without its tool_result is a 400 (project.ts UNPAIRED_TOOL_USE).
12
- */
13
-
14
- import { assertNever } from "./exhaustive.ts";
15
- import type { ImageRef } from "./images.ts";
16
-
17
- export type QueueKind = "steering" | "followup";
18
- export type DeliveryMode = "all" | "one-at-a-time";
19
-
20
- export interface QueuedMessage {
21
- readonly kind: QueueKind;
22
- readonly text: string;
23
- readonly images?: readonly ImageRef[];
24
- }
25
-
26
- /**
27
- * Where the turn is. after_tools means every tool_use of this assistant
28
- * round already has a result. turn_end means send() has returned. idle
29
- * is the editor waiting — leftover lines still deliver, steering first.
30
- */
31
- export type Phase = "after_tools" | "turn_end" | "idle";
32
-
33
- /**
34
- * Which queued message may be written to the log right now.
35
- *
36
- * Same kind stays in enqueue order. Steering as a group always precedes
37
- * follow-up: a follow-up is defined as "after the whole send", so it
38
- * cannot win while a steering line is still waiting.
39
- */
40
- export function nextDelivery(
41
- queue: readonly QueuedMessage[],
42
- phase: Phase,
43
- ): QueuedMessage | null {
44
- switch (phase) {
45
- case "after_tools":
46
- return firstOfKind(queue, "steering");
47
- case "turn_end":
48
- case "idle":
49
- return firstOfKind(queue, "steering") ?? firstOfKind(queue, "followup");
50
- default:
51
- return assertNever(phase, "nextDelivery");
52
- }
53
- }
54
-
55
- function firstOfKind(
56
- queue: readonly QueuedMessage[],
57
- kind: QueueKind,
58
- ): QueuedMessage | null {
59
- for (const item of queue) {
60
- if (item.kind === kind) return item;
61
- }
62
- return null;
63
- }
64
-
65
- /** Bash remains an atomic follow-up; batching never turns commands into model prompts. */
66
- export function deliveryBatch(queue: readonly QueuedMessage[], phase: Phase,
67
- modes: { steering?: DeliveryMode; followup?: DeliveryMode }): readonly QueuedMessage[] {
68
- const first = nextDelivery(queue, phase);
69
- if (first === null) return [];
70
- if (modes[first.kind] !== "all" || first.text.startsWith("!")) return [first];
71
- const batch: QueuedMessage[] = [];
72
- for (const item of queue) {
73
- if (item.kind !== first.kind) continue;
74
- if (item.text.startsWith("!")) break;
75
- batch.push(item);
76
- }
77
- return batch;
78
- }
@@ -1,24 +0,0 @@
1
- /** N.12: recoverable file bounds are observation metadata, not model guesses. */
2
- export function readBounds(total: number, first: unknown, last: unknown):
3
- { ok: true; start: number; end: number; notice: string } | { ok: false; message: string } {
4
- if (!Number.isSafeInteger(total) || total < 1) throw new RangeError("split text must contain at least one line");
5
- const start = first === undefined ? 1 : first, end = last === undefined ? total : last;
6
- const bounds = `total lines: ${total}; valid range: 1-${total}`;
7
- if (typeof start !== "number" || typeof end !== "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < 1) {
8
- return { ok: false, message: `read start and end must be positive safe integers (${bounds})` };
9
- }
10
- if (start > total) return { ok: false, message: `read start ${start} is beyond end of file (${bounds}); choose a start in that range` };
11
- if (end < start) return { ok: false, message: `read end ${end} precedes start ${start} (${bounds})` };
12
- const actualEnd = Math.min(end, total);
13
- return { ok: true, start, end: actualEnd, notice: readNotice(start, actualEnd, total) };
14
- }
15
-
16
- /**
17
- * The selection header. `budget` names the byte limit when it, rather than the
18
- * requested range, ended the selection, so "next start" is never mistaken for
19
- * the end of what was asked.
20
- */
21
- export function readNotice(start: number, end: number, total: number, budget?: number): string {
22
- const rest = end < total ? `next start: ${end + 1}${budget === undefined ? "" : `; stopped at the ${budget}-byte output budget`}` : "end of file";
23
- return `[Read selection: lines ${start}-${end}; total lines: ${total}; ${rest}.]`;
24
- }
@@ -1,106 +0,0 @@
1
- /** R2 / D8: source bytes, an unclipped observation, and the visible projection have distinct identities. */
2
- import type { ArtifactRef } from "./events.ts";
3
- import { artifactRecovery } from "./prompt-policy.ts";
4
- import { headTailSelection, isObservationArtifact as ref, isBytePreview, type BytePreview } from "./observation-view.ts";
5
-
6
- export interface ObservationReceipt {
7
- version: 1;
8
- source: { host: string; workspace: string; resource: string; sha256: string; raw: ArtifactRef };
9
- artifact: ArtifactRef;
10
- /** R4: content-addressed version premise, separate from this formatted observation. */
11
- descriptor?: ArtifactRef;
12
- selection: { start: number; end: number; total_lines: number };
13
- preview: BytePreview;
14
- }
15
-
16
- const encoder = new TextEncoder();
17
- const size = (text: string) => encoder.encode(text).length;
18
- /** Existing assumed preview cap; archival removes data loss without changing the display policy. */
19
- const READ_MAX_LINE_CHARS = 2000;
20
-
21
- export function readObservationText(header: string, lines: readonly string[], start: number): string {
22
- return header + lines.map((line, i) => `${start + i}\t${line}`).join("\n");
23
- }
24
-
25
- /** Map two successive cuts (per line and total bytes) back to the same immutable observation. */
26
- export function previewReadObservation(header: string, lines: readonly string[], start: number, artifact: string,
27
- maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): { text: string; preview: ObservationReceipt["preview"]; semantic: string } {
28
- const spans: { view: number; source: number; bytes: number }[] = [];
29
- let text = "", source = 0, view = 0;
30
- const mapped = (value: string, offset: number) => {
31
- const bytes = size(value);
32
- if (bytes) spans.push({ view, source: offset, bytes });
33
- text += value; view += bytes;
34
- };
35
- const synthetic = (value: string) => { text += value; view += size(value); };
36
- mapped(header, 0); source += size(header);
37
- for (let i = 0; i < lines.length; i++) {
38
- const line = lines[i]!, prefix = `${start + i}\t`;
39
- let end = Math.min(line.length, maxLineChars);
40
- // JavaScript string indices are UTF-16; a character can occupy a surrogate pair.
41
- if (end > 0 && end < line.length && /[\uD800-\uDBFF]/.test(line[end - 1]!)) end--;
42
- const kept = prefix + line.slice(0, end);
43
- mapped(kept, source);
44
- if (end < line.length) synthetic(` [line truncated: ${line.length} chars; ${artifactRecovery(artifact, source + size(kept), tools)}]`);
45
- source += size(prefix + line);
46
- if (i < lines.length - 1) { mapped("\n", source); source++; }
47
- }
48
- const bytes = encoder.encode(text);
49
- const selected = headTailSelection(bytes, bytes, bytes.length, maxBytes,
50
- omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
51
- const visible: [number, number][] = [];
52
- for (const [lo, hi] of selected.visible) for (const span of spans) {
53
- const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
54
- if (end <= begin) continue;
55
- const a = span.source + begin - span.view, b = span.source + end - span.view;
56
- const last = visible.at(-1);
57
- if (last?.[1] === a) last[1] = b;
58
- else visible.push([a, b]);
59
- }
60
- let next = 0;
61
- for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
62
- const full = encoder.encode(readObservationText(header, lines, start));
63
- const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(Math.max(lo, size(header)), Math.max(hi, size(header))))).join("");
64
- return { text: selected.text, semantic, preview: { unit: "utf8_bytes", complete: next === source,
65
- visible, next_offset: next === source ? null : next } };
66
- }
67
-
68
- /**
69
- * How many of `lines` a read shows whole within `maxBytes`. OpenCode and Pi cut
70
- * a file read at a line boundary and name the next offset; eliding the middle
71
- * lets a receipt claim "end of file" while hiding the lines between, so the
72
- * model re-reads them and the stall ledger, trusting the claimed range, counts
73
- * that as no progress. At least one line is kept: a single line over budget
74
- * is still cut inside itself, with its recovery reference.
75
- */
76
- export function linesWithin(header: (count: number) => string, lines: readonly string[], start: number,
77
- maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): number {
78
- // Rendered without a byte cut, so only the budget decides; a line cut at
79
- // maxLineChars is already part of the rendering. Any 64-hex artifact name
80
- // renders recovery references at their final length.
81
- const fits = (count: number) => size(previewReadObservation(header(count), lines.slice(0, count), start, "0".repeat(64),
82
- Number.MAX_SAFE_INTEGER, maxLineChars, tools).text) <= maxBytes;
83
- if (lines.length === 0 || fits(lines.length)) return lines.length;
84
- let low = 1, high = lines.length - 1;
85
- while (low < high) {
86
- const mid = Math.ceil((low + high) / 2);
87
- if (fits(mid)) low = mid; else high = mid - 1;
88
- }
89
- return low;
90
- }
91
-
92
- const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
93
- const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
94
- const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
95
-
96
- /** Optional on old logs, strict when present. A cursor cannot certify bytes outside its artifact. */
97
- export function isObservationReceipt(value: unknown): value is ObservationReceipt {
98
- if (!record(value) || value["version"] !== 1 || !ref(value["artifact"])) return false;
99
- if (value["descriptor"] !== undefined && !ref(value["descriptor"])) return false;
100
- const source = value["source"], selection = value["selection"], preview = value["preview"];
101
- if (!record(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") ||
102
- !digest(source["sha256"]) || !ref(source["raw"]) || source["sha256"] !== source["raw"].sha256) return false;
103
- if (!record(selection) || !natural(selection["start"]) || selection["start"] < 1 || !natural(selection["end"]) ||
104
- !natural(selection["total_lines"]) || selection["end"] < selection["start"] || selection["end"] > selection["total_lines"]) return false;
105
- return isBytePreview(preview, value["artifact"].bytes);
106
- }
@@ -1,74 +0,0 @@
1
- /** R9 / D8: navigation into original ancestry, never a semantic summary or permission grant. */
2
- import type { LogEvent } from "./events.ts";
3
- import { historyEntries, type HistoryEntry } from "./history-query.ts";
4
- import { isEnvironmentReceipt } from "./environment.ts";
5
- import { resourceKey } from "./evidence-links.ts";
6
- import type { ToolDiagnostic } from "./tool-contract.ts";
7
-
8
- export const RECOVERY_SECTIONS = ["users", "failures", "environment", "next_actions"] as const;
9
- export type RecoverySection = typeof RECOVERY_SECTIONS[number];
10
- export const RECOVERY_KINDS = { users: "user", failures: "failure", environment: "environment", next_actions: "next_action" } as const;
11
- export type RecoveryNodes = readonly { node: number; events: readonly LogEvent[] }[];
12
- export type RecoveryReference = {
13
- node: number; event: string; kind: "user" | "failure" | "environment" | "next_action";
14
- /** This is the recorded role, not proof of current authorization or human authorship. */
15
- provenance: "user_message" | "observation" | "model_annotation";
16
- abandoned?: true; tool?: string; work?: string;
17
- resources?: readonly string[];
18
- /** A failed request can name a resource without having observed it. */
19
- requested_resource?: string;
20
- diagnostic?: Pick<ToolDiagnostic, "code" | "execution" | "effects">;
21
- host?: string; workspace?: string; cwd?: string; program?: string;
22
- };
23
- export type RecoverySources = Record<RecoverySection, RecoveryReference[]>;
24
- export type RecoveryRanges = Record<RecoverySection, { total: number; omitted: number }>;
25
-
26
- /** Newest first within each role; no keywords decide which user sentence is a constraint. */
27
- export function recoverySources(nodes: RecoveryNodes, entries: readonly HistoryEntry[] = historyEntries(nodes)): RecoverySources {
28
- const abandoned = new Set(nodes.flatMap(part => part.events.flatMap(event => event.t === "interrupt" ? [event.user_id] : [])));
29
- const result: RecoverySources = { users: [], failures: [], environment: [], next_actions: [] };
30
- for (const entry of entries.toReversed()) {
31
- const source = { node: entry.node, event: entry.event };
32
- if (entry.kind === "user") result.users.push({ ...source, kind: "user", provenance: "user_message",
33
- ...(abandoned.has(entry.event) ? { abandoned: true } : {}) });
34
- if (entry.kind === "next_action") result.next_actions.push({ ...source, kind: "next_action", provenance: "model_annotation",
35
- ...(entry.work_ids?.[0] ? { work: entry.work_ids[0] } : {}) });
36
- if (entry.kind !== "tool_result") continue;
37
- if (entry.ok === false) {
38
- const input = entry.input !== null && typeof entry.input === "object" ? entry.input as Record<string, unknown> : {};
39
- const requested = typeof input["path"] === "string" ? input["path"] : typeof input["cwd"] === "string" ? input["cwd"] : undefined;
40
- const diagnostic = entry.diagnostic;
41
- result.failures.push({ ...source, kind: "failure", provenance: "observation", ...(entry.tool ? { tool: entry.tool } : {}),
42
- ...(entry.resources?.length ? { resources: entry.resources } : {}),
43
- ...(requested ? { requested_resource: resourceKey(requested) } : {}),
44
- ...(diagnostic ? { diagnostic: { code: diagnostic.code,
45
- ...(diagnostic.execution ? { execution: diagnostic.execution } : {}), ...(diagnostic.effects ? { effects: diagnostic.effects } : {}) } } : {}) });
46
- }
47
- if (entry.tool === "environment") {
48
- const receipt = entry.data;
49
- // Only a validated actual receipt contributes identity. Text that looks
50
- // like a host or a successful probe does not become a machine fact (D11).
51
- const identity = isEnvironmentReceipt(receipt) && entry.effect?.kind === "command" && entry.effect.exit === receipt.evidence.exit
52
- ? { host: receipt.source.host, workspace: receipt.source.workspace, cwd: receipt.source.cwd, program: receipt.query.program } : {};
53
- result.environment.push({ ...source, kind: "environment", provenance: "observation", ...identity });
54
- }
55
- }
56
- return result;
57
- }
58
-
59
- /** Source labels are stable even when the runtime attaches a fresh validity view. */
60
- export function recoveryPage(sources: RecoverySources, entries: readonly HistoryEntry[], section: RecoverySection, offset: number, limit: number): {
61
- total: number; offset: number; next: number | null; items: { source: RecoveryReference; original: HistoryEntry }[];
62
- } {
63
- if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
64
- throw new RangeError("recovery offset must be nonnegative and limit must be 1..100");
65
- }
66
- const indexed = new Map(entries.map(entry => [entry.event, entry]));
67
- const references = sources[section], end = Math.min(references.length, offset + limit);
68
- return { total: references.length, offset, next: end < references.length ? end : null,
69
- items: references.slice(offset, end).map(source => {
70
- const original = indexed.get(source.event);
71
- if (!original || original.node !== source.node) throw new Error("recovery source is absent from the selected ancestry");
72
- return { source, original };
73
- }) };
74
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Brings the ripgrep regex dialect to JS RegExp where the two differ in
3
- * spelling, not meaning.
4
- *
5
- * grep runs ripgrep when it is on PATH and a TypeScript walk otherwise, and
6
- * the model writes for ripgrep (Rust regex), where a leading `(?i)` is the
7
- * usual way to ask for case-insensitivity. JS rejects that spelling but
8
- * accepts the scoped form `(?i:...)`, so a leading global group is rewritten
9
- * into a scoped group around the rest of the pattern. Without this the same
10
- * call works or fails depending on whether rg happens to be installed.
11
- *
12
- * Only the leading form and only flags JS modifiers support (i, m, s) are
13
- * rewritten; anything else is passed through so RegExp reports it as it is.
14
- */
15
- const LEADING_FLAGS = /^\(\?([ims]+)\)/;
16
-
17
- export function jsPatternFromRipgrep(pattern: string): string {
18
- const match = LEADING_FLAGS.exec(pattern);
19
- if (match === null) return pattern;
20
- const flags = match[1]!;
21
- // A repeated flag is a SyntaxError inside a modifier group; ripgrep
22
- // tolerates it, so duplicates are dropped rather than surfaced.
23
- const unique = [...new Set(flags)].join("");
24
- return `(?${unique}:${pattern.slice(match[0].length)})`;
25
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * D6 / Q.26: finalization spends the existing budget and cannot admit new work.
3
- * `Infinity` is no cap (S.6 item 1): there is no final request to reserve, and
4
- * the stall watchdog is what ends a run that stops producing evidence.
5
- */
6
- export function requestPhase(index: number, limit: number): "work" | "finalize" | "exhausted" {
7
- if (!(Number.isSafeInteger(limit) || limit === Infinity) || limit < 1 || !Number.isSafeInteger(index) || index < 0)
8
- throw new RangeError("request budget requires a positive integer or unbounded limit and a nonnegative index");
9
- if (index >= limit) return "exhausted";
10
- // One request cannot both investigate and close. Preserve that work opportunity;
11
- // its deterministic outcome is the exit, without a second paid request.
12
- return limit > 1 && index === limit - 1 ? "finalize" : "work";
13
- }
14
-
15
- export function finalizationNotice(): string {
16
- return "Harness: this is the final request in the existing budget. Tools are disabled by the host. Give the user a concise answer from the observations already recorded: what is established, what remains unresolved, and the next useful action. Do not claim unobserved execution or verified completion.";
17
- }
18
-
19
- /** Without a request cap the stall watchdog is the only backstop, so it cannot be off (S.6 item 1). */
20
- export function validateRequestBudget(limit: number, stallLimit: number): void {
21
- requestPhase(0, limit);
22
- if (limit === Infinity && stallLimit === 0) throw new RangeError("an unbounded request budget requires the stall watchdog");
23
- }
@@ -1,87 +0,0 @@
1
- /** Q23 / D9: diagnostic observations, never a new anti-stall or success policy. */
2
- import type { LogEvent, ToolResultEvent } from "./events.ts";
3
- import { deriveAcceptance, type AcceptanceState } from "./acceptance.ts";
4
- import { advanceRecovery, emptyRecoveryState, failureIdentity } from "./failure-recovery.ts";
5
- import { canonicalValue } from "./continuation.ts";
6
- import { changedPaths } from "./checkpoints.ts";
7
-
8
- export type RequestSource = { node: number; event: string };
9
- export type RequestRecord = { node: number; event: LogEvent };
10
- export type RequestProgress = Record<"observations" | "artifact_changes" | "acceptance_advances" |
11
- "new_diagnostics" | "unchanged_retries" | "unknown_diagnostics", RequestSource[]>;
12
- const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
13
-
14
- function observationDigest(event: ToolResultEvent): string | undefined {
15
- if (event.observation_sha256 !== undefined) return event.observation_sha256;
16
- // Command receipts archive stdout/stderr before the timing/session header is
17
- // rendered. Only that explicit matching output reference is a safe fallback.
18
- const data = event.data;
19
- if (event.effect?.kind !== "command" || !event.content_ref || !object(data) || data["capture"] === "partial") return undefined;
20
- const output = data["output"];
21
- return object(output) &&
22
- output["sha256"] === event.content_ref.sha256 && output["bytes"] === event.content_ref.bytes ? event.content_ref.sha256 : undefined;
23
- }
24
-
25
- export function deriveRequestProgress(records: readonly RequestRecord[]): RequestProgress {
26
- const result: RequestProgress = { observations: [], artifact_changes: [], acceptance_advances: [],
27
- new_diagnostics: [], unchanged_retries: [], unknown_diagnostics: [] };
28
- const observations = new Set<string>(), versions = new Set<string>(), failures = new Set<string>();
29
- let acceptance: AcceptanceState | undefined;
30
- let recovery = emptyRecoveryState();
31
- const version = (path: string, before: string | null, after: string | null, source: RequestSource) => {
32
- const key = (sha: string | null) => canonicalValue([path, sha]);
33
- versions.add(key(before));
34
- if (!versions.has(key(after))) result.artifact_changes.push(source);
35
- versions.add(key(after));
36
- };
37
- for (const { node, event } of records) {
38
- const source = { node, event: event.id };
39
- const before = acceptance;
40
- acceptance = deriveAcceptance(acceptance, [event]);
41
- if (acceptance && before?.contract_event === acceptance.contract_event && acceptance.criteria.some(item =>
42
- (item.status === "pass" || item.status === "accepted") &&
43
- before.criteria.find(old => old.id === item.id)?.status !== item.status)) result.acceptance_advances.push(source);
44
- if (event.t === "tool_result") {
45
- const effect = event.effect;
46
- const call = recovery.calls.get(event.call_id);
47
- if (effect?.kind === "edit") version(effect.path, effect.sha256_before, effect.sha256_after, source);
48
- if (effect?.kind === "read") versions.add(canonicalValue([effect.path, effect.sha256]));
49
- // Semantic digests exclude command timing/preview metadata. Legacy raw
50
- // output hashes cannot prove a new diagnostic and stay explicitly unknown.
51
- const digest = observationDigest(event);
52
- if (!event.ok) {
53
- const identity = call ? failureIdentity(call, event, recovery, path => path) : null;
54
- if (digest === undefined || identity === null) result.unknown_diagnostics.push(source);
55
- else {
56
- const key = canonicalValue([identity, digest]);
57
- (failures.has(key) ? result.unchanged_retries : result.new_diagnostics).push(source);
58
- failures.add(key);
59
- }
60
- } else if ((effect?.kind === "read" || effect?.kind === "command" ||
61
- ["grep", "find", "ls", "query", "history", "environment"].includes(call?.name ?? "")) &&
62
- digest !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 && !observations.has(digest)) {
63
- result.observations.push(source); observations.add(digest);
64
- }
65
- if (effect?.kind === "command") for (const output of effect.artifacts?.outputs ?? []) {
66
- if (output.after.state === "observed") version(output.path,
67
- output.before.state === "observed" ? output.before.sha256 : null, output.after.sha256, source);
68
- }
69
- } else if (event.t === "checkpoint" && event.phase === "applied") {
70
- for (const path of changedPaths(event.changes)) {
71
- const change = event.changes[path]!;
72
- version(path, change.before?.sha256 ?? null, change.after?.sha256 ?? null, source);
73
- }
74
- }
75
- recovery = advanceRecovery(recovery, [event], path => path);
76
- }
77
- // One receipt may change several artifacts or satisfy several criteria.
78
- for (const key of Object.keys(result) as (keyof RequestProgress)[]) {
79
- const seen = new Set<string>();
80
- result[key] = result[key].filter(source => {
81
- const id = canonicalValue(source);
82
- if (seen.has(id)) return false;
83
- seen.add(id); return true;
84
- });
85
- }
86
- return result;
87
- }