@visual-z/xeno 0.2.0 → 0.3.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 (259) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +52 -9
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/cli.ts +10 -11
  8. package/src/apps/commands.ts +38 -5
  9. package/src/apps/completions.ts +73 -19
  10. package/src/apps/image-draft.ts +46 -0
  11. package/src/apps/project-trust.ts +2 -42
  12. package/src/apps/provider-connect.ts +346 -0
  13. package/src/apps/render.ts +22 -47
  14. package/src/apps/resources.ts +5 -2
  15. package/src/apps/shell.ts +298 -1526
  16. package/src/apps/startup-input.ts +130 -0
  17. package/src/apps/startup.ts +143 -115
  18. package/src/apps/ui/app.ts +1383 -0
  19. package/src/apps/ui/extension-host.ts +194 -0
  20. package/src/apps/ui/image-cache.ts +78 -0
  21. package/src/apps/ui/palette.ts +88 -0
  22. package/src/apps/ui/project.ts +127 -0
  23. package/src/apps/ui/standalone.ts +146 -0
  24. package/src/apps/ui/status-model.ts +68 -0
  25. package/src/apps/ui/surface.ts +84 -0
  26. package/src/apps/ui/theme.ts +86 -0
  27. package/src/core/acceptance.ts +107 -0
  28. package/src/core/action-defaults.ts +47 -295
  29. package/src/core/benchmark.ts +2 -2
  30. package/src/core/branch-receipt.ts +24 -0
  31. package/src/core/checkpoints.ts +64 -0
  32. package/src/core/churn.ts +2 -0
  33. package/src/core/command-artifacts.ts +94 -0
  34. package/src/core/command-contract.ts +35 -0
  35. package/src/core/completion-review.ts +21 -5
  36. package/src/core/context-detail.ts +37 -0
  37. package/src/core/context-policy.ts +74 -1
  38. package/src/core/continuation.ts +17 -2
  39. package/src/core/derive.ts +29 -3
  40. package/src/core/draft-images.ts +56 -0
  41. package/src/core/environment.ts +145 -0
  42. package/src/core/evaluation-coverage.ts +74 -0
  43. package/src/core/event-validation.ts +80 -5
  44. package/src/core/events.ts +97 -3
  45. package/src/core/evidence-links.ts +7 -0
  46. package/src/core/evidence.ts +13 -3
  47. package/src/core/execution.ts +8 -2
  48. package/src/core/experiment.ts +133 -0
  49. package/src/core/export.ts +9 -0
  50. package/src/core/failure-recovery.ts +173 -0
  51. package/src/core/file-precondition.ts +72 -0
  52. package/src/core/handoff.ts +65 -4
  53. package/src/core/history-query.ts +38 -2
  54. package/src/core/history.ts +38 -1
  55. package/src/core/integration-health.ts +34 -0
  56. package/src/core/keymap.ts +8 -20
  57. package/src/core/model-picker.ts +64 -0
  58. package/src/core/native-eval.ts +25 -0
  59. package/src/core/note-guidance.ts +34 -0
  60. package/src/core/observation-view.ts +49 -8
  61. package/src/core/permissions.ts +24 -2
  62. package/src/core/process-receipt.ts +105 -0
  63. package/src/core/process-state.ts +37 -0
  64. package/src/core/project.ts +45 -18
  65. package/src/core/prompt-policy.ts +37 -0
  66. package/src/core/query-eval-contract.ts +14 -0
  67. package/src/core/read-observation.ts +82 -0
  68. package/src/core/recovery-corpus.ts +17 -0
  69. package/src/core/recovery-evaluation.ts +60 -0
  70. package/src/core/recovery-sources.ts +74 -0
  71. package/src/core/runtime-target.ts +94 -0
  72. package/src/core/search-observation.ts +97 -0
  73. package/src/core/session-tree.ts +147 -0
  74. package/src/core/settings.ts +26 -10
  75. package/src/core/skills.ts +44 -0
  76. package/src/core/sql-query.ts +211 -0
  77. package/src/core/theme.ts +36 -58
  78. package/src/core/tool-contract.ts +51 -2
  79. package/src/core/tool-discovery.ts +87 -0
  80. package/src/core/tool-failure.ts +60 -0
  81. package/src/core/tool-mode-protocol.ts +25 -0
  82. package/src/core/transcript.ts +19 -4
  83. package/src/core/trial-lifecycle.ts +14 -0
  84. package/src/core/tui-mode.ts +29 -0
  85. package/src/core/update-check.ts +97 -0
  86. package/src/core/version.ts +55 -0
  87. package/src/core/work-state.ts +49 -3
  88. package/src/core/working-set.ts +91 -0
  89. package/src/runtime/auth.ts +2 -2
  90. package/src/runtime/branch-jobs.ts +36 -0
  91. package/src/runtime/clipboard.ts +15 -4
  92. package/src/runtime/command-artifacts.ts +64 -0
  93. package/src/runtime/environment-tools.ts +60 -0
  94. package/src/runtime/evidence-notes.ts +4 -2
  95. package/src/runtime/exec-sessions.ts +206 -61
  96. package/src/runtime/execution-control.ts +1 -0
  97. package/src/runtime/extension-ui.ts +22 -30
  98. package/src/runtime/extensions.ts +95 -16
  99. package/src/runtime/harness-comparison.ts +27 -7
  100. package/src/runtime/harness-eval.ts +460 -0
  101. package/src/runtime/history-cache.ts +57 -0
  102. package/src/runtime/history-tools.ts +62 -12
  103. package/src/runtime/integrations.ts +210 -0
  104. package/src/runtime/loop.ts +1021 -268
  105. package/src/runtime/model-discovery.ts +208 -0
  106. package/src/runtime/native-eval-fixtures.ts +33 -0
  107. package/src/runtime/native-eval.ts +143 -0
  108. package/src/runtime/oauth.ts +1 -1
  109. package/src/runtime/provider-connect.ts +109 -0
  110. package/src/runtime/provider-presets.ts +87 -0
  111. package/src/runtime/query-eval-verifier.ts +28 -0
  112. package/src/runtime/recovery-coverage.ts +71 -0
  113. package/src/runtime/recovery-evidence.ts +76 -0
  114. package/src/runtime/recovery-fixtures.ts +116 -0
  115. package/src/runtime/runtime-target.ts +149 -0
  116. package/src/runtime/tool-broker.ts +137 -0
  117. package/src/runtime/tool-discovery.ts +57 -0
  118. package/src/runtime/tool-mode-eval.ts +146 -0
  119. package/src/runtime/tool-queue.ts +77 -0
  120. package/src/runtime/tools.ts +628 -183
  121. package/src/runtime/trial-control.ts +45 -0
  122. package/src/runtime/update-check.ts +47 -0
  123. package/src/runtime/window-trial.ts +2 -1
  124. package/src/runtime/working-notes.ts +33 -3
  125. package/src/runtime/workspace-lifecycle.ts +97 -0
  126. package/src/store/acceptance.ts +15 -0
  127. package/src/store/checkpoint-history.ts +43 -0
  128. package/src/store/command-artifacts.ts +96 -0
  129. package/src/store/command.ts +65 -37
  130. package/src/store/file-checkpoints.ts +104 -0
  131. package/src/store/file-index.ts +13 -1
  132. package/src/store/git-checkpoints.ts +67 -0
  133. package/src/store/observations.ts +61 -20
  134. package/src/store/process-receipts.ts +34 -0
  135. package/src/store/query-source.ts +13 -8
  136. package/src/store/recovery.ts +2 -3
  137. package/src/store/replay.ts +16 -4
  138. package/src/store/runtime-target.ts +64 -0
  139. package/src/store/search.ts +171 -132
  140. package/src/store/session-archive.ts +4 -0
  141. package/src/store/source-cancellation.ts +6 -0
  142. package/src/store/update-cache.ts +36 -0
  143. package/src/store/workspace-observation.ts +113 -0
  144. package/src/tui/ansi-spans.ts +126 -0
  145. package/src/tui/style.ts +19 -16
  146. package/src/tui/ui/atomic-draft.ts +121 -0
  147. package/src/tui/ui/canvas.ts +96 -0
  148. package/src/tui/ui/colors.ts +70 -0
  149. package/src/tui/ui/composer.ts +199 -0
  150. package/src/tui/ui/dialog.ts +379 -0
  151. package/src/tui/ui/draft.ts +23 -0
  152. package/src/tui/ui/edge.ts +186 -0
  153. package/src/tui/ui/image-size.ts +40 -0
  154. package/src/tui/ui/ink.ts +210 -0
  155. package/src/tui/ui/items.ts +40 -0
  156. package/src/tui/ui/keys.ts +40 -0
  157. package/src/tui/ui/layout.ts +74 -0
  158. package/src/tui/ui/list-model.ts +86 -0
  159. package/src/tui/ui/list-view.ts +172 -0
  160. package/src/tui/ui/mascot.ts +67 -0
  161. package/src/tui/ui/messages.ts +518 -0
  162. package/src/tui/ui/motion.ts +28 -0
  163. package/src/tui/ui/paths.ts +10 -0
  164. package/src/tui/ui/pictures.ts +78 -0
  165. package/src/tui/ui/screen.ts +421 -0
  166. package/src/tui/ui/status.ts +271 -0
  167. package/src/tui/ui/styled.ts +85 -0
  168. package/src/tui/ui/testing.ts +5 -0
  169. package/src/tui/ui/tool-card.ts +150 -0
  170. package/src/apps/extension-ui.ts +0 -330
  171. package/src/apps/resource-browser.ts +0 -75
  172. package/src/apps/session-browser.ts +0 -113
  173. package/src/apps/session-picker.ts +0 -55
  174. package/src/tui/ansi.ts +0 -67
  175. package/src/tui/autocomplete.ts +0 -29
  176. package/src/tui/chat-viewport.ts +0 -47
  177. package/src/tui/complete.ts +0 -87
  178. package/src/tui/editor.ts +0 -940
  179. package/src/tui/host.ts +0 -264
  180. package/src/tui/input.ts +0 -75
  181. package/src/tui/keys.ts +0 -378
  182. package/src/tui/markdown.ts +0 -17
  183. package/src/tui/render.ts +0 -153
  184. package/src/tui/select.ts +0 -258
  185. package/src/tui/settings.ts +0 -44
  186. package/src/tui/statusline.ts +0 -162
  187. package/src/tui/system-theme.ts +0 -700
  188. package/src/tui/terminal.ts +0 -30
  189. package/src/tui/text.ts +0 -52
  190. package/src/tui/toolview.ts +0 -386
  191. package/src/tui/transcript.ts +0 -123
  192. package/src/tui/tree.ts +0 -471
  193. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  194. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  195. package/src/vendor/pi-tui/VENDOR.md +0 -25
  196. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  197. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  198. package/src/vendor/pi-tui/colors.ts +0 -367
  199. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  200. package/src/vendor/pi-tui/components/box.ts +0 -166
  201. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  202. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  203. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  204. package/src/vendor/pi-tui/components/image.ts +0 -127
  205. package/src/vendor/pi-tui/components/input.ts +0 -494
  206. package/src/vendor/pi-tui/components/loader.ts +0 -101
  207. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  208. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  209. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  210. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  211. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  212. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  213. package/src/vendor/pi-tui/components/stack.ts +0 -154
  214. package/src/vendor/pi-tui/components/text.ts +0 -107
  215. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  216. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  217. package/src/vendor/pi-tui/editor-component.ts +0 -74
  218. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  219. package/src/vendor/pi-tui/index.ts +0 -186
  220. package/src/vendor/pi-tui/keybindings.ts +0 -320
  221. package/src/vendor/pi-tui/keys.ts +0 -1401
  222. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  223. package/src/vendor/pi-tui/latex.ts +0 -1506
  224. package/src/vendor/pi-tui/layout-node.ts +0 -51
  225. package/src/vendor/pi-tui/layout.ts +0 -449
  226. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  227. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  228. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  229. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  232. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  233. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  234. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  237. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  238. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  239. package/src/vendor/pi-tui/native/napi.h +0 -91
  240. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  241. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  244. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  245. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  246. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  247. package/src/vendor/pi-tui/native-platform.ts +0 -65
  248. package/src/vendor/pi-tui/oklab.ts +0 -233
  249. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  250. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  251. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  252. package/src/vendor/pi-tui/terminal.ts +0 -554
  253. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  254. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  255. package/src/vendor/pi-tui/tui.ts +0 -1493
  256. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  257. package/src/vendor/pi-tui/utils.ts +0 -1385
  258. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  259. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Semantic version comparison (semver.org §11), without a dependency. Pure.
3
+ *
4
+ * Used to decide whether a published version is newer than the running one
5
+ * (docs/tasks/feat-update-check.md). Anything that does not parse is not a
6
+ * version, and an unparseable version never produces an update notice.
7
+ */
8
+
9
+ export interface Version {
10
+ readonly major: number;
11
+ readonly minor: number;
12
+ readonly patch: number;
13
+ /** Dot-separated pre-release identifiers; empty for a release. */
14
+ readonly prerelease: readonly (number | string)[];
15
+ }
16
+
17
+ const PATTERN = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
18
+
19
+ export function parseVersion(text: string): Version | null {
20
+ const match = PATTERN.exec(text.trim());
21
+ if (!match) return null;
22
+ const numbers = [match[1], match[2], match[3]].map(Number);
23
+ if (numbers.some(n => !Number.isSafeInteger(n))) return null;
24
+ const prerelease = match[4] ? match[4].split(".").map(part => /^\d+$/.test(part) ? Number(part) : part) : [];
25
+ return { major: numbers[0]!, minor: numbers[1]!, patch: numbers[2]!, prerelease };
26
+ }
27
+
28
+ /** -1, 0 or 1, by §11: numbers, then a release outranks its pre-releases, then identifiers left to right. Build metadata is ignored. */
29
+ export function compareVersions(a: Version, b: Version): -1 | 0 | 1 {
30
+ for (const key of ["major", "minor", "patch"] as const) if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
31
+ const x = a.prerelease, y = b.prerelease;
32
+ if (!x.length || !y.length) return x.length === y.length ? 0 : x.length ? -1 : 1;
33
+ for (let i = 0; i < Math.min(x.length, y.length); i++) {
34
+ const p = x[i]!, q = y[i]!;
35
+ if (p === q) continue;
36
+ if (typeof p === "number" && typeof q === "number") return p < q ? -1 : 1;
37
+ // Numeric identifiers have lower precedence than alphanumeric ones.
38
+ if (typeof p === "number") return -1;
39
+ if (typeof q === "number") return 1;
40
+ return p < q ? -1 : 1;
41
+ }
42
+ return x.length === y.length ? 0 : x.length < y.length ? -1 : 1;
43
+ }
44
+
45
+ /**
46
+ * Whether `latest` should be offered to someone running `current`. Never a
47
+ * downgrade, never something unparseable, and a release is not nudged
48
+ * towards a pre-release (a pre-release user may move to the next release).
49
+ */
50
+ export function isUpgrade(latest: string, current: string): boolean {
51
+ const next = parseVersion(latest), now = parseVersion(current);
52
+ if (!next || !now) return false;
53
+ if (next.prerelease.length && !now.prerelease.length) return false;
54
+ return compareVersions(next, now) > 0;
55
+ }
@@ -15,8 +15,35 @@ export type WorkItem = {
15
15
  evidence?: Evidence;
16
16
  /** Reopening keeps provenance without carrying a second copy of old proof. */
17
17
  previous_evidence?: string;
18
+ depends_on?: readonly string[];
19
+ owner?: string;
20
+ owner_event?: string;
18
21
  };
19
22
 
23
+ export type WorkReadiness = { ready: boolean; reason: "ready" | "waiting" | "blocked" | "closed" | "owned" | "missing"; waiting_for: string[] };
24
+ export function workReadiness(items: readonly WorkItem[], id: string, actor?: string): WorkReadiness {
25
+ const item = items.find(work => work.id === id);
26
+ if (!item) return { ready: false, reason: "missing", waiting_for: [id] };
27
+ if (item.status === "completed" || item.status === "cancelled") return { ready: false, reason: "closed", waiting_for: [] };
28
+ const waiting = (item.depends_on ?? []).filter(dep => items.find(work => work.id === dep)?.status !== "completed");
29
+ const reason = item.status === "blocked" ? "blocked" : waiting.length ? "waiting" : item.owner && item.owner !== actor ? "owned" : "ready";
30
+ return { ready: reason === "ready", reason, waiting_for: [...waiting] };
31
+ }
32
+
33
+ function dependencyError(items: readonly WorkItem[], id: string, dependencies: readonly string[]): string | null {
34
+ if (new Set(dependencies).size !== dependencies.length) return "duplicate work dependencies";
35
+ if (dependencies.includes(id)) return "work dependency cycle: self dependency";
36
+ if (dependencies.some(dep => !items.some(item => item.id === dep))) return "unknown work dependency";
37
+ const seen = new Set<string>(), pending = [...dependencies];
38
+ while (pending.length) {
39
+ const dep = pending.pop()!;
40
+ if (dep === id) return "work dependency cycle";
41
+ if (seen.has(dep)) continue;
42
+ seen.add(dep); pending.push(...items.find(item => item.id === dep)?.depends_on ?? []);
43
+ }
44
+ return null;
45
+ }
46
+
20
47
  /** O.2: sibling sessions may choose the same local label without aliasing work. */
21
48
  export function scopedWorkId(namespace: string, requested: string, items: readonly WorkItem[]): string {
22
49
  return items.some(item => item.id === requested) || requested.startsWith(`${namespace}:`)
@@ -26,6 +53,18 @@ export function scopedWorkId(namespace: string, requested: string, items: readon
26
53
  export function workChangeError(items: readonly WorkItem[], event: LogEvent): string | null {
27
54
  if (!("work_id" in event) || event.work_id === undefined) return null;
28
55
  const item = items.find(item => item.id === event.work_id);
56
+ if ((event.t === "next_action" || event.t === "work_dependencies") && event.depends_on !== undefined) {
57
+ const error = dependencyError(items, event.work_id, event.depends_on);
58
+ if (error) return error;
59
+ }
60
+ if (event.t === "work_dependencies" && !item) return `unknown work_id ${event.work_id}`;
61
+ if (event.t === "work_owner") {
62
+ if (!item) return `unknown work_id ${event.work_id}`;
63
+ if (event.operation === "release") return item.owner === event.owner ? null : "only the recorded owner can release work";
64
+ const ready = workReadiness(items, item.id, event.owner);
65
+ return ready.ready ? null : `work ${item.id} is ${ready.reason}; cannot claim it`;
66
+ }
67
+ if (event.t === "claim" && item && (item.depends_on ?? []).some(dep => items.find(work => work.id === dep)?.status !== "completed")) return "work dependencies are not completed";
29
68
  if (event.t === "work_status") {
30
69
  // D15 carries post-fork instructions, not their whole history. A harness-
31
70
  // authenticated transition may seed intent in the adopted branch, never proof.
@@ -51,13 +90,19 @@ export function deriveWorkItems(previous: readonly WorkItem[], events: readonly
51
90
  const items = new Map(previous.map(item => [item.id, { ...item }]));
52
91
  for (let index = 0; index < events.length; index++) {
53
92
  const event = events[index]!;
54
- if (!("work_id" in event) || event.work_id === undefined || workChangeError([...items.values()], event)) continue;
93
+ if ((event.t !== "next_action" && event.t !== "blocked" && event.t !== "work_status" && event.t !== "claim" && event.t !== "work_dependencies" && event.t !== "work_owner") ||
94
+ event.work_id === undefined || workChangeError([...items.values()], event)) continue;
55
95
  const old = items.get(event.work_id);
56
- const item: WorkItem = old ? { ...old, what: event.what, updated_at: event.id } : {
96
+ const item: WorkItem = old ? { ...old, what: event.t === "work_owner" || event.t === "work_dependencies" ? old.what : event.what, updated_at: event.id } : {
57
97
  id: event.work_id, what: event.what, status: "active", verification: "not_run",
58
98
  opened_at: event.id, updated_at: event.id,
59
99
  };
60
- if (event.t === "next_action") item.next_action = event.next_action;
100
+ if (event.t === "next_action") { item.next_action = event.next_action; if (event.depends_on !== undefined) item.depends_on = [...event.depends_on]; }
101
+ else if (event.t === "work_dependencies") item.depends_on = [...event.depends_on];
102
+ else if (event.t === "work_owner") {
103
+ if (event.operation === "claim") { item.owner = event.owner; item.owner_event = event.id; }
104
+ else { delete item.owner; delete item.owner_event; }
105
+ }
61
106
  else if (event.t === "blocked") {
62
107
  item.status = "blocked";
63
108
  item.reason = event.reason;
@@ -89,6 +134,7 @@ export function deriveWorkItems(previous: readonly WorkItem[], events: readonly
89
134
  } else { item.verification = "unverified"; item.reason = resolved.reason; }
90
135
  }
91
136
  items.set(item.id, item);
137
+ if (item.status === "completed" || item.status === "cancelled") { delete item.owner; delete item.owner_event; }
92
138
  }
93
139
  return [...items.values()].map(item => {
94
140
  if (!item.evidence) return item;
@@ -0,0 +1,91 @@
1
+ /** O.10: prioritize explicit intent and references without changing D20 arrays. */
2
+ import type { Handoff } from "./handoff.ts";
3
+ import type { NodeRef } from "./events.ts";
4
+ import { workReadiness, type WorkItem, type WorkReadiness } from "./work-state.ts";
5
+ import { emitYaml } from "./yaml.ts";
6
+ import { estimateTokens } from "./tokens.ts";
7
+ import { RECOVERY_SECTIONS, recoverySources, type RecoveryNodes, type RecoveryRanges, type RecoveryReference } from "./recovery-sources.ts";
8
+ import { recoveryInstructions } from "./prompt-policy.ts";
9
+
10
+ export type WorkingSet = {
11
+ version: 1 | 2; source: NodeRef;
12
+ work: (Pick<WorkItem, "id" | "what" | "status" | "next_action" | "reason" | "depends_on" | "owner"> & { readiness?: WorkReadiness })[];
13
+ references: ({ node?: number; event: string; kind: string; work?: string } & Partial<Omit<RecoveryReference, "node" | "event" | "kind" | "work">>)[];
14
+ omitted: { work: number; references: number };
15
+ recovery_sections?: RecoveryRanges;
16
+ recovery: string;
17
+ };
18
+
19
+ export function workingSet(full: Handoff, budget: number): WorkingSet | null {
20
+ if (!full.predecessor) return null;
21
+ const items = (full.state.work_items ?? []).filter(item => item.status === "blocked" || item.status === "active")
22
+ .toSorted((a, b) => Number(b.status === "blocked") - Number(a.status === "blocked") || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
23
+ const active = new Set(items.map(item => item.id));
24
+ const references: WorkingSet["references"] = [
25
+ ...(full.evidence_links ?? []).filter(link => active.has(link.work) && !link.superseded_by)
26
+ .map(link => ({ event: link.event, kind: "evidence", work: link.work })),
27
+ ...full.decisions.filter(item => item.work_id && active.has(item.work_id) && item.source)
28
+ .map(item => ({ node: item.source!.node, event: item.source!.event_id, kind: "decision", work: item.work_id! })),
29
+ ...full.pitfalls.filter(item => item.work_id && active.has(item.work_id) && item.source)
30
+ .map(item => ({ node: item.source!.node, event: item.source!.event_id, kind: "pitfall", work: item.work_id! })),
31
+ ...(full.progress_evidence ?? []).toReversed().map(item => ({ node: item.source.node, event: item.source.event_id, kind: item.kind })),
32
+ ];
33
+ const view: WorkingSet = { version: 1, source: full.predecessor, work: [], references: [],
34
+ omitted: { work: items.length, references: references.length }, recovery: "history(work_id=ID), history(event=ID), or history(node=source.node, section=...)" };
35
+ const fits = () => budget === Infinity || estimateTokens.count([], emitYaml(view)) <= budget;
36
+ if (!fits()) return null;
37
+ for (const item of items) {
38
+ const { id, what, status, next_action, reason, depends_on, owner } = item;
39
+ view.work.push({ id, what, status, ...(next_action ? { next_action } : {}), ...(reason ? { reason } : {}),
40
+ ...(depends_on ? { depends_on, readiness: workReadiness(full.state.work_items ?? [], id) } : {}), ...(owner ? { owner } : {}) });
41
+ view.omitted.work--;
42
+ if (!fits()) { view.work.pop(); view.omitted.work++; }
43
+ }
44
+ for (const reference of references) {
45
+ view.references.push(reference); view.omitted.references--;
46
+ if (!fits()) { view.references.pop(); view.omitted.references++; }
47
+ }
48
+ return view;
49
+ }
50
+
51
+ /** R9: a new projection version keeps the original v1 ordering byte-stable. */
52
+ export function recoveryWorkingSet(full: Handoff, nodes: RecoveryNodes, budget: number, tools?: readonly string[]): WorkingSet | null {
53
+ const legacy = workingSet(full, Infinity);
54
+ if (!legacy) return null;
55
+ const sources = recoverySources(nodes);
56
+ const view: WorkingSet = { version: 2, source: legacy.source, work: [], references: [],
57
+ omitted: { work: legacy.work.length, references: legacy.references.length + RECOVERY_SECTIONS.reduce((sum, section) => sum + sources[section].length, 0) },
58
+ recovery_sections: Object.fromEntries(RECOVERY_SECTIONS.map(section => [section, { total: sources[section].length, omitted: sources[section].length }])) as RecoveryRanges,
59
+ recovery: tools === undefined ? "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; event=ID reads one original. Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting. history(work_id=ID) recovers work." : recoveryInstructions(tools) };
60
+ const fits = () => estimateTokens.count([], emitYaml(view)) <= budget;
61
+ if (!fits()) return null;
62
+ const queues = RECOVERY_SECTIONS.map(section => ({ section, items: [...sources[section]], at: 0 }));
63
+ // The latest and first live user messages anchor correction and initial scope;
64
+ // remaining user sources are newest first. Cancelled turns stay pageable.
65
+ const live = sources.users.filter(ref => !ref.abandoned), first = live.at(-1);
66
+ queues[0]!.items = [...live.slice(0, 1), ...(first && first !== live[0] ? [first] : []),
67
+ ...live.slice(1, -1), ...sources.users.filter(ref => ref.abandoned)];
68
+ const takeReference = (reference: WorkingSet["references"][number], section?: keyof RecoveryRanges) => {
69
+ view.references.push(reference); view.omitted.references--;
70
+ if (section) view.recovery_sections![section].omitted--;
71
+ if (!fits()) {
72
+ view.references.pop(); view.omitted.references++;
73
+ if (section) view.recovery_sections![section].omitted++;
74
+ }
75
+ };
76
+ // One source per role precedes work prose; round-robin growth thereafter
77
+ // prevents a long task list or repeated failures starving other categories.
78
+ for (const queue of queues) { const reference = queue.items[queue.at++]; if (reference) takeReference(reference, queue.section); }
79
+ let at = 0;
80
+ while (at < legacy.work.length || at < legacy.references.length || queues.some(queue => queue.at < queue.items.length)) {
81
+ const work = legacy.work[at];
82
+ if (work) {
83
+ view.work.push(work); view.omitted.work--;
84
+ if (!fits()) { view.work.pop(); view.omitted.work++; }
85
+ }
86
+ for (const queue of queues) { const reference = queue.items[queue.at++]; if (reference) takeReference(reference, queue.section); }
87
+ const reference = legacy.references[at++];
88
+ if (reference) takeReference(reference);
89
+ }
90
+ return view;
91
+ }
@@ -304,8 +304,8 @@ export function envLabel(provider: string): string {
304
304
  */
305
305
  export function describeMissingCredential(provider: string): string {
306
306
  return (
307
- `no credential for ${provider}. Either export ${envLabel(provider)}, ` +
308
- `or run /login ${provider} to store one in ~/.xeno/auth.json`
307
+ `no credential for ${provider}. Run /login ${provider} to add a key, ` +
308
+ `or export ${envLabel(provider)}`
309
309
  );
310
310
  }
311
311
 
@@ -0,0 +1,36 @@
1
+ /** O.14: volatile ownership, never a replacement for the durable branch log. */
2
+ export class BranchJobs<T> {
3
+ private readonly jobs = new Map<number, { controller: AbortController; done: Promise<void>; value?: T; error?: string; detach(): void; ready: boolean }>();
4
+
5
+ get active(): number { return [...this.jobs.values()].filter(job => !job.ready).length; }
6
+ state(node: number): "running" | "ready" | undefined {
7
+ const job = this.jobs.get(node); return job ? job.ready ? "ready" : "running" : undefined;
8
+ }
9
+ nodes(): number[] { return [...this.jobs.keys()]; }
10
+ start(node: number, controller: AbortController, run: () => Promise<T>, detach: () => void): void {
11
+ if (this.jobs.has(node)) throw new Error("branch already owned");
12
+ const job: { controller: AbortController; done: Promise<void>; value?: T; error?: string; detach(): void; ready: boolean } = {
13
+ controller, ready: false, detach, done: Promise.resolve(),
14
+ };
15
+ this.jobs.set(node, job);
16
+ job.done = run().then(value => { job.value = value; }, () => { job.error = "branch finalization failed; inspect its original log"; })
17
+ .finally(() => { job.ready = true; job.detach(); });
18
+ }
19
+ cancel(node?: number): void {
20
+ for (const [id, job] of this.jobs) if (node === undefined || node === id) job.controller.abort();
21
+ }
22
+ async drain(node?: number): Promise<void> { await Promise.all([...this.jobs].filter(([id]) => node === undefined || node === id).map(([, job]) => job.done)); }
23
+ async wait(node: number, ms: number): Promise<{ value?: T; error?: string; running: boolean } | undefined> {
24
+ const job = this.jobs.get(node); if (!job) return undefined;
25
+ if (!Number.isFinite(ms) || ms < 0 || ms > 60_000) throw new RangeError("wait_ms must be between 0 and 60000");
26
+ let timer: ReturnType<typeof setTimeout> | undefined;
27
+ try {
28
+ if (!job.ready && ms > 0) await Promise.race([job.done, new Promise<void>(resolve => { timer = setTimeout(resolve, ms); })]);
29
+ } finally { clearTimeout(timer); }
30
+ return { ...(job.value !== undefined ? { value: job.value } : {}), ...(job.error ? { error: job.error } : {}), running: !job.ready };
31
+ }
32
+ forget(node: number): void {
33
+ if (this.state(node) === "running") throw new Error("cannot forget a running branch");
34
+ this.jobs.delete(node);
35
+ }
36
+ }
@@ -1,6 +1,17 @@
1
- /** Pi's files → image → text precedence, with native-helper and OS-command fallbacks (MIT, 02eed88). */
1
+ /**
2
+ * Pi's files → image → text precedence (MIT, 02eed88) over the platform's own
3
+ * clipboard commands. The optional native helper went with the vendored TUI
4
+ * (D20); a caller may still inject one.
5
+ */
2
6
  import { fileURLToPath } from "node:url";
3
- import { getNativeClipboard, type NativeClipboard } from "../vendor/pi-tui/native-platform.ts";
7
+
8
+ export interface NativeClipboard {
9
+ setText?(text: string): Promise<void>;
10
+ getText?(): Promise<string | null | undefined>;
11
+ getImage?(): Promise<Uint8Array | null | undefined>;
12
+ getFilePaths?(): Promise<string[] | null | undefined>;
13
+ }
14
+ function getNativeClipboard(): NativeClipboard | null { return null; }
4
15
  import { runClipboardCommand } from "../vendor/pi-rich/clipboard-command.ts";
5
16
 
6
17
  export type ClipboardContent = { files: string[] } | { image: Uint8Array } | { text: string } | null;
@@ -104,9 +115,9 @@ export async function readClipboard(options: ClipboardOptions = {}): Promise<Cli
104
115
  return bytes.length ? { text: bytes.toString("utf8") } : null;
105
116
  }
106
117
  }
107
- const image = await native?.getImage();
118
+ const image = await native?.getImage?.();
108
119
  if (image?.length) return { image };
109
- const text = await native?.getText();
120
+ const text = await native?.getText?.();
110
121
  if (text !== undefined) return text ? { text } : null;
111
122
  const fallback = env["TERMUX_VERSION"] ? await run("termux-clipboard-get", []) : platform === "darwin" ? await run("pbpaste", []) : undefined;
112
123
  if (fallback === undefined) throw new Error("Clipboard unavailable for this terminal session");
@@ -0,0 +1,64 @@
1
+ /** R8: one owner observes an admitted command once, including after a managed call yields. */
2
+ import { hostname } from "node:os";
3
+ import { resolve, join } from "node:path";
4
+ import { artifactChange, commandArtifactState, parseCommandArtifactPlan, COMMAND_ARTIFACT_SCOPE, type CommandArtifactReceipt } from "../core/command-artifacts.ts";
5
+ import { observeArtifactInputs, observeArtifactFiles, artifactExclusions, archiveCommandArtifacts } from "../store/command-artifacts.ts";
6
+ import type { ToolContext } from "./tools.ts";
7
+
8
+ export type ArtifactCommandEnd = { exit: number; stopped?: "cancelled" | "timeout"; capture?: "complete" | "partial"; cleanup?: unknown };
9
+ export type CommandArtifactObserver = {
10
+ pending: CommandArtifactReceipt;
11
+ finish(end: ArtifactCommandEnd): Promise<CommandArtifactReceipt>;
12
+ };
13
+
14
+ /** Syntactic validation precedes admission; no preflight disk probing is smuggled through a denied tool. */
15
+ export function prepareArtifactArguments(input: unknown): unknown {
16
+ if (input === null || typeof input !== "object" || Array.isArray(input) || !("artifacts" in input)) return input;
17
+ return { ...input, artifacts: parseCommandArtifactPlan(input.artifacts) };
18
+ }
19
+
20
+ export async function beginCommandArtifacts(ctx: Pick<ToolContext, "root" | "hostId" | "ownedStatePaths" | "outputStore" | "signal">,
21
+ command: string, cwd: string, requested: unknown): Promise<CommandArtifactObserver | undefined> {
22
+ if (requested === undefined) return undefined;
23
+ const plan = parseCommandArtifactPlan(requested), exclusions = artifactExclusions(ctx.root, plan,
24
+ [...(ctx.ownedStatePaths ?? []), ctx.outputStore ?? join(ctx.root, ".xeno", "observations")]);
25
+ const beforeInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
26
+ const before = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
27
+ const pending: CommandArtifactReceipt = {
28
+ kind: "command_artifacts", version: 1, state: "pending", plan,
29
+ source: { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd), runtime: `bun/${Bun.version}` },
30
+ command, exit: null, started_at: new Date().toISOString(), observed_at: null,
31
+ inputs: { scope: COMMAND_ARTIFACT_SCOPE, exclusions, before: beforeInputs.sha256, after: null },
32
+ outputs: [], reasons: beforeInputs.reason ? [beforeInputs.reason] : [],
33
+ };
34
+ let completed: Promise<CommandArtifactReceipt> | undefined;
35
+ return { pending, finish(end) {
36
+ completed ??= (async () => {
37
+ // Observation failures retain the actual exit. Never retry a post-exit read against later files.
38
+ const reasons = [...pending.reasons];
39
+ if (end.exit !== 0) reasons.push("command exited unsuccessfully");
40
+ if (end.stopped) reasons.push(`command ${end.stopped}`);
41
+ if (end.capture === "partial") reasons.push("command output capture is partial");
42
+ if (end.cleanup) reasons.push("command cleanup is unconfirmed");
43
+ const after = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
44
+ const afterInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
45
+ if (afterInputs.reason) reasons.push(afterInputs.reason);
46
+ const receipt: CommandArtifactReceipt = { ...pending, exit: end.exit, observed_at: new Date().toISOString(),
47
+ inputs: { ...pending.inputs, after: afterInputs.sha256 },
48
+ outputs: plan.paths.map((path, i) => ({ path, before: before[i]!, after: after[i]!, change: artifactChange(before[i]!, after[i]!) })), reasons };
49
+ receipt.state = commandArtifactState(receipt);
50
+ try { receipt.manifest = await archiveCommandArtifacts(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), receipt); }
51
+ catch { receipt.reasons.push("artifact observation archive unavailable"); receipt.state = commandArtifactState(receipt); }
52
+ return receipt;
53
+ })();
54
+ return completed;
55
+ } };
56
+ }
57
+
58
+ export function commandArtifactSummary(receipt: CommandArtifactReceipt | undefined): string {
59
+ if (!receipt) return "";
60
+ const changes = ["created", "modified", "unchanged", "unknown"].map(change => `${change}=${receipt.outputs.filter(output => output.change === change).length}`).join(", ");
61
+ return `\n[artifacts ${receipt.state}; ${changes}; reasons=${receipt.reasons.length}; ` +
62
+ (receipt.manifest ? `history artifact=${receipt.manifest.sha256}; ` : "") +
63
+ "command association only, not build causality or runtime acceptance]";
64
+ }
@@ -0,0 +1,60 @@
1
+ /** R6: observe through the same shell and process owner; never infer versions from the host PATH. */
2
+ import { createHash } from "node:crypto";
3
+ import { hostname } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ENVIRONMENT_OUTPUT_BYTES, ENVIRONMENT_PARSE_BYTES, ENVIRONMENT_TIMEOUT_MS, ENVIRONMENT_TTL_MS,
6
+ environmentCapability, parseEnvironmentFrame, type EnvironmentProbe, type EnvironmentReceipt, type EnvironmentSource } from "../core/environment.ts";
7
+ import { executionOutcome } from "../core/outcome.ts";
8
+ import { canonicalValue } from "../core/continuation.ts";
9
+ import { failureDiagnostic } from "../core/tool-failure.ts";
10
+ import { observeCommand } from "../store/command.ts";
11
+ import type { ToolContext, ToolResult } from "./tools.ts";
12
+
13
+ const hash = (text: string) => createHash("sha256").update(text).digest("hex");
14
+ /** Hash only resolution-related inherited inputs; never expose the full environment or its credentials. */
15
+ export function environmentSource(ctx: Pick<ToolContext, "root" | "hostId">, cwd: string): EnvironmentSource {
16
+ const keys = ["PATH", "HOME", "SHELL", "LANG", "BASH_ENV", "PYTHONHOME", "PYTHONPATH", "PYENV_VERSION", "VIRTUAL_ENV", "CONDA_PREFIX", "NVM_BIN"];
17
+ return { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd),
18
+ runtime: `bun/${Bun.version};${process.platform}/${process.arch};${process.execPath}`,
19
+ inherited_sha256: hash(canonicalValue(Object.fromEntries(keys.map(key => [key, process.env[key] ?? null])))) };
20
+ }
21
+
22
+ export async function observeEnvironment(ctx: ToolContext, cwd: string, program: string, probe: EnvironmentProbe, command: string): Promise<ToolResult> {
23
+ const budget = new AbortController();
24
+ const signal = ctx.signal ? AbortSignal.any([ctx.signal, budget.signal]) : budget.signal;
25
+ let outputBytes = 0;
26
+ const source = environmentSource(ctx, cwd);
27
+ const captured = await observeCommand({ command, cwd, cleanupOnExit: true, outputLimitBytes: ENVIRONMENT_OUTPUT_BYTES,
28
+ outputStore: ctx.outputStore ?? join(ctx.root, ".xeno", "observations"),
29
+ maxOutputBytes: ENVIRONMENT_PARSE_BYTES, timeoutMs: Math.min(ENVIRONMENT_TIMEOUT_MS, ctx.timeoutMs, ctx.maxTimeoutMs ?? Infinity), signal,
30
+ onOutput: chunk => {
31
+ outputBytes += chunk.byteLength;
32
+ if (outputBytes > ENVIRONMENT_OUTPUT_BYTES) budget.abort();
33
+ ctx.onOutput?.(chunk);
34
+ },
35
+ });
36
+ const outputLimited = budget.signal.aborted;
37
+ const parseLimited = captured.ref.bytes > ENVIRONMENT_PARSE_BYTES;
38
+ const frame = parseEnvironmentFrame(new TextDecoder().decode(captured.head), program, probe);
39
+ const capability = captured.cleanup ? { state: "unknown" as const, reported_version: null, reason: captured.cleanup.message } : environmentCapability(frame, captured.exit, { complete: captured.capture === "complete",
40
+ ...(captured.stopped ? { stopped: captured.stopped } : {}),
41
+ ...(outputLimited ? { limited: "output" } : parseLimited ? { limited: "parse" } : {}) });
42
+ const observedAt = Date.now();
43
+ const receipt: EnvironmentReceipt = { kind: "environment", version: 1, query: { program, probe }, source,
44
+ observed_at: observedAt, expires_at: observedAt + ENVIRONMENT_TTL_MS, ...capability,
45
+ shell: frame ? { executable: frame.shell, version: frame.shell_version, cwd: frame.cwd, path_sha256: hash(frame.path) } : null,
46
+ resolution: frame ? { kind: frame.kind, executable: frame.executable } : null,
47
+ evidence: { output: captured.ref, exit: captured.exit, complete: captured.capture === "complete" } };
48
+ const status = outputLimited ? "tool_failed" : captured.stopped ?? (capability.state === "observed" ? "tool_completed" : "tool_failed");
49
+ const code = outputLimited ? "execution_failed" : captured.stopped ?? (capability.state === "missing" ? "environment_missing" : "execution_failed");
50
+ const text = JSON.stringify(receipt);
51
+ const bytes = new TextEncoder().encode(text);
52
+ // Raw output is the primary artifact. Structured data is separately preserved by the Session broker.
53
+ const content = bytes.length <= ctx.maxOutputBytes ? text :
54
+ `environment ${receipt.state}; exit ${captured.exit}; full receipt in data; original output: ${captured.ref.sha256}`;
55
+ return { ok: capability.state === "observed", content, content_ref: captured.ref, data: receipt,
56
+ ...(captured.cleanup ? { warnings: [captured.cleanup] } : {}),
57
+ observation_sha256: hash(canonicalValue({ query: receipt.query, source, ...capability, shell: receipt.shell, resolution: receipt.resolution, evidence: receipt.evidence })),
58
+ effect: { kind: "command", cmd: command, exit: captured.exit }, outcome: executionOutcome(status),
59
+ ...(capability.state === "observed" ? {} : { diagnostic: failureDiagnostic(code, capability.reason ?? "capability unconfirmed", { execution: "started", effects: "possible" }) }) };
60
+ }
@@ -5,14 +5,16 @@ import { historyEntries } from "../core/history-query.ts";
5
5
  import { observedVersion, resourceKey } from "../core/evidence-links.ts";
6
6
  import { scopedWorkId } from "../core/work-state.ts";
7
7
  import { readNodeHistory } from "../store/restore.ts";
8
+ import { failureDiagnostic, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
8
9
  import type { ToolResult } from "./tools.ts";
9
10
 
10
11
  export async function resolveEvidenceNote(logDir: string, node: number, namespace: string, current: Handoff, event: EvidenceLinkEvent): Promise<{ event?: EvidenceLinkEvent; result: ToolResult }> {
11
12
  const work = scopedWorkId(namespace, event.work, current.state.work_items ?? []);
12
- const fail = (message: string) => ({ result: { ok: false, content: message } });
13
+ const fail = (message: string, attempted = false) => ({ result: { ok: false, content: message, data: { recorded: false },
14
+ diagnostic: failureDiagnostic(attempted ? "execution_failed" : "invalid_input", message, attempted ? READ_ATTEMPTED : NOT_STARTED) } });
13
15
  if (!current.state.work_items?.some(item => item.id === work)) return fail("pin requires an existing work item");
14
16
  const history = await readNodeHistory(logDir, node);
15
- if (!history.ok) return fail(history.error);
17
+ if (!history.ok) return fail(history.error, true);
16
18
  const entries = historyEntries(history.nodes);
17
19
  const source = entries.find(entry => entry.event === event.source && entry.kind === "tool_result");
18
20
  if (!source) return fail("pin source must be an original tool observation in the active ancestry");