@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,139 +0,0 @@
1
- /** O.11: declared criteria, command evidence and human acceptance are separate. */
2
- import type { LogEvent } from "./events.ts";
3
- import type { Evidence } from "./handoff.ts";
4
- import { evidenceStale } from "./evidence.ts";
5
- import { workspaceChanged } from "./checkpoints.ts";
6
- import { parseRuntimeTarget, type RuntimeTarget, type TargetAcceptance } from "./runtime-target.ts";
7
-
8
- /**
9
- * A command criterion either tests the change -- it fails before the work and
10
- * passes after it (SWE-bench's FAIL_TO_PASS) -- or, with `guard`, protects
11
- * what the work must not break: it passes before and after (PASS_TO_PASS),
12
- * such as "the test file is unchanged" (S.7).
13
- */
14
- export type AcceptanceCriterion = { id: string; description: string } &
15
- ({ kind: "command"; command: string; guard?: true } | { kind: "human"; target?: string });
16
- export type AcceptanceContract = { version: 1; id: string; scope?: "request" | "changes"; criteria: AcceptanceCriterion[];
17
- source?: { path: string; sha256: string }; targets?: RuntimeTarget[] };
18
- export type AcceptanceState = {
19
- contract_id: string; contract_event: string; request: string; scope: "request" | "changes"; changed: boolean;
20
- criteria: (AcceptanceCriterion & { status: "not_run" | "pass" | "fail" | "stale" | "awaiting_human" | "accepted"; evidence?: Evidence; accepted_event?: string; accepted_binding?: string })[];
21
- targets?: TargetAcceptance[];
22
- status: "needs_work" | "awaiting_human" | "verified" | "not_applicable";
23
- };
24
- /**
25
- * The longest check command a contract accepts. Drafted checks are often one
26
- * inline script (`python3 -c '...'`); a real 599-character structure check
27
- * was refused at the earlier 500 (fix-goal-read-only-draft). Bounded so a
28
- * frozen contract stays small.
29
- */
30
- export const ACCEPTANCE_COMMAND_LIMIT = 2000;
31
- const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
32
- const label = (value: unknown, max: number) => typeof value === "string" && value.trim().length > 0 && value.length <= max;
33
-
34
- export function parseAcceptanceContract(value: unknown): AcceptanceContract {
35
- if (!record(value) || value["version"] !== 1 || !label(value["id"], 80) ||
36
- (value["scope"] !== undefined && value["scope"] !== "request" && value["scope"] !== "changes") ||
37
- !Array.isArray(value["criteria"]) || value["criteria"].length < 1 || value["criteria"].length > 20) throw new Error("invalid acceptance contract v1");
38
- if (Object.keys(value).some(key => !["version", "id", "scope", "criteria", "source", "targets"].includes(key))) throw new Error("unsupported acceptance contract field");
39
- const source = value["source"];
40
- if (source !== undefined && (!record(source) || !label(source["path"], 4096) || typeof source["sha256"] !== "string" || !/^[a-f0-9]{64}$/.test(source["sha256"]))) throw new Error("invalid acceptance source");
41
- const criteria: AcceptanceCriterion[] = value["criteria"].map((item: unknown) => {
42
- if (!record(item) || !label(item["id"], 80) || !label(item["description"], 300)) throw new Error("invalid acceptance criterion");
43
- if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(String(item["id"])) || Object.keys(item).some(key => !["id", "description", "kind", "command", "target", "guard"].includes(key))) throw new Error("unsupported acceptance criterion field or ID");
44
- const fields = { id: String(item["id"]), description: String(item["description"]) };
45
- if (item["kind"] === "human" && item["command"] === undefined && item["guard"] === undefined && (item["target"] === undefined || label(item["target"], 80))) return { ...fields, kind: "human", ...(item["target"] ? { target: String(item["target"]) } : {}) };
46
- if (item["kind"] === "command" && item["target"] === undefined && label(item["command"], ACCEPTANCE_COMMAND_LIMIT) && (item["guard"] === undefined || item["guard"] === true)) {
47
- return { ...fields, kind: "command", command: String(item["command"]), ...(item["guard"] === true ? { guard: true as const } : {}) };
48
- }
49
- throw new Error("acceptance criterion must declare a command or human decision");
50
- });
51
- if (new Set(criteria.map(item => item.id)).size !== criteria.length) throw new Error("duplicate acceptance criterion IDs");
52
- const declarations = value["targets"];
53
- if (declarations !== undefined && (!Array.isArray(declarations) || declarations.length < 1 || declarations.length > 8)) throw Error("targets requires 1..8 declared runtimes");
54
- const targets = Array.isArray(declarations) ? declarations.map(parseRuntimeTarget) : undefined;
55
- if (targets && (new Set(targets.map(target => target.id)).size !== targets.length || targets.some(target =>
56
- !criteria.some(item => item.id === target.build && item.kind === "command") || !criteria.some(item => item.kind === "human" && item.target === target.id)))) throw Error("targets require a build condition and a bound human condition");
57
- if (criteria.some(item => item.kind === "human" && item.target && !targets?.some(target => target.id === item.target))) throw Error("unknown human acceptance target");
58
- return { version: 1, id: String(value["id"]), criteria,
59
- ...(targets ? { targets } : {}),
60
- ...(record(source) ? { source: { path: String(source["path"]), sha256: String(source["sha256"]) } } : {}),
61
- ...(value["scope"] ? { scope: value["scope"] as "request" | "changes" } : {}) };
62
- }
63
-
64
- /**
65
- * What one counter-check observation means (S.1, S.7): the check run once more
66
- * in a clone with the request's changes undone. A check that tests the change
67
- * must fail there; a guard must still pass there. The log records only the
68
- * exit code and the role it ran under, so every view derives the verdict here
69
- * and none can disagree with another.
70
- *
71
- * tests the change fails there: discriminating passes there: vacuous
72
- * guard passes there: held fails there: not_held
73
- *
74
- * A guard that did not hold is not a defect in the evidence: the check does
75
- * test the change, only its declaration was wrong.
76
- */
77
- export type CounterVerdict = "discriminating" | "vacuous" | "held" | "not_held" | "unavailable";
78
- export function counterVerdict(observation: { guard?: true } & ({ exit: number } | { reason: string })): CounterVerdict {
79
- if (!("exit" in observation)) return "unavailable";
80
- if (observation.guard) return observation.exit === 0 ? "held" : "not_held";
81
- return observation.exit === 0 ? "vacuous" : "discriminating";
82
- }
83
-
84
- export function deriveAcceptance(previous: AcceptanceState | undefined, events: readonly LogEvent[]): AcceptanceState | undefined {
85
- let state = previous ? { ...previous, criteria: previous.criteria.map(item => ({ ...item })), ...(previous.targets ? { targets: previous.targets.map(target => ({ ...target })) } : {}) } : undefined;
86
- for (const event of events) {
87
- if (event.t === "acceptance_contract") {
88
- state = { contract_id: event.contract.id, contract_event: event.id, request: event.request, scope: event.contract.scope ?? "request", changed: false,
89
- criteria: event.contract.criteria.map(item => ({ ...item, status: item.kind === "human" ? "awaiting_human" : "not_run" })), status: "needs_work" };
90
- if (event.contract.targets) state.targets = event.contract.targets.map(target => ({ id: target.id, build: target.build, status: "not_run" }));
91
- continue;
92
- }
93
- if (!state) continue;
94
- const mutation = workspaceChanged(event);
95
- if (mutation) { state.changed = true; for (const target of state.targets ?? []) target.status = "stale"; }
96
- if (event.t === "target_observation" && event.observation.request === state.request && event.observation.contract_event === state.contract_event) {
97
- const target = state.targets?.find(item => item.id === event.observation.target), receipt = event.observation;
98
- if (target) {
99
- const build = state.criteria.find(item => item.id === target.build);
100
- target.status = receipt.state === "ready" && (build?.status !== "pass" || build.evidence?.event_id !== receipt.build?.event) ? "stale" : receipt.state;
101
- target.observation = event.id; target.reason = receipt.reason;
102
- if (receipt.build) target.build_event = receipt.build.event;
103
- if (receipt.instance) { target.binding = receipt.instance.id; target.session = receipt.instance.session; target.pid = receipt.instance.pid; target.entry = receipt.instance.entry.path; }
104
- else { delete target.binding; delete target.session; delete target.pid; delete target.entry; }
105
- if (receipt.port !== undefined) target.port = receipt.port;
106
- }
107
- }
108
- for (const target of state.targets ?? []) if (event.t === "process_exit" && event.session === target.session ||
109
- event.t === "tool_result" && record(event.data) && event.data["session_id"] === target.session && event.data["running"] === false) target.status = "stale";
110
- for (const item of state.criteria) {
111
- if (item.evidence && evidenceStale([event], item.evidence, item.status === "stale")) item.status = "stale";
112
- if (mutation && item.kind === "human" && item.status === "accepted") { item.status = "awaiting_human"; delete item.accepted_event; delete item.accepted_binding; }
113
- if (item.kind === "command" && event.t === "tool_result" && event.effect?.kind === "command" && event.effect.cmd === item.command) {
114
- item.status = event.effect.exit === 0 && event.ok ? event.effect.workspace_observed === false ? "stale" : "pass" : "fail";
115
- item.evidence = { command: event.effect.cmd, exit: event.effect.exit, event_id: event.id, at: event.at,
116
- ...(event.effect.workspace_scope ? { workspace_scope: event.effect.workspace_scope } : {}),
117
- ...(event.effect.workspace_sha256 ? { workspace_sha256: event.effect.workspace_sha256 } : {}) };
118
- for (const target of state.targets ?? []) if (target.build === item.id && target.build_event !== event.id) target.status = "stale";
119
- }
120
- if (item.kind === "human" && event.t === "acceptance_decision" && event.request === state.request &&
121
- event.contract_id === state.contract_id && event.criterion === item.id && event.actor === "user") {
122
- const target = item.target ? state.targets?.find(target => target.id === item.target) : undefined;
123
- if (!item.target || target?.status === "ready" && target.binding === event.target_binding && target.observation === event.target_observation) {
124
- item.status = "accepted"; item.accepted_event = event.id;
125
- if (event.target_binding) item.accepted_binding = event.target_binding;
126
- }
127
- }
128
- }
129
- for (const target of state.targets ?? []) if (target.status === "ready" && !state.criteria.some(item => item.id === target.build && item.status === "pass" && item.evidence?.event_id === target.build_event)) target.status = "stale";
130
- for (const item of state.criteria) if (item.kind === "human" && item.target && item.status === "accepted") {
131
- const target = state.targets?.find(target => target.id === item.target);
132
- if (target?.status !== "ready" || target.binding !== item.accepted_binding) { item.status = "awaiting_human"; delete item.accepted_event; delete item.accepted_binding; }
133
- }
134
- }
135
- if (state) state.status = state.scope === "changes" && !state.changed ? "not_applicable" :
136
- state.criteria.some(item => item.kind === "command" && item.status !== "pass") || state.targets?.some(target => target.status !== "ready") ? "needs_work" :
137
- state.criteria.some(item => item.kind === "human" && item.status !== "accepted") ? "awaiting_human" : "verified";
138
- return state;
139
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * xeno's key bindings (D20). One flat namespace:
3
- * app.* global, handled before the focused component
4
- * editor.* the composer (submit / newline / history / completion)
5
- * list.* every list dialog (palette, model, login, resume, tree, …)
6
- * Shortcuts that only exist inside one dialog are shown in that dialog's
7
- * hint row instead of being configurable globals; the palette (Ctrl+P)
8
- * reaches every command, so rarely used actions need no chord at all.
9
- */
10
- export const ACTION_DEFAULTS = {
11
- "app.palette": ["ctrl+p"],
12
- "app.model": ["ctrl+l"],
13
- "app.cycleModel": ["alt+m"],
14
- "app.cycleModelReverse": ["alt+shift+m"],
15
- "app.cycleThinking": ["shift+tab"],
16
- "app.toggleTools": ["ctrl+o"],
17
- "app.toggleThinking": ["ctrl+t"],
18
- "app.toggleSidebar": ["ctrl+b"],
19
- "app.interrupt": ["escape"],
20
- "app.clear": ["ctrl+c"],
21
- "app.exit": ["ctrl+d"],
22
- "app.suspend": ["ctrl+z"],
23
- "app.externalEditor": ["ctrl+g"],
24
- "app.message.copy": ["ctrl+x"],
25
- "app.message.followUp": ["alt+enter"],
26
- "app.message.dequeue": ["alt+up"],
27
- "app.clipboard.pasteImage": ["ctrl+v"],
28
- "app.scroll.pageUp": ["pageup"],
29
- "app.scroll.pageDown": ["pagedown"],
30
- "app.scroll.top": ["ctrl+home"],
31
- "app.scroll.bottom": ["ctrl+end"],
32
- "app.session.new": [],
33
- "app.session.tree": [],
34
- "app.session.fork": [],
35
- "app.session.resume": [],
36
- "app.saveDefault": ["ctrl+s"],
37
- "editor.submit": ["enter"],
38
- "editor.newLine": ["shift+enter", "ctrl+j"],
39
- "editor.complete": ["tab"],
40
- "editor.historyPrevious": ["up"],
41
- "editor.historyNext": ["down"],
42
- "list.up": ["up", "ctrl+k"],
43
- "list.down": ["down", "ctrl+j"],
44
- "list.pageUp": ["pageup"],
45
- "list.pageDown": ["pagedown"],
46
- "list.confirm": ["enter"],
47
- "list.cancel": ["escape"],
48
- "list.toggle": ["space"],
49
- } as const satisfies Record<string, readonly string[]>;
50
-
51
- /** Renamed ids still read from a user's keybindings file. */
52
- export const ACTION_ALIASES: Readonly<Record<string, string>> = {
53
- "app.model.select": "app.model",
54
- "app.thinking.cycle": "app.cycleThinking",
55
- "app.thinking.toggle": "app.toggleThinking",
56
- "app.tools.expand": "app.toggleTools",
57
- "app.editor.external": "app.externalEditor",
58
- };
@@ -1,52 +0,0 @@
1
- /** A finer view of OSC status, derived from the same runtime facts (D11). */
2
- import { programStatus, type ProgramStatus } from "./program-status.ts";
3
- import type { ExecutionOutcome } from "./outcome.ts";
4
-
5
- export interface ActivityFacts {
6
- readonly running: boolean;
7
- readonly waiting?: "permission" | "question" | "auth" | null;
8
- readonly outcome?: ExecutionOutcome;
9
- readonly goal?: string;
10
- readonly phase?: "connecting" | "thinking" | "streaming" | "tool";
11
- readonly tool?: string;
12
- readonly retry?: RetryFacts;
13
- readonly queued: number;
14
- }
15
- /** One pending retry: which attempt, out of how many, how long until it, and why. */
16
- export interface RetryFacts { readonly attempt: number; readonly max?: number; readonly seconds: number; readonly reason?: string }
17
-
18
- const attemptText = (retry: RetryFacts): string => retry.max === undefined ? `${retry.attempt}` : `${retry.attempt}/${retry.max}`;
19
-
20
- /**
21
- * The stream-area retry line. The cause appears from the third attempt (or
22
- * the last, for a smaller budget), as in Claude Code: one or two retries are
23
- * noise, and naming them would send the user to check a network that is fine.
24
- */
25
- export function retryLine(retry: RetryFacts): string {
26
- const named = retry.reason !== undefined && retry.attempt >= Math.min(3, retry.max ?? 3);
27
- return `retry ${attemptText(retry)} in ${Math.max(0, retry.seconds).toFixed(1)}s · ${named ? `${retry.reason} · ` : ""}esc to cancel`;
28
- }
29
-
30
- export interface ActivityStatus { readonly state: ProgramStatus["state"]; readonly label: string; readonly detail: string; readonly queued: number }
31
-
32
- const STOP_CAUSE: Partial<Record<ExecutionOutcome["reason"], string>> = {
33
- stalled: "no progress", no_progress: "no progress", max_turns: "request limit", context_budget: "context budget",
34
- output_limit: "output limit", timeout: "time limit",
35
- };
36
-
37
- export function activityStatus(facts: ActivityFacts): ActivityStatus {
38
- const status = programStatus(facts), queued = Math.max(0, Math.floor(facts.queued));
39
- const view = (label: string, detail = status.msg ?? ""): ActivityStatus => ({ state: status.state, label, detail, queued });
40
- // A harness stop is not a question: saying "Awaiting input" left the user
41
- // guessing what to answer (fix-finished-turn-status). The OSC state stays blocked.
42
- if (!facts.running && !facts.waiting && facts.outcome?.status === "stopped") return view("Stopped", STOP_CAUSE[facts.outcome.reason] ?? facts.outcome.reason.replaceAll("_", " "));
43
- if (status.state === "blocked") return view(status.kind === "auth" ? "Sign in required" : status.kind === "permission" ? "Awaiting approval"
44
- : !facts.waiting && facts.goal === "awaiting_human" ? "Awaiting review" : "Awaiting input");
45
- if (status.state === "error") return view("Failed");
46
- if (!facts.running) return view(status.state === "done" ? "Turn finished" : facts.outcome?.status === "cancelled" ? "Cancelled" : "Ready");
47
- if (facts.retry) return view("Retrying", `attempt ${attemptText(facts.retry)} · ${Math.max(0, Math.ceil(facts.retry.seconds))}s`);
48
- if (facts.phase === "tool") return view("Running tool", facts.tool ?? "Tool execution");
49
- if (facts.phase === "thinking") return view("Thinking");
50
- if (facts.phase === "streaming") return view("Responding");
51
- return view("Connecting", "Waiting for the first response");
52
- }
@@ -1,130 +0,0 @@
1
- /** R13 / D13: explicit role layers share tool, model, workflow and budget ownership. */
2
- import { isWorkflowMode, type WorkflowMode } from "./workflow-mode.ts";
3
- import { isToolProfile, parsePermissionRule, type PermissionRule, type ToolSelection } from "./permissions.ts";
4
- import { asThinkingLevel, type ThinkingLevel } from "./thinking.ts";
5
-
6
- export interface AgentProfile {
7
- readonly name: string;
8
- readonly description?: string;
9
- readonly mode?: WorkflowMode;
10
- readonly instructions?: string;
11
- readonly model?: { readonly provider: string; readonly id: string; readonly thinking?: ThinkingLevel };
12
- readonly tools?: ToolSelection;
13
- readonly permissions?: readonly PermissionRule[];
14
- /** maxTurns is a hard cap; timeBudgetMs retains the existing advisory semantics. */
15
- readonly budget?: { readonly maxTurns?: number; readonly timeBudgetMs?: number };
16
- }
17
- export type AgentProfileField = Exclude<keyof AgentProfile, "name">;
18
- export interface AgentProfileSource { readonly kind: "builtin" | "user" | "project" | "cli"; readonly path?: string }
19
- export interface AgentProfileLayer { readonly source: AgentProfileSource; readonly profiles: readonly AgentProfile[] }
20
- export interface ResolvedAgentProfile {
21
- readonly profile: AgentProfile;
22
- readonly sources: readonly AgentProfileSource[];
23
- readonly provenance: Readonly<Partial<Record<AgentProfileField, AgentProfileSource>>>;
24
- }
25
- export interface AgentProfileParse { readonly profiles: readonly AgentProfile[]; readonly diagnostics: readonly string[] }
26
-
27
- /** Named builtins are explicit selectable presets; they never fill missing fields in another role. */
28
- export function builtinAgentProfileLayer(): AgentProfileLayer {
29
- return { source: { kind: "builtin" }, profiles: [{ name: "build", mode: "build", tools: { profile: "foundation" } }, { name: "plan", mode: "plan", tools: { profile: "foundation" } },
30
- { name: "reviewer", mode: "plan", tools: { profile: "readonly" },
31
- instructions: "Review current code for actionable defects. Cite concrete evidence and paths; do not edit the workspace." }] };
32
- }
33
-
34
- const FIELDS: readonly AgentProfileField[] = ["description", "mode", "instructions", "model", "tools", "permissions", "budget"];
35
- export function isAgentProfileName(value: string): boolean { return /^[a-z][a-z0-9_-]{0,63}$/.test(value); }
36
- function record(value: unknown, label: string, allowed: readonly string[]): Record<string, unknown> {
37
- if (!value || typeof value !== "object" || Array.isArray(value)) throw Error(`${label} must be an object`);
38
- const obj = value as Record<string, unknown>;
39
- const unknown = Object.keys(obj).find(key => !allowed.includes(key));
40
- if (unknown) throw Error(`${label} has unknown field ${unknown}`);
41
- return obj;
42
- }
43
- function text(value: unknown, label: string): string {
44
- if (typeof value !== "string" || !value.trim() || value.includes("\0")) throw Error(`${label} must be a nonempty string`);
45
- return value;
46
- }
47
- function strings(value: unknown, label: string): string[] {
48
- if (!Array.isArray(value) || value.some(item => typeof item !== "string" || !item.trim()) || new Set(value).size !== value.length) throw Error(`${label} must contain unique nonempty names`);
49
- return [...value] as string[];
50
- }
51
-
52
- export function parseAgentProfile(value: unknown): AgentProfile {
53
- const obj = record(value, "agent profile", ["name", ...FIELDS]);
54
- const name = text(obj["name"], "agent name");
55
- if (!isAgentProfileName(name)) throw Error("agent name must begin with a lowercase letter and contain only letters, digits, _ or - (max 64)");
56
- const profile: { name: string; description?: string; mode?: WorkflowMode; instructions?: string;
57
- model?: NonNullable<AgentProfile["model"]>; tools?: ToolSelection; permissions?: PermissionRule[]; budget?: NonNullable<AgentProfile["budget"]> } = { name };
58
- if (obj["description"] !== undefined) profile.description = text(obj["description"], "agent description");
59
- if (obj["instructions"] !== undefined) profile.instructions = text(obj["instructions"], "agent instructions");
60
- if (obj["mode"] !== undefined) {
61
- if (typeof obj["mode"] !== "string" || !isWorkflowMode(obj["mode"])) throw Error("agent mode must be build or plan");
62
- profile.mode = obj["mode"];
63
- }
64
- if (obj["model"] !== undefined) {
65
- const model = record(obj["model"], "agent model", ["provider", "id", "thinking"]);
66
- const provider = text(model["provider"], "agent model provider"), id = text(model["id"], "agent model id");
67
- const thinking = asThinkingLevel(model["thinking"]);
68
- if (model["thinking"] !== undefined && thinking === undefined) throw Error("agent model thinking is invalid");
69
- profile.model = { provider, id, ...(thinking !== undefined ? { thinking } : {}) };
70
- }
71
- if (obj["tools"] !== undefined) {
72
- const tools = record(obj["tools"], "agent tools", ["profile", "only", "exclude", "none", "noBuiltin"]);
73
- const selected: { profile?: NonNullable<ToolSelection["profile"]>; only?: string[]; exclude?: string[]; none?: boolean; noBuiltin?: boolean } = {};
74
- if (tools["profile"] !== undefined) {
75
- if (typeof tools["profile"] !== "string" || !isToolProfile(tools["profile"])) throw Error("agent tool profile is invalid");
76
- selected.profile = tools["profile"];
77
- }
78
- if (tools["only"] !== undefined) selected.only = strings(tools["only"], "agent tools.only");
79
- if (tools["exclude"] !== undefined) selected.exclude = strings(tools["exclude"], "agent tools.exclude");
80
- for (const key of ["none", "noBuiltin"] as const) {
81
- if (tools[key] !== undefined) { if (typeof tools[key] !== "boolean") throw Error(`agent tools.${key} must be boolean`); selected[key] = tools[key]; }
82
- }
83
- profile.tools = selected;
84
- }
85
- if (obj["permissions"] !== undefined) {
86
- if (!Array.isArray(obj["permissions"])) throw Error("agent permissions must be an array");
87
- profile.permissions = obj["permissions"].map(parsePermissionRule);
88
- }
89
- if (obj["budget"] !== undefined) {
90
- const budget = record(obj["budget"], "agent budget", ["maxTurns", "timeBudgetMs"]);
91
- const selected: { maxTurns?: number; timeBudgetMs?: number } = {};
92
- for (const key of ["maxTurns", "timeBudgetMs"] as const) {
93
- if (budget[key] !== undefined) {
94
- if (!Number.isSafeInteger(budget[key]) || Number(budget[key]) < 1) throw Error(`agent budget.${key} must be a positive safe integer`);
95
- selected[key] = Number(budget[key]);
96
- }
97
- }
98
- profile.budget = selected;
99
- }
100
- return profile;
101
- }
102
-
103
- /** A malformed document contributes no partial authority. */
104
- export function parseAgentProfiles(value: unknown): AgentProfileParse {
105
- try {
106
- const obj = record(value, "agents document", ["profiles"]);
107
- if (!Array.isArray(obj["profiles"])) throw Error("agents document profiles must be an array");
108
- const profiles = obj["profiles"].map(parseAgentProfile);
109
- if (new Set(profiles.map(profile => profile.name)).size !== profiles.length) throw Error("agents document contains duplicate profile names");
110
- return { profiles, diagnostics: [] };
111
- } catch (error) { return { profiles: [], diagnostics: [error instanceof Error ? error.message : "Invalid agents document"] }; }
112
- }
113
-
114
- /** Caller supplies low-to-high layers; whole-field replacement stays explainable. */
115
- export function resolveAgentProfile(name: string, layers: readonly AgentProfileLayer[]): ResolvedAgentProfile | null {
116
- let profile: AgentProfile | undefined;
117
- const sources: AgentProfileSource[] = [], provenance: Partial<Record<AgentProfileField, AgentProfileSource>> = {};
118
- for (const layer of layers) {
119
- const matching = layer.profiles.find(item => item.name === name);
120
- if (!matching) continue;
121
- const source = structuredClone(layer.source);
122
- sources.push(source); profile = { ...profile, ...structuredClone(matching) };
123
- for (const field of FIELDS) if (matching[field] !== undefined) provenance[field] = structuredClone(source);
124
- }
125
- return profile ? { profile, sources, provenance } : null;
126
- }
127
-
128
- export function agentProfileNames(layers: readonly AgentProfileLayer[]): string[] {
129
- return [...new Set(layers.flatMap(layer => layer.profiles.map(profile => profile.name)))].sort();
130
- }
@@ -1,17 +0,0 @@
1
- /** Only an entire list of image paths is a drop; ordinary prose and shell syntax remain literal. */
2
- export function droppedImagePaths(text: string): string[] | null {
3
- const paths: string[] = [];
4
- let token = "", quote = "", escaped = false;
5
- for (const char of text.trim()) {
6
- if (escaped) { token += char; escaped = false; }
7
- else if (char === "\\" && quote !== "'") escaped = true;
8
- else if (quote) { if (char === quote) quote = ""; else token += char; }
9
- else if (char === "'" || char === '"') quote = char;
10
- else if (/\s/.test(char)) { if (token) { paths.push(token); token = ""; } }
11
- else token += char;
12
- }
13
- if (escaped || quote) return null;
14
- if (token) paths.push(token);
15
- return paths.length > 0 && paths.every(path => !/[\x00-\x1f]/.test(path) && !path.startsWith("@") &&
16
- /\.(png|jpe?g|gif|webp|bmp)$/i.test(path) && !/[;$|<>`]/.test(path)) ? paths : null;
17
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Blind checks: the independent rung of the evidence ladder (S.1).
3
- *
4
- * A check the model can see is a check the model can work toward. Blind
5
- * checks live outside the workspace, never enter a projection, and run only
6
- * after the goal ends, against a clone of the final result. Because the log
7
- * is complete, whether the model ever touched them is a mechanical question
8
- * over its own tool inputs and read receipts -- not a promise.
9
- */
10
- import type { LogEvent } from "./events.ts";
11
-
12
- /**
13
- * Assumed, not measured: a hidden test suite of a handful of cases plus their
14
- * fixtures. Bounded so freezing a blind directory into the session context
15
- * cannot grow without limit.
16
- */
17
- export const BLIND_CHECK_LIMIT = 8;
18
- export const BLIND_FILE_LIMIT = 64;
19
- export const BLIND_BYTES_LIMIT = 8 * 1024 * 1024;
20
-
21
- export type BlindCheck = { id: string; description: string; command: string };
22
- export type BlindManifest = { version: 1; checks: BlindCheck[] };
23
- /**
24
- * What a session freezes: where the checks live (the real path, plus every
25
- * spelling of it the user gave, since a symlinked temp directory is a
26
- * different string), the manifest, and every file's digest.
27
- */
28
- export type BlindChecks = { dir: string; aliases: readonly string[]; manifest: BlindManifest; files: Record<string, string> };
29
-
30
- const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
31
- const label = (value: unknown, max: number): value is string => typeof value === "string" && value.trim().length > 0 && value.length <= max;
32
-
33
- export function parseBlindManifest(value: unknown): BlindManifest {
34
- if (!record(value) || value["version"] !== 1 || Object.keys(value).some(key => key !== "version" && key !== "checks")) throw Error("blind.json must be {version: 1, checks: [...]}");
35
- const checks = value["checks"];
36
- if (!Array.isArray(checks) || checks.length < 1 || checks.length > BLIND_CHECK_LIMIT) throw Error(`blind.json needs 1..${BLIND_CHECK_LIMIT} checks`);
37
- const parsed = checks.map((check: unknown, index): BlindCheck => {
38
- if (!record(check) || Object.keys(check).some(key => !["id", "description", "command"].includes(key))) throw Error(`blind check ${index + 1}: only id, description and command are allowed`);
39
- const { id, description, command } = check;
40
- if (!label(id, 80) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(id)) throw Error(`blind check ${index + 1}: invalid id`);
41
- if (!label(description, 300) || !label(command, 500)) throw Error(`blind check ${index + 1}: description and command are required`);
42
- return { id, description, command };
43
- });
44
- if (new Set(parsed.map(check => check.id)).size !== parsed.length) throw Error("blind.json has duplicate check ids");
45
- return { version: 1, checks: parsed };
46
- }
47
-
48
- export function isBlindChecks(value: unknown): value is BlindChecks {
49
- if (!record(value) || typeof value["dir"] !== "string" || !record(value["files"]) ||
50
- !Array.isArray(value["aliases"]) || !value["aliases"].every(alias => typeof alias === "string" && alias.length > 0)) return false;
51
- try { parseBlindManifest(value["manifest"]); } catch { return false; }
52
- return Object.values(value["files"]).every(sha => typeof sha === "string" && /^[a-f0-9]{64}$/.test(sha));
53
- }
54
-
55
- /**
56
- * Whether the model could have seen the blind checks: a tool input naming the
57
- * blind directory, or a read whose content digest matches a blind file. This
58
- * covers what the harness records for every tool; output a command produced
59
- * without naming the path (a recursive search, say) is not matched, and the
60
- * report says only what was found.
61
- */
62
- export function blindExposure(events: readonly LogEvent[], blind: BlindChecks): string | null {
63
- const digests = new Set(Object.values(blind.files)), paths = [blind.dir, ...blind.aliases];
64
- for (const event of events) {
65
- if (event.t === "assistant") for (const block of event.blocks) {
66
- const input = block.type === "tool_use" ? JSON.stringify(block.input) : "";
67
- if (block.type === "tool_use" && paths.some(path => input.includes(path))) return `tool call ${block.call_id} names the blind check directory`;
68
- }
69
- if (event.t === "tool_result" && event.effect?.kind === "read" && digests.has(event.effect.sha256)) return `tool result ${event.id} read a blind check file`;
70
- }
71
- return null;
72
- }
@@ -1,174 +0,0 @@
1
- /**
2
- * Explicit fixed-segment policy (decisions D1, D4, D5; new default in N.8).
3
- *
4
- * Pure. Given a token count and where the loop currently stands, decide whether
5
- * to close the segment.
6
- *
7
- * Three rules, each with a reason:
8
- *
9
- * ONLY AT A TURN BOUNDARY (D5). A cut between a tool_use and its tool_result
10
- * would produce an unpairable request. The gap between trigger and hard limit is
11
- * the headroom that makes waiting for the boundary safe.
12
- *
13
- * THE MODEL MAY ADVANCE A CUT BUT NEVER DEFER ONE (D4). A checkpoint request at
14
- * a clean semantic boundary is usually better than an arbitrary threshold, so it
15
- * is honoured -- but the hard limit is checked first and cannot be overridden.
16
- *
17
- * ABSOLUTE TOKENS, NOT A PERCENTAGE OF THE WINDOW. Cost and quality both argue
18
- * for tens of thousands of tokens, and neither argument scales with whatever
19
- * window the model happens to advertise.
20
- *
21
- * On the defaults below: the optimum from the cost model is L - H = 5*sqrt(H*d),
22
- * where H is handoff size and d is per-turn growth. H was originally *assumed* to
23
- * be ~10k, which put the optimum near 37k and the thresholds at 60k/80k.
24
- * The original single-segment measurement (prev=null) yielded 366 to 3201
25
- * tokens, 1.6-7.6% of L. It did not measure accumulation across boundaries.
26
- * D20 adds bounded carried state and continuous-history measurements; full
27
- * derivation state still grows with distinct facts. The old local cost optimum
28
- * is a workload diagnostic, not proof of a globally optimal threshold.
29
- *
30
- * The defaults are 30k/40k rather than 15k/25k because the cost model prices only
31
- * bytes. Two things it does not price argue for longer segments: the re-read a new
32
- * segment usually needs before it can act (the digest check on file_anchors), and
33
- * the fact that too-short segments break stateful work -- the documented failure
34
- * mode of fresh-context loops is that extended debugging loses context that was
35
- * load-bearing. 30k/40k is within 15% of the cost optimum for every re-read cost
36
- * from 0 to 24k tokens per boundary, which 60k/80k is not.
37
- */
38
-
39
- import type { CutReason } from "./events.ts";
40
-
41
- export interface BoundaryConfig {
42
- /** Start looking for a turn boundary at or above this count. */
43
- triggerTokens: number;
44
- /** Cut at the next turn boundary regardless. Must exceed triggerTokens. */
45
- hardTokens: number;
46
- /**
47
- * How much of the context a node must have produced itself before cutting is
48
- * worth it.
49
- *
50
- * Every node starts with tokens it did not create: the frozen prefix plus,
51
- * after the first cut, the handoff. Without this floor, a trigger set below
52
- * that inherited weight makes the first turn of every node cut -- a real run
53
- * produced 36 nodes for a five-file task, spending its budget on handoffs.
54
- *
55
- * The value follows the crossover: a handoff only pays for itself once the
56
- * node's own history exceeds it. Measured handoffs ran 366-3201 tokens, so
57
- * the default sits above the top of that range.
58
- *
59
- * This is also the floor for a model-requested checkpoint. A separate knob
60
- * for that case would be the same idea written twice: a checkpoint on a node
61
- * that has produced nothing costs a handoff write and discards the node's
62
- * cache for no gain, which is exactly what this floor exists to prevent.
63
- */
64
- minNodeWorkTokens: number;
65
- }
66
-
67
- export const DEFAULT_BOUNDARY: BoundaryConfig = {
68
- triggerTokens: 30_000,
69
- hardTokens: 40_000,
70
- minNodeWorkTokens: 5_000,
71
- };
72
-
73
- export interface BoundaryInput {
74
- tokens: number;
75
- /**
76
- * Tokens the node did not produce: the frozen prefix, plus the rendered
77
- * handoff for any node after the first. Cutting cannot reduce these, so they
78
- * do not count toward the node having earned a boundary.
79
- */
80
- inheritedTokens?: number;
81
- /** True only between a completed assistant/tool exchange and the next request. */
82
- atTurnBoundary: boolean;
83
- /** Set when the model called the checkpoint tool during this turn. */
84
- checkpointRequested?: boolean;
85
- config?: Partial<BoundaryConfig>;
86
- }
87
-
88
- export type BoundaryDecision =
89
- | { cut: true; reason: CutReason }
90
- | {
91
- cut: false;
92
- reason:
93
- | "below_trigger"
94
- | "awaiting_turn_boundary"
95
- | "below_min_node_work"
96
- /**
97
- * The inherited floor alone is at or past the hard limit, so a cut
98
- * would hand the next node a context it cannot work in either. The
99
- * thresholds need widening or the frozen prefix shrinking; cutting
100
- * would only thrash.
101
- */
102
- | "inherited_floor_exceeds_limit";
103
- /**
104
- * True when the count is already past the hard limit and a cut is not yet
105
- * possible -- i.e. a single turn consumed more than the headroom. The caller
106
- * should surface this: the thresholds need retuning, or one tool returned an
107
- * output large enough to need its own handling.
108
- */
109
- overrun: boolean;
110
- };
111
-
112
- export function shouldCut(input: BoundaryInput): BoundaryDecision {
113
- const cfg = { ...DEFAULT_BOUNDARY, ...input.config };
114
- if (cfg.hardTokens <= cfg.triggerTokens) {
115
- throw new RangeError(
116
- `boundary: hardTokens (${cfg.hardTokens}) must exceed triggerTokens (${cfg.triggerTokens}); ` +
117
- "the difference is the headroom needed to reach a turn boundary",
118
- );
119
- }
120
- // The mirror of thrashing, and just as silent: a work floor at or above the
121
- // trigger means the trigger can never be acted on, so the session grows
122
- // without bound and nothing says why.
123
- if (cfg.minNodeWorkTokens >= cfg.triggerTokens) {
124
- throw new RangeError(
125
- `boundary: minNodeWorkTokens (${cfg.minNodeWorkTokens}) must be below triggerTokens ` +
126
- `(${cfg.triggerTokens}); otherwise no node can ever reach a boundary`,
127
- );
128
- }
129
-
130
- const { tokens, atTurnBoundary, checkpointRequested = false, inheritedTokens = 0 } = input;
131
- const ownWork = Math.max(0, tokens - inheritedTokens);
132
-
133
- if (!atTurnBoundary) {
134
- return {
135
- cut: false,
136
- reason: "awaiting_turn_boundary",
137
- overrun: tokens >= cfg.hardTokens,
138
- };
139
- }
140
-
141
- // Is there any reason to consider a boundary at all?
142
- const wouldCut =
143
- tokens >= cfg.hardTokens || checkpointRequested || tokens >= cfg.triggerTokens;
144
- if (!wouldCut) return { cut: false, reason: "below_trigger", overrun: false };
145
-
146
- // There is -- but a node that has produced almost nothing cannot be helped
147
- // by one: the next node inherits the same floor and lands in the same place.
148
- if (ownWork < cfg.minNodeWorkTokens) {
149
- if (inheritedTokens >= cfg.hardTokens) {
150
- return { cut: false, reason: "inherited_floor_exceeds_limit", overrun: true };
151
- }
152
- return { cut: false, reason: "below_min_node_work", overrun: tokens >= cfg.hardTokens };
153
- }
154
-
155
- // Worth cutting. The hard limit is checked first so the model cannot defer
156
- // it by asking for a checkpoint.
157
- if (tokens >= cfg.hardTokens) return { cut: true, reason: "hard_limit" };
158
- if (checkpointRequested) return { cut: true, reason: "model_checkpoint" };
159
- return { cut: true, reason: "trigger" };
160
- }
161
-
162
- /**
163
- * Stop-and-ask rule (D9 / M.4). New read observations or fresh successful checks
164
- * reset the counter. Exact repetitions, annotations alone and gate regressions
165
- * do not. This detects inactivity; it cannot judge relevance to the user's goal.
166
- */
167
- export const DEFAULT_MAX_SEGMENTS_WITHOUT_PROGRESS = 3;
168
-
169
- export function shouldStopAndAsk(
170
- segmentsWithoutProgress: number,
171
- max = DEFAULT_MAX_SEGMENTS_WITHOUT_PROGRESS,
172
- ): boolean {
173
- return segmentsWithoutProgress >= max;
174
- }