@visual-z/xeno 0.8.3 → 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 (348) hide show
  1. package/CHANGELOG.md +24 -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 -2066
  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 -1713
  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 -3001
  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/canvas.ts +0 -96
  312. package/src/tui/ui/colors.ts +0 -70
  313. package/src/tui/ui/composer.ts +0 -200
  314. package/src/tui/ui/control-panel.ts +0 -25
  315. package/src/tui/ui/control-sidebar.ts +0 -49
  316. package/src/tui/ui/dialog.ts +0 -398
  317. package/src/tui/ui/draft.ts +0 -23
  318. package/src/tui/ui/duration.ts +0 -16
  319. package/src/tui/ui/edge.ts +0 -186
  320. package/src/tui/ui/image-size.ts +0 -44
  321. package/src/tui/ui/ink.ts +0 -210
  322. package/src/tui/ui/items.ts +0 -42
  323. package/src/tui/ui/keys.ts +0 -40
  324. package/src/tui/ui/layout.ts +0 -74
  325. package/src/tui/ui/list-model.ts +0 -86
  326. package/src/tui/ui/list-view.ts +0 -172
  327. package/src/tui/ui/mascot.ts +0 -67
  328. package/src/tui/ui/messages.ts +0 -518
  329. package/src/tui/ui/motion.ts +0 -28
  330. package/src/tui/ui/paths.ts +0 -10
  331. package/src/tui/ui/pictures.ts +0 -108
  332. package/src/tui/ui/screen.ts +0 -548
  333. package/src/tui/ui/status.ts +0 -403
  334. package/src/tui/ui/styled.ts +0 -85
  335. package/src/tui/ui/tool-card.ts +0 -150
  336. package/src/tui/width.ts +0 -567
  337. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  338. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  339. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  340. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  341. package/src/vendor/pi-rich/html.ts +0 -51
  342. package/src/vendor/pi-rich/image-convert.ts +0 -49
  343. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  344. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  345. package/src/vendor/pi-rich/image-resize.ts +0 -123
  346. package/src/vendor/pi-rich/mime.ts +0 -116
  347. package/src/vendor/pi-rich/photon.ts +0 -139
  348. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,26 +0,0 @@
1
- /** O.9: references to facts, never a child's self-assessed completion. */
2
- import type { LogEvent, NodeRef } from "./events.ts";
3
- import type { Handoff } from "./handoff.ts";
4
- import type { ExecutionOutcome } from "./outcome.ts";
5
- import { deriveRequestState } from "./request-state.ts";
6
-
7
- export function deriveBranchReceipt(input: {
8
- source: NodeRef; outcome: ExecutionOutcome; handoff: Handoff;
9
- nodes: readonly { node: number; events: readonly LogEvent[] }[];
10
- }) {
11
- const entries = input.nodes.flatMap(part => part.events.map(event => ({ node: part.node, event })));
12
- const commands = entries.flatMap(({ node, event }) => event.t === "tool_result" && event.effect?.kind === "command"
13
- ? [{ node, event: event.id, exit: event.effect.exit }] : []);
14
- const changes = entries.flatMap(({ node, event }) => event.t === "tool_result" && event.effect?.kind === "edit"
15
- ? [{ node, event: event.id, path: event.effect.path }] : []);
16
- const observations = entries.flatMap(({ node, event }) => event.t === "tool_result" ? [{ node, event: event.id }] : []);
17
- const unfinished = (input.handoff.state.work_items ?? []).filter(item => item.status === "active" || item.status === "blocked")
18
- .map(item => ({ work_id: item.id, status: item.status, next_action: item.next_action ?? null }));
19
- return { version: 1, source: input.source, execution: input.outcome,
20
- // S.1: the child's request ledger, the same verdict any request view shows.
21
- verification: deriveRequestState(input.nodes)?.status ?? "not_applicable",
22
- task_acceptance: "not_assessed", commands, changes, observations, unfinished,
23
- ...(input.handoff.processes?.length ? { processes: input.handoff.processes } : {}),
24
- unresolved_branches: input.handoff.branches.filter(b => b.resolution === null).map(b => b.node),
25
- };
26
- }
@@ -1,19 +0,0 @@
1
- import { modelKey } from "./session-position.ts";
2
-
3
- /** D13: identity and selection are independent of provider SDKs and source order. */
4
- export function mergeCatalogs<T extends { provider: string; id: string }>(...layers: readonly (readonly T[])[]): T[] {
5
- const models = new Map<string, T>();
6
- for (const layer of layers) for (const model of layer) models.set(modelKey(model), model);
7
- return [...models.values()];
8
- }
9
-
10
- /** Provider-local allow/deny patterns use literal characters plus '*' and '?'. */
11
- export function matchesModelPattern(id: string, pattern: string): boolean {
12
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
13
- return new RegExp(`^${escaped}$`, "u").test(id);
14
- }
15
-
16
- export function modelEnabled(id: string, allow?: readonly string[], deny?: readonly string[]): boolean {
17
- return (allow === undefined || allow.some(pattern => matchesModelPattern(id, pattern)))
18
- && !(deny?.some(pattern => matchesModelPattern(id, pattern)) ?? false);
19
- }
@@ -1,85 +0,0 @@
1
- /** P.3: recovery describes observed file transitions, never a complete disk image. */
2
- export type FileVersion = { sha256: string; mode: number } | null;
3
- export type FileChange = { before: FileVersion; after: FileVersion };
4
- export type FileChanges = Record<string, FileChange>;
5
- export interface CheckpointEvent {
6
- t: "checkpoint";
7
- id: string;
8
- at: string;
9
- operation: string;
10
- source: "edit" | "git" | "restore";
11
- phase: "prepared" | "applied" | "unavailable";
12
- changes: FileChanges;
13
- tree?: string;
14
- reason?: string;
15
- }
16
- export interface WorkspaceObservationEvent {
17
- t: "workspace_observation";
18
- id: string;
19
- at: string;
20
- /** P.3: this scope excludes ignored/build output and is not a backup. */
21
- fingerprint?: string;
22
- scope: "source-v1";
23
- changed: boolean;
24
- reason?: string;
25
- /** The workspace as a goal request began: the state a counter-check must revert to exactly. */
26
- baseline?: true;
27
- }
28
-
29
- export function sameVersion(a: FileVersion, b: FileVersion): boolean {
30
- return a === null || b === null ? a === b : a.sha256 === b.sha256 && a.mode === b.mode;
31
- }
32
- export function changedPaths(changes: FileChanges): string[] {
33
- return Object.keys(changes).filter(path => !sameVersion(changes[path]!.before, changes[path]!.after)).sort();
34
- }
35
-
36
- /** Undo the abandoned suffix then replay the selected suffix, touching only their paths. */
37
- export function planFileTransition(from: readonly CheckpointEvent[], to: readonly CheckpointEvent[]): FileChanges {
38
- const left = from.filter(event => event.phase === "applied");
39
- const right = to.filter(event => event.phase === "applied");
40
- let common = 0;
41
- while (common < left.length && common < right.length && left[common]!.id === right[common]!.id) common++;
42
- const changes: FileChanges = Object.create(null) as FileChanges;
43
- const apply = (path: string, before: FileVersion, after: FileVersion) => {
44
- const existing = changes[path];
45
- if (existing && !sameVersion(existing.after, before)) throw new Error(`discontinuous file history: ${path}; inspect intervening external edits`);
46
- changes[path] = { before: existing ? existing.before : before, after };
47
- };
48
- for (const event of left.slice(common).reverse()) for (const path of changedPaths(event.changes)) {
49
- const change = event.changes[path]!;
50
- apply(path, change.after, change.before);
51
- }
52
- for (const event of right.slice(common)) for (const path of changedPaths(event.changes)) {
53
- const change = event.changes[path]!;
54
- apply(path, change.before, change.after);
55
- }
56
- return Object.fromEntries(changedPaths(changes).map(path => [path, changes[path]!])) as FileChanges;
57
- }
58
-
59
- export function validFileChanges(value: unknown): value is FileChanges {
60
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
61
- const version = (v: unknown): boolean => v === null || typeof v === "object" && !Array.isArray(v) &&
62
- typeof (v as Record<string, unknown>)["sha256"] === "string" && /^[a-f0-9]{64}$/.test(String((v as Record<string, unknown>)["sha256"])) &&
63
- Number.isInteger((v as Record<string, unknown>)["mode"]) && Number((v as Record<string, unknown>)["mode"]) >= 0 && Number((v as Record<string, unknown>)["mode"]) <= 0o777;
64
- return Object.entries(value).every(([path, change]: [string, unknown]) => path.length > 0 && typeof change === "object" && change !== null &&
65
- version((change as Record<string, unknown>)["before"]) && version((change as Record<string, unknown>)["after"]));
66
- }
67
-
68
- /**
69
- * The one test for "this event changed the workspace": a real edit, an
70
- * applied checkpoint that moved a file, or an observation that saw a change.
71
- * Acceptance, request evidence and environment validity all ask exactly this
72
- * question and must not answer it three different ways (S.1).
73
- */
74
- export function workspaceChanged(event: import("./events.ts").LogEvent): boolean {
75
- return event.t === "tool_result" && event.effect?.kind === "edit" && event.effect.sha256_before !== event.effect.sha256_after ||
76
- event.t === "checkpoint" && event.phase === "applied" && changedPaths(event.changes).length > 0 ||
77
- event.t === "workspace_observation" && event.changed;
78
- }
79
-
80
- /** The transition back: every path's before and after swapped. Applying it undoes `changes`. */
81
- export function invertFileChanges(changes: FileChanges): FileChanges {
82
- const inverted: FileChanges = Object.create(null) as FileChanges;
83
- for (const [path, change] of Object.entries(changes)) inverted[path] = { before: change.after, after: change.before };
84
- return inverted;
85
- }
package/src/core/churn.ts DELETED
@@ -1,61 +0,0 @@
1
- /**
2
- * Two loops the repetition check cannot see.
3
- *
4
- * `continuationGuidance` flags an action/observation cycle that repeats
5
- * exactly. A real rut rarely does: a rerun test prints a new timestamp, a
6
- * build a new temp path, and every observation digest differs -- so neither
7
- * that check nor D9 stall counting fires while the model retries one idea.
8
- * These two keys come from facts the harness recorded itself, not from
9
- * comparing output: the exact command with its exit code, and the path of
10
- * each successful edit (LangChain's harness write-up uses the same edit
11
- * count; opencode's doom_loop misses exactly these near-repeats).
12
- *
13
- * Advice only, once per request: nothing is blocked and the stop rules are
14
- * unchanged.
15
- */
16
- import type { GuidanceEvent, LogEvent } from "./events.ts";
17
- import { currentRequestEvents } from "./request-state.ts";
18
-
19
- /**
20
- * Assumed, not measured. Three failures leaves room for "change, retry,
21
- * still failing" -- normal debugging -- before calling it a loop; eight edits
22
- * of one file sits above a typical refactor. Too low costs one extra notice;
23
- * too high falls back to today's behaviour.
24
- */
25
- export const CHURN_FAILURES = 3;
26
- export const CHURN_EDITS = 8;
27
-
28
- export function churnReview(events: readonly LogEvent[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
29
- const current = currentRequestEvents(events);
30
- if (current.some((event) => event.t === "guidance" && event.reason === "churn")) return null;
31
- const failures = new Map<string, { count: number; last: string }>();
32
- const edits = new Map<string, { count: number; last: string }>();
33
- for (const event of current) {
34
- if (event.t !== "tool_result" || event.effect === undefined) continue;
35
- const effect = event.effect;
36
- if (effect.kind === "command") {
37
- // R5 handles structured failures with version/change premises; keep this path for legacy logs.
38
- if (event.diagnostic?.execution !== undefined) continue;
39
- const key = effect.cmd.trim();
40
- if (effect.exit === 0) { failures.delete(key); continue; }
41
- const seen = { count: (failures.get(key)?.count ?? 0) + 1, last: event.id };
42
- failures.set(key, seen);
43
- if (seen.count >= CHURN_FAILURES) return {
44
- reason: "churn", sources: [event.id],
45
- text: `Harness: \`${key}\` failed ${seen.count} times in this request without succeeding. ` +
46
- "Retrying the same approach is unlikely to change the result. Read the end of the last failure, " +
47
- "state the cause you believe it has, and test that belief with a different step before running it again.",
48
- };
49
- } else if (effect.kind === "edit") {
50
- const seen = { count: (edits.get(effect.path)?.count ?? 0) + 1, last: event.id };
51
- edits.set(effect.path, seen);
52
- if (seen.count >= CHURN_EDITS) return {
53
- reason: "churn", sources: [event.id],
54
- text: `Harness: ${effect.path} was edited ${seen.count} times in this request. ` +
55
- "Repeated small edits usually mean the underlying cause is not yet understood. Re-read the file and the " +
56
- "failing evidence, then make one change that addresses the cause.",
57
- };
58
- }
59
- }
60
- return null;
61
- }
@@ -1,94 +0,0 @@
1
- /** R8 / D8 / D11: an explicit observation scope is not a claim of build causality. */
2
- import type { ArtifactRef } from "./events.ts";
3
- import { isObservationArtifact } from "./observation-view.ts";
4
-
5
- // Assumed observation budgets keep a model-supplied plan bounded before execution.
6
- export const MAX_COMMAND_ARTIFACTS = 16;
7
- export const MAX_ARTIFACT_PLAN_BYTES = 16 * 1024;
8
- export const MAX_COMMAND_ARTIFACT_BYTES = 512 * 1024 * 1024;
9
- export const COMMAND_ARTIFACT_SCOPE = "source-v1-minus-artifacts";
10
- export type CommandArtifactPlan = { paths: string[]; output_roots: string[] };
11
- export type ArtifactFileIdentity = { device: string; inode: string; mtime_ns: string; ctime_ns: string; mode: number };
12
- export type ArtifactFile = { state: "observed"; sha256: string; bytes: number; identity: ArtifactFileIdentity } |
13
- { state: "missing" | "unsafe" | "changed" | "unknown"; reason: string };
14
- export type ArtifactChange = "created" | "modified" | "unchanged" | "unknown";
15
- export type CommandArtifactOutput = { path: string; before: ArtifactFile; after: ArtifactFile; change: ArtifactChange };
16
- export type CommandArtifactReceipt = {
17
- kind: "command_artifacts"; version: 1;
18
- state: "pending" | "observed" | "changed" | "unknown";
19
- plan: CommandArtifactPlan;
20
- source: { host: string; workspace: string; cwd: string; runtime: string };
21
- command: string; exit: number | null;
22
- started_at: string; observed_at: string | null;
23
- inputs: { scope: typeof COMMAND_ARTIFACT_SCOPE; exclusions: string[]; before: string | null; after: string | null };
24
- outputs: CommandArtifactOutput[];
25
- reasons: string[];
26
- manifest?: ArtifactRef;
27
- };
28
-
29
- const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
30
- const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
31
- const covers = (root: string, path: string) => root === path || path.startsWith(root + "/");
32
- export function validArtifactPath(value: unknown): value is string {
33
- return typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).length <= 4096 &&
34
- !/[\x00-\x1f\x7f\\:]/.test(value) && value.split("/").every(part => part !== "" && part !== "." && part !== ".." && part !== ".git" && part !== ".xeno");
35
- }
36
- export function parseCommandArtifactPlan(value: unknown): CommandArtifactPlan {
37
- if (!object(value) || Object.keys(value).some(key => !["paths", "output_roots"].includes(key))) throw new Error("artifacts needs paths and optional output_roots only");
38
- const paths = value["paths"], roots = value["output_roots"] ?? paths;
39
- for (const list of [paths, roots]) {
40
- if (!Array.isArray(list) || list.length < 1 || list.length > MAX_COMMAND_ARTIFACTS || !list.every(validArtifactPath) || new Set(list).size !== list.length)
41
- throw new Error("artifacts needs 1..16 unique workspace-relative file paths; no traversal or protected paths");
42
- }
43
- const plan = { paths: [...paths as string[]].sort(), output_roots: [...roots as string[]].sort() };
44
- if (new TextEncoder().encode(JSON.stringify(plan)).length > MAX_ARTIFACT_PLAN_BYTES) throw new Error("artifact plan exceeds 16 KiB");
45
- if (plan.paths.some((path, i) => plan.paths.some((other, j) => i !== j && covers(path, other)))) throw new Error("artifact file paths cannot contain one another");
46
- if (plan.output_roots.some((root, i) => !plan.paths.some(path => covers(root, path)) || plan.output_roots.some((other, j) => i !== j && covers(root, other))) ||
47
- plan.paths.some(path => !plan.output_roots.some(root => covers(root, path)))) throw new Error("output_roots must be nonoverlapping and cover exactly the requested artifact paths");
48
- return plan;
49
- }
50
- export function artifactChange(before: ArtifactFile, after: ArtifactFile): ArtifactChange {
51
- if (after.state !== "observed") return "unknown";
52
- if (before.state === "missing") return "created";
53
- if (before.state !== "observed") return "unknown";
54
- return before.sha256 === after.sha256 && before.bytes === after.bytes && sameArtifactIdentity(before.identity, after.identity) ? "unchanged" : "modified";
55
- }
56
- export function sameArtifactIdentity(a: ArtifactFileIdentity, b: ArtifactFileIdentity): boolean {
57
- return a.device === b.device && a.inode === b.inode && a.mtime_ns === b.mtime_ns && a.ctime_ns === b.ctime_ns && a.mode === b.mode;
58
- }
59
- export function commandArtifactState(receipt: Pick<CommandArtifactReceipt, "exit" | "inputs" | "outputs" | "reasons">): CommandArtifactReceipt["state"] {
60
- if (receipt.exit === null) return "pending";
61
- if (receipt.inputs.before !== null && receipt.inputs.after !== null && receipt.inputs.before !== receipt.inputs.after ||
62
- receipt.outputs.some(output => output.after.state === "changed")) return "changed";
63
- if (receipt.exit !== 0 || receipt.reasons.length > 0 || receipt.inputs.before === null || receipt.inputs.after === null ||
64
- receipt.outputs.length === 0 || receipt.outputs.some(output => output.after.state !== "observed")) return "unknown";
65
- return "observed";
66
- }
67
- export function isArtifactFile(value: unknown): value is ArtifactFile {
68
- if (!object(value)) return false;
69
- if (value["state"] !== "observed") return ["missing", "unsafe", "changed", "unknown"].includes(String(value["state"])) && typeof value["reason"] === "string" && value["reason"].length > 0;
70
- const id = value["identity"];
71
- return digest(value["sha256"]) && Number.isSafeInteger(value["bytes"]) && Number(value["bytes"]) >= 0 && Number(value["bytes"]) <= MAX_COMMAND_ARTIFACT_BYTES &&
72
- object(id) && ["device", "inode", "mtime_ns", "ctime_ns"].every(key => typeof id[key] === "string" && /^-?\d+$/.test(id[key])) &&
73
- Number.isInteger(id["mode"]) && Number(id["mode"]) >= 0 && Number(id["mode"]) <= 0o777;
74
- }
75
- export function isCommandArtifactReceipt(value: unknown): value is CommandArtifactReceipt {
76
- if (!object(value) || value["kind"] !== "command_artifacts" || value["version"] !== 1 || typeof value["command"] !== "string" || !value["command"].trim() ||
77
- typeof value["started_at"] !== "string" || (value["exit"] !== null && !Number.isInteger(value["exit"]))) return false;
78
- let plan: CommandArtifactPlan;
79
- try { plan = parseCommandArtifactPlan(value["plan"]); } catch { return false; }
80
- if (!object(value["plan"]) || JSON.stringify(value["plan"]["paths"]) !== JSON.stringify(plan.paths) ||
81
- JSON.stringify(value["plan"]["output_roots"]) !== JSON.stringify(plan.output_roots)) return false;
82
- const source = value["source"], inputs = value["inputs"], outputs = value["outputs"], reasons = value["reasons"];
83
- if (!object(source) || !["host", "workspace", "cwd", "runtime"].every(key => typeof source[key] === "string" && source[key] !== "") ||
84
- !object(inputs) || inputs["scope"] !== COMMAND_ARTIFACT_SCOPE || !Array.isArray(inputs["exclusions"]) ||
85
- !inputs["exclusions"].every(path => typeof path === "string" && path.length > 0) || new Set(inputs["exclusions"]).size !== inputs["exclusions"].length ||
86
- !plan.output_roots.every(root => (inputs["exclusions"] as unknown[]).includes(root)) ||
87
- !["before", "after"].every(key => inputs[key] === null || digest(inputs[key])) || !Array.isArray(outputs) ||
88
- !Array.isArray(reasons) || !reasons.every(reason => typeof reason === "string" && reason.length > 0)) return false;
89
- if (value["exit"] === null) return value["state"] === "pending" && value["observed_at"] === null && inputs["after"] === null && outputs.length === 0 && value["manifest"] === undefined;
90
- if (typeof value["observed_at"] !== "string" || !value["observed_at"] || outputs.length !== plan.paths.length || !outputs.every((item, index) =>
91
- object(item) && item["path"] === plan.paths[index] && isArtifactFile(item["before"]) && isArtifactFile(item["after"]) && item["change"] === artifactChange(item["before"], item["after"]))) return false;
92
- if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
93
- return value["state"] === commandArtifactState(value as CommandArtifactReceipt) && (value["state"] !== "observed" || isObservationArtifact(value["manifest"]));
94
- }
@@ -1,151 +0,0 @@
1
- /** R1 / D11: all command entrances agree on invocation and observed termination. */
2
- import { executionOutcome } from "./outcome.ts";
3
- import type { ToolDiagnostic } from "./tool-contract.ts";
4
-
5
- /** Operational grace, not a measured optimum: ignored SIGTERM cannot defeat cancellation. */
6
- export const COMMAND_TERMINATION_GRACE_MS = 200;
7
-
8
- /** A fresh login Bash per command; interactive stdin does not change shell success semantics. */
9
- export function commandArgv(command: string): string[] {
10
- return ["bash", "-o", "pipefail", "-lc", command];
11
- }
12
-
13
- /**
14
- * Preserve inherited values except documented noninteractive defaults; never mutate the host.
15
- *
16
- * `appendPath` is xeno's own tool directory (store/tool-binaries.ts). It goes
17
- * last, so anything the user installed wins and xeno only fills a gap.
18
- */
19
- export function commandEnvironment(base: Readonly<Record<string, string | undefined>>, appendPath?: string): Record<string, string> {
20
- const env: Record<string, string> = {};
21
- for (const [key, value] of Object.entries(base)) if (value !== undefined) env[key] = value;
22
- if (appendPath !== undefined && appendPath.length > 0 && !(env["PATH"] ?? "").split(":").includes(appendPath)) {
23
- env["PATH"] = env["PATH"] ? `${env["PATH"]}:${appendPath}` : appendPath;
24
- }
25
- Object.assign(env, {
26
- TERM: "dumb", PAGER: "cat", GIT_PAGER: "cat", NO_COLOR: "1",
27
- GIT_TERMINAL_PROMPT: "0", DEBIAN_FRONTEND: "noninteractive",
28
- });
29
- if (env["LANG"] === undefined || env["LANG"] === "") env["LANG"] = "C.UTF-8";
30
- return env;
31
- }
32
-
33
- /**
34
- * Codex's `max_output_tokens` as an inline byte budget for one call. It can
35
- * only lower the session ceiling: full output is still captured and
36
- * recoverable, so this shapes the preview, never the receipt.
37
- *
38
- * Assumed, not measured: 4 bytes per token rounds the ASCII estimate in
39
- * tokens.ts (about 3.6) up, so the model never gets less than it asked for.
40
- * The 1 KiB floor keeps room for the header and recovery hint (about 300
41
- * bytes); below it a preview shows almost no output and forces a re-read.
42
- */
43
- export const OUTPUT_BYTES_PER_TOKEN = 4;
44
- export const MIN_OUTPUT_BYTES = 1_024;
45
- export function outputBytesForTokens(tokens: unknown, ceiling: number): number {
46
- if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0) return ceiling;
47
- return Math.min(ceiling, Math.max(MIN_OUTPUT_BYTES, Math.floor(tokens) * OUTPUT_BYTES_PER_TOKEN));
48
- }
49
-
50
- /** Model seconds and host milliseconds meet once; a yield is never a command deadline. */
51
- export function commandTimeout(requestedSeconds: number | undefined, defaultMs: number, maximumMs = defaultMs): number {
52
- return Math.min(requestedSeconds === undefined ? defaultMs : requestedSeconds * 1000, maximumMs);
53
- }
54
-
55
- /**
56
- * Programs whose exit 1 is an answer rather than a fault (POSIX: grep's 1 is
57
- * "no match" and >1 an error; diff and cmp use 1 for "differ"; test for
58
- * false). Reporting them as failures tells the model to inspect and retry an
59
- * answer it already holds -- the same reading Claude Code's command semantics
60
- * give these programs.
61
- */
62
- const NEGATIVE_ANSWERS: Readonly<Record<string, string>> = {
63
- grep: "no match", egrep: "no match", fgrep: "no match", rg: "no match",
64
- diff: "files differ", cmp: "files differ", test: "condition false", "[": "condition false",
65
- };
66
-
67
- /**
68
- * The final simple command of `command` and the separator before it, or
69
- * null when this scanner cannot read the command with certainty --
70
- * unbalanced quotes, subshells, compound statements, substitutions or a
71
- * background `&`. Only top-level `;`, newline, `&&`, `||` and `|` split.
72
- */
73
- function finalStage(command: string): { separator: string; words: string[] } | null {
74
- let quote: "'" | "\"" | null = null, start = 0, separator = "";
75
- for (let i = 0; i < command.length; i++) {
76
- const c = command[i]!;
77
- if (quote !== null) {
78
- if (c === "\\" && quote === "\"") i++;
79
- else if (c === quote) quote = null;
80
- continue;
81
- }
82
- if (c === "\\") { i++; continue; }
83
- if (c === "'" || c === "\"") { quote = c; continue; }
84
- if (c === "`" || c === "(" || c === ")" || c === "{" || c === "}" || (c === "$" && command[i + 1] === "(")) return null;
85
- const two = command.slice(i, i + 2);
86
- if (two === "&&" || two === "||") { separator = two; start = i + 2; i++; continue; }
87
- if (c === ";" || c === "\n" || c === "|") { separator = c; start = i + 1; continue; }
88
- if (c === "&" && command[i + 1] !== ">" && command[i - 1] !== ">") return null;
89
- }
90
- if (quote !== null) return null;
91
- return { separator, words: command.slice(start).trim().split(/\s+/).filter(word => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) };
92
- }
93
-
94
- const programName = (words: readonly string[]) => (words[0] ?? "").replace(/^.*\//, "");
95
-
96
- /**
97
- * What exit 1 of the command's final simple command means, or null.
98
- *
99
- * Only the last segment after a top-level `;`, newline, `||` or `|` is read.
100
- * After `&&` the 1 may come from the command that short-circuited, so it is
101
- * not classified. Under pipefail an earlier pipeline stage can also set the
102
- * status; the exit is kept in the receipt either way, and the label states
103
- * the program's meaning rather than claiming success. Anything this scanner
104
- * cannot read with certainty -- unbalanced quotes, subshells, compound
105
- * statements, substitutions -- yields null.
106
- */
107
- export function negativeAnswer(command: string, exit: number): string | null {
108
- if (exit !== 1) return null;
109
- const stage = finalStage(command);
110
- if (stage === null || stage.separator === "&&") return null;
111
- const program = programName(stage.words);
112
- if (program === "git") return stage.words[1] === "grep" ? "no match" : null;
113
- return Object.hasOwn(NEGATIVE_ANSWERS, program) ? NEGATIVE_ANSWERS[program]! : null;
114
- }
115
-
116
- /**
117
- * Readers that may stop reading before their input ends (`head -n`, `sed Nq`,
118
- * `awk '{exit}'`, `grep -m` / `-q`), closing the pipe on the writer before it.
119
- */
120
- const EARLY_READERS: ReadonlySet<string> = new Set(["head", "sed", "awk", "gawk", "mawk", "grep", "egrep", "fgrep", "rg"]);
121
-
122
- /**
123
- * What exit 141 means when an early reader ends the pipeline, or null.
124
- *
125
- * Measured (pipe-early-close card): under `bash -o pipefail`, `seq 1 1000000
126
- * | head -1` exits 141 -- pipefail reports the rightmost nonzero stage, here
127
- * the writer that SIGPIPE killed after head closed its input, while every
128
- * stage to its right, the final reader included, exited 0. The output the
129
- * command asked for is complete. Only a final stage joined by a top-level
130
- * `|` is read: after `;`, `&&` or `||` the 141 is the last command's own.
131
- */
132
- export function earlyCloseAnswer(command: string, exit: number): string | null {
133
- if (exit !== 141) return null;
134
- const stage = finalStage(command);
135
- if (stage === null || stage.separator !== "|") return null;
136
- const program = programName(stage.words);
137
- return EARLY_READERS.has(program) ? `${program} closed the pipe early; the earlier stage's SIGPIPE is not a failure` : null;
138
- }
139
-
140
- /** The program's own meaning of a nonzero exit that is an answer, not a fault. */
141
- export function commandAnswer(command: string, exit: number): string | null {
142
- return negativeAnswer(command, exit) ?? earlyCloseAnswer(command, exit);
143
- }
144
-
145
- /** A trap can exit zero after cancellation: successful exit alone does not erase the stop. */
146
- export function commandCompletion(exit: number, stopped?: "cancelled" | "timeout", cleanup?: ToolDiagnostic, command?: string) {
147
- const answered = exit === 0 || command !== undefined && commandAnswer(command, exit) !== null;
148
- return { ok: stopped === undefined && answered && cleanup === undefined,
149
- outcome: executionOutcome(stopped ?? (answered && cleanup === undefined ? "tool_completed" : "tool_failed")),
150
- ...(cleanup ? { warnings: [cleanup] } : {}) };
151
- }
@@ -1,20 +0,0 @@
1
- import { assertNever } from "./exhaustive.ts";
2
-
3
- /** D19: confirmation follows the selected completion, never the unfinished draft. */
4
- export function completionAction(kind: "command" | "argument" | "path", key: "enter" | "tab", requiresArgument: boolean): "insert" | "submit" {
5
- if (key === "tab") return "insert";
6
- switch (kind) {
7
- case "command": return requiresArgument ? "insert" : "submit";
8
- case "argument": return "submit";
9
- case "path": return "insert";
10
- default: return assertNever(kind, "completion kind");
11
- }
12
- }
13
-
14
- /** Match the parser's exact identity before discoverable prefixes and substrings. */
15
- export function commandCompletionRank(name: string, query: string): number {
16
- const canonical = name.toLowerCase(), typed = query.toLowerCase();
17
- if (canonical === typed) return 0;
18
- if (canonical.startsWith(typed)) return 1;
19
- return typed.length > 1 && canonical.includes(typed) ? 2 : Number.POSITIVE_INFINITY;
20
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * A one-time reconciliation when the model ends a request.
3
- *
4
- * The model deciding it is done is a claim like any other (D11), and the
5
- * failure modes that end a Terminal-Bench run early all look like a normal
6
- * final reply: an edit nobody ran, a question handed back to a user who is
7
- * not there, or nothing at all. ForgeCode found a programmatic check at this
8
- * point was its largest single gain and that a prompt instruction alone did
9
- * not reproduce it. The check here only appends a notice; the model still
10
- * decides, and a second finish is respected -- an honest blocker must not
11
- * turn into a loop.
12
- */
13
- import type { GuidanceEvent, LogEvent } from "./events.ts";
14
- import type { AcceptanceState } from "./acceptance.ts";
15
- import { deriveRequestState, type RequestState } from "./request-state.ts";
16
-
17
- /**
18
- * Wording that hands the task back instead of finishing it. Ported from
19
- * waner `crates/waner_core/src/turn/premature_stop.rs`, including its window:
20
- * the first 400 and last 300 characters, where an opening refusal or a
21
- * closing offer sits. The same words in the middle of a long report are
22
- * usually a quotation or a description, not a stop.
23
- */
24
- const HAND_BACK = [
25
- "i can't proceed", "i cannot proceed", "i can't continue", "i cannot continue",
26
- "giving up", "stopping here", "unable to complete", "i'm unable to", "i am unable to",
27
- "let me know if you", "would you like me to", "do you want me to", "shall i ",
28
- "我无法继续", "无法完成", "需要你确认", "请确认是否", "是否需要我", "要不要我",
29
- ];
30
-
31
- export function looksLikeHandBack(text: string): boolean {
32
- const chars = [...text.trim()];
33
- const window = `${chars.slice(0, 400).join("")}\n${chars.slice(Math.max(0, chars.length - 300)).join("")}`.toLowerCase();
34
- return HAND_BACK.some((pattern) => window.includes(pattern));
35
- }
36
-
37
- export function completionReview(
38
- events: readonly LogEvent[],
39
- remainingRequests: number,
40
- options?: { strictVerification?: boolean; acceptance?: AcceptanceState },
41
- ): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
42
- const state = deriveRequestState([{ node: 0, events }], { strict: options?.strictVerification === true });
43
- const decision = state ? requestCompletion(state, remainingRequests, options) : { action: "finish" as const };
44
- return decision.action === "continue" ? decision.guidance : null;
45
- }
46
-
47
- export type RequestCompletion = { action: "finish" } |
48
- { action: "continue"; guidance: Pick<GuidanceEvent, "reason" | "text" | "sources"> };
49
-
50
- /**
51
- * Q23 / S.1: loop termination reads the same ledger as every request view.
52
- * A reported blocker does not end the check early: the model's statement is
53
- * not evidence (D11), and the check still runs only once, so an honest
54
- * blocker restated at the second finish is respected.
55
- */
56
- export function requestCompletion(state: RequestState, remainingRequests: number,
57
- options?: { acceptance?: AcceptanceState }): RequestCompletion {
58
- const finish = { action: "finish" as const };
59
- if (remainingRequests < 1 || state.reviewed) return finish;
60
- const final = state.final;
61
- if (!final || final.has_tools || state.execution?.reason !== "model_end" && state.execution?.reason !== "unknown") return finish;
62
- const text = final.text;
63
-
64
- const gaps: string[] = [];
65
- const acceptance = options?.acceptance ?? state.acceptance;
66
- const missingCommands = acceptance?.status === "needs_work" ? acceptance.criteria.flatMap(item =>
67
- item.kind === "command" && item.status !== "pass" ? [{ id: item.id, command: item.command, status: item.status }] : []) : [];
68
- const freshCheck = acceptance ? undefined : state.criteria.find(item => item.kind === "fresh_check");
69
- if (state.used_tools && text.length === 0) gaps.push("the final reply is empty");
70
- // Strict verification counts only configured gates (O.11); say so, since a successful `cat` would otherwise look like a check.
71
- if (freshCheck && freshCheck.status !== "pass") gaps.push(freshCheck.gate_only
72
- ? "files were changed but no configured verification gate (a test, typecheck, lint, build or check command) has passed since the last edit; reading or printing files does not count, and a gate counts only when run as plain commands joined by && (trailing output redirects to fixed files are fine); ;, ||, pipes, $() and variables hide it"
73
- : "files were changed but no command has succeeded since the last edit");
74
- if (acceptance?.status === "needs_work") gaps.push(`declared acceptance criteria need evidence: ${[
75
- ...missingCommands.map(item => `${item.id}=${item.status}`),
76
- ...(acceptance.targets ?? []).filter(target => target.status !== "ready").map(target => `target ${target.id}=${target.status}`),
77
- ].join(", ")}`);
78
- if (text.length > 0 && looksLikeHandBack(text) && acceptance?.status !== "awaiting_human")
79
- gaps.push("the reply deferred the request; check whether authorized work remains or a specific blocker requires the user");
80
- if (gaps.length === 0) return finish;
81
- // D11 / O.11: a declared command needs its exact receipt even when the
82
- // request has no recorded edit. Compound shell success is not that receipt.
83
- const next = missingCommands.length > 0
84
- ? "Obtain a fresh successful receipt for each missing command. Run each command exactly as a separate invocation without adding other commands:\n" +
85
- missingCommands.map(item => `- ${item.id}: ${JSON.stringify(item.command)}`).join("\n") +
86
- "\nIf verification cannot run, report the specific blocker. "
87
- : !state.changed
88
- ? "Use original sources for research and full inputs for calculations; do not invent command evidence. Finish with the result or the specific blocker. "
89
- : "Run the command that verifies the result, or finish with the specific blocker that prevents verification. ";
90
- return { action: "continue", guidance: {
91
- reason: "completion_review",
92
- sources: [state.request.event, final.source.event],
93
- text: `Harness completion check: ${gaps.join("; ")}. ` +
94
- `Re-read the original request (event ${state.request.event}) and check each requirement against evidence in this session. ` +
95
- next + (acceptance?.criteria.some(item => item.kind === "human" && item.status !== "accepted") ? "Required human acceptance remains pending. " : "") +
96
- "This check runs once per request.",
97
- } };
98
- }
@@ -1,37 +0,0 @@
1
- /** O.8: a read-only explanation of the same projection that is dispatched. */
2
- import { projectContext, type ProjectInput } from "./project.ts";
3
- import { anchoredCount, estimateTokens, type Anchor, type TokenCounter } from "./tokens.ts";
4
- import { historyItems, HISTORY_SECTIONS } from "./history.ts";
5
- import { contextLimits, type ContextPolicy } from "./context-policy.ts";
6
- import type { BoundaryConfig } from "./boundary.ts";
7
-
8
- export function contextDetail(input: ProjectInput & {
9
- anchor: Anchor | null; counter?: TokenCounter; policy: ContextPolicy;
10
- contextWindow: number | undefined; outputReserve: number; boundary?: Partial<BoundaryConfig>;
11
- }) {
12
- const counter = input.counter ?? estimateTokens;
13
- const projection = projectContext({ ...input, trace: true });
14
- const base = counter.count([], "", []);
15
- const system = counter.count([], projection.system, []);
16
- const tools = counter.count([], projection.system, projection.tools);
17
- const floor = counter.count(projection.messages.slice(0, input.handoff ? 1 : 0), projection.system, projection.tools);
18
- const estimated = counter.count(projection.messages, projection.system, projection.tools);
19
- const anchored = anchoredCount(projection.messages, projection.system, input.anchor, counter, projection.tools);
20
- const sections = input.handoff ? HISTORY_SECTIONS.map(section => {
21
- const retained = historyItems(input.handoff!, section).length;
22
- const index = input.handoff!.history?.sections[section];
23
- return { section, retained, omitted: index?.offset ?? 0, total: index?.total ?? retained,
24
- recovery: index && index.offset > 0 ? { node: input.handoff!.history!.source.node, section, offset: 0 } : null };
25
- }) : [];
26
- return {
27
- version: projection.provenance!.version,
28
- countKind: counter.kind,
29
- components: { framing: base, system: system - base, tools: tools - system, handoff: floor - tools, messages: estimated - floor },
30
- estimatedTotal: estimated, used: anchored.tokens,
31
- anchorCorrection: anchored.tokens - estimated,
32
- measuredThrough: anchored.exactUpTo,
33
- limits: contextLimits(input), policy: input.policy,
34
- sources: projection.provenance!.messages, omittedEvents: projection.provenance!.omitted,
35
- sections, violations: projection.violations,
36
- };
37
- }