@visual-z/xeno 0.1.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 (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Restoring the workspace to a node.
3
+ *
4
+ * This is where appendix H's argument becomes executable: reverting is not
5
+ * overwriting the present with the past, it is moving a cursor to another
6
+ * node. The nodes are untouched, so the cursor can move again -- which is
7
+ * why a restore here cannot produce the failure that made Zed's checkpoint
8
+ * restore an unanswered S1, where reverting destroyed both the files and the
9
+ * thread with no way back.
10
+ *
11
+ * WHY THIS DOES NOT REUSE readAncestry. That walk stops at a cut, because a
12
+ * handoff basis means the parent's events do not replay -- correct for
13
+ * context, catastrophic for files. A node whose ancestry crosses a cut would
14
+ * see only the snapshots after it, and applyRestore deletes whatever is not
15
+ * in its target, so restoring past a boundary would silently remove every
16
+ * file written before it. File history therefore always walks to the root,
17
+ * whatever each node declared about context.
18
+ */
19
+
20
+ import { readNode, readManifest, digestNode, MAX_ANCESTRY_DEPTH, type Manifest } from "./log.ts";
21
+ import type { LogEvent } from "../core/events.ts";
22
+ import {
23
+ planRestore, applyRestore, scanWorkspace, DEFAULT_IGNORE,
24
+ type ScanDelta, type FileSet, type RestoreResult,
25
+ } from "./workspace.ts";
26
+
27
+ /**
28
+ * Where to move the workspace to.
29
+ *
30
+ * A bare node means the end of that node. `atEvent` names a snapshot inside
31
+ * it, which is what makes an ordinary undo possible: a node accumulates many
32
+ * turns, so without it the earliest reachable state is the end of the node
33
+ * and nothing in the node being worked in can be taken back.
34
+ */
35
+ export type RestoreTarget = number | { node: number; atEvent?: string };
36
+
37
+ /** One snapshot, kept separate from its neighbours so a turn can be addressed. */
38
+ export type FileDelta = ScanDelta & { node: number; event: string };
39
+
40
+ export type FileHistoryResult =
41
+ | { ok: true; deltas: FileDelta[] }
42
+ | { ok: false; error: string };
43
+
44
+ /** One ancestor's contribution: its events, bounded by any fork point below it. */
45
+ export interface NodeEvents {
46
+ node: number;
47
+ events: LogEvent[];
48
+ }
49
+
50
+ export type NodeHistoryResult =
51
+ | { ok: true; nodes: NodeEvents[] }
52
+ | { ok: false; error: string };
53
+
54
+ export type PlanResult =
55
+ | { ok: true; files: FileSet; willWrite: string[]; willDelete: string[] }
56
+ | { ok: false; error: string };
57
+
58
+ export type RestoreToNodeResult =
59
+ | ({ ok: true } & RestoreResult)
60
+ | { ok: false; error: string };
61
+
62
+ /**
63
+ * Every snapshot delta from the root to `node`, in replay order.
64
+ *
65
+ * Two fields on the way up, treated oppositely, because they answer different
66
+ * questions:
67
+ *
68
+ * `basis` says what the MODEL should see, and is ignored here. A cut
69
+ * truncates the context walk; the files it wrote are still on disk.
70
+ *
71
+ * `at_event` says where this node's history BEGINS in its parent, which is
72
+ * a question about history, so it is obeyed. Ignoring it makes a rewind
73
+ * restore the very file it was asked to undo -- the parent's later turns
74
+ * would flow into a node that forked before them.
75
+ */
76
+ /**
77
+ * Every ancestor's events, root-to-leaf, bounded by the fork points below.
78
+ *
79
+ * The one traversal both file history and turn history are built on. They
80
+ * have to agree about where a node's history begins, or a rewind would move
81
+ * the files to one point and the conversation to another.
82
+ */
83
+ export async function readNodeHistory(
84
+ dir: string,
85
+ node: number,
86
+ manifest?: Manifest,
87
+ scope: "workspace" | "context" = "workspace",
88
+ ): Promise<NodeHistoryResult> {
89
+ const index = manifest ?? await readManifest(dir);
90
+ const chain: NodeEvents[] = [];
91
+ const seen = new Set<number>();
92
+ let current: number | null = node;
93
+ // Set by a child's parent pointer: this ancestor contributes only the
94
+ // events up to and including that one.
95
+ let cutoff: string | undefined;
96
+ let depth = 0;
97
+
98
+ while (current !== null) {
99
+ if (seen.has(current)) {
100
+ return {
101
+ ok: false,
102
+ error: `CYCLE: parent chain from node ${node} revisits node ${current}`,
103
+ };
104
+ }
105
+ seen.add(current);
106
+ if (depth++ >= MAX_ANCESTRY_DEPTH) {
107
+ return {
108
+ ok: false,
109
+ error: `TOO_DEEP: history of node ${node} exceeded ${MAX_ANCESTRY_DEPTH} nodes`,
110
+ };
111
+ }
112
+
113
+ const { header, events, corrupt } = await readNode(dir, current);
114
+ const sealed = index.nodes.find((n) => n.node === current);
115
+ if (sealed && sealed.sha256 !== await digestNode(dir, current)) {
116
+ return { ok: false, error: `INTEGRITY: node ${current} digest differs from its sealed record` };
117
+ }
118
+ if (header === null) {
119
+ return { ok: false, error: `MISSING_HEADER: node ${current} has no header line` };
120
+ }
121
+ if (corrupt.length > 0) {
122
+ return {
123
+ ok: false,
124
+ error: `CORRUPT: node ${current} has ${corrupt.length} unparsable line(s)`,
125
+ };
126
+ }
127
+
128
+ // The fork point is an event id in the node's whole stream, not a
129
+ // snapshot index: a turn that changed nothing writes no snapshot, so
130
+ // counting snapshots would land on the wrong turn.
131
+ let contributed = events;
132
+ if (cutoff !== undefined) {
133
+ const at = events.findIndex((e) => e.id === cutoff);
134
+ if (at === -1) {
135
+ // Refused rather than falling back to the whole node. The fallback is
136
+ // the silent form of the bug the cutoff exists to prevent: it would
137
+ // restore exactly the work the fork was made to leave behind.
138
+ return {
139
+ ok: false,
140
+ error: `NO_SUCH_EVENT: node ${current} has no event ${cutoff} (fork point reached from ${node})`,
141
+ };
142
+ }
143
+ contributed = events.slice(0, at + 1);
144
+ }
145
+
146
+ chain.push({ node: current, events: contributed });
147
+ cutoff = header.parent?.at_event;
148
+ // A fresh subagent inherits files, not the parent's observations. Context
149
+ // history crosses compaction boundaries but stops at an explicit new root.
150
+ current = scope === "context" && header.basis === "none" ? null : header.parent?.node ?? null;
151
+ }
152
+
153
+ chain.reverse();
154
+ return { ok: true, nodes: chain };
155
+ }
156
+
157
+ /**
158
+ * Every snapshot delta from the root to `node`, in replay order.
159
+ *
160
+ * Two fields on the way up, treated oppositely, because they answer different
161
+ * questions:
162
+ *
163
+ * `basis` says what the MODEL should see, and is ignored here. A cut
164
+ * truncates the context walk; the files it wrote are still on disk.
165
+ *
166
+ * `at_event` says where this node's history BEGINS in its parent, which is
167
+ * a question about history, so it is obeyed. Ignoring it makes a rewind
168
+ * restore the very file it was asked to undo -- the parent's later turns
169
+ * would flow into a node that forked before them.
170
+ *
171
+ * Kept one entry per snapshot rather than merged per node: merging is lossy
172
+ * in exactly the direction that matters, since the turns inside a node are
173
+ * the ones a person is most likely to want back.
174
+ */
175
+ export async function readFileHistory(
176
+ dir: string,
177
+ node: number,
178
+ manifest?: Manifest,
179
+ ): Promise<FileHistoryResult> {
180
+ const history = await readNodeHistory(dir, node, manifest);
181
+ if (!history.ok) return { ok: false, error: history.error };
182
+
183
+ const deltas: FileDelta[] = [];
184
+ for (const n of history.nodes) {
185
+ for (const e of n.events) {
186
+ if (e.t !== "snapshot") continue;
187
+ deltas.push({
188
+ node: n.node, event: e.id,
189
+ added: { ...e.added }, changed: { ...e.changed }, removed: [...e.removed],
190
+ });
191
+ }
192
+ }
193
+ return { ok: true, deltas };
194
+ }
195
+
196
+ /**
197
+ * What a restore would do, without doing it.
198
+ *
199
+ * Separate from the restore because applyRestore deletes whatever is not in
200
+ * the target -- necessary, or the workspace would end up a mixture of two
201
+ * states, but destructive enough that a caller has to be able to look first.
202
+ */
203
+ export async function planRestoreToNode(
204
+ dir: string,
205
+ target: RestoreTarget,
206
+ manifest?: Manifest,
207
+ root?: string,
208
+ ignore: readonly string[] = DEFAULT_IGNORE,
209
+ ): Promise<PlanResult> {
210
+ const { node, atEvent } = typeof target === "number" ? { node: target, atEvent: undefined } : target;
211
+ const history = await readNodeHistory(dir, node, manifest);
212
+ if (!history.ok) return { ok: false, error: history.error };
213
+
214
+ const deltas: FileDelta[] = [];
215
+ for (const n of history.nodes) {
216
+ // The cutoff is an id in the EVENT stream, not in the snapshot stream: a
217
+ // fork point is where a conversation was whole, which is the end of an
218
+ // exchange and usually not a snapshot at all. Cutting the events first
219
+ // and collecting snapshots after is what keeps the files and the
220
+ // conversation landing on the same point.
221
+ let events = n.events;
222
+ if (atEvent !== undefined && n.node === node) {
223
+ const at = events.findIndex((e) => e.id === atEvent);
224
+ if (at === -1) {
225
+ return { ok: false, error: `NO_SUCH_EVENT: node ${node} has no event ${atEvent}` };
226
+ }
227
+ events = events.slice(0, at + 1);
228
+ }
229
+ for (const e of events) {
230
+ if (e.t !== "snapshot") continue;
231
+ deltas.push({
232
+ node: n.node, event: e.id,
233
+ added: { ...e.added }, changed: { ...e.changed }, removed: [...e.removed],
234
+ });
235
+ }
236
+ }
237
+
238
+ const files = planRestore(deltas, node);
239
+ const willWrite = Object.keys(files).sort();
240
+ if (!root) return { ok: true, files, willWrite, willDelete: [] };
241
+
242
+ const current = await scanWorkspace(root, ignore);
243
+ if (!current.ok) return { ok: false, error: current.error };
244
+ const willDelete = Object.keys(current.files).filter((p) => !(p in files)).sort();
245
+ return { ok: true, files, willWrite, willDelete };
246
+ }
247
+
248
+ /**
249
+ * Moves the workspace to the state it had at `node`.
250
+ *
251
+ * Writes nothing to the log. The log is the truth and the workspace is a
252
+ * projection of it, so there is nothing to record: the nodes did not change,
253
+ * only which one is being projected. That is also what keeps this safe
254
+ * despite being irreversible in itself -- the previous cursor position is
255
+ * still a node, so the move can be made again in the other direction.
256
+ */
257
+ export async function restoreToNode(
258
+ dir: string,
259
+ blobDir: string,
260
+ root: string,
261
+ target: RestoreTarget,
262
+ manifest?: Manifest,
263
+ ignore: readonly string[] = DEFAULT_IGNORE,
264
+ ): Promise<RestoreToNodeResult> {
265
+ const plan = await planRestoreToNode(dir, target, manifest ?? (await readManifest(dir)), root, ignore);
266
+ if (!plan.ok) return { ok: false, error: plan.error };
267
+ return { ok: true, ...(await applyRestorePlan(root, blobDir, plan.files, ignore, describeTarget(target))) };
268
+ }
269
+
270
+ /**
271
+ * Applies a plan that has already been computed.
272
+ *
273
+ * Split out so a caller holding a plan does not have to compute it twice.
274
+ * That is not only wasted work: a caller that re-planned inside the apply
275
+ * could never see the apply fail on its own, which turns its own "did the
276
+ * files move?" guard into unreachable code.
277
+ */
278
+ export async function applyRestorePlan(
279
+ root: string,
280
+ blobDir: string,
281
+ files: FileSet,
282
+ ignore: readonly string[],
283
+ label: string,
284
+ ): Promise<RestoreResult> {
285
+ const result = await applyRestore(root, blobDir, files, ignore);
286
+ if (result.unrecoverable.length > 0) return result;
287
+ return {
288
+ ...result,
289
+ note:
290
+ `Workspace moved to ${label}. ` +
291
+ result.note +
292
+ " Other logged snapshots remain available. File versions never " +
293
+ "snapshotted cannot be reconstructed from the log.",
294
+ };
295
+ }
296
+
297
+ function describeTarget(target: RestoreTarget): string {
298
+ if (typeof target === "number") return `node ${target}`;
299
+ return target.atEvent === undefined
300
+ ? `node ${target.node}`
301
+ : `node ${target.node} as of snapshot ${target.atEvent}`;
302
+ }