@visual-z/xeno 0.2.1 → 0.4.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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,3 +1,6 @@
1
+ import type { ArtifactRef, LogEvent } from "./events.ts";
2
+ import { isObservationArtifact } from "./observation-view.ts";
3
+
1
4
  /**
2
5
  * Facts about the machine, fixed at session start.
3
6
  *
@@ -27,3 +30,145 @@ export function environmentSection(env: SessionEnvironment): string {
27
30
  `- Git repository: ${env.git ? "yes" : "no"}`,
28
31
  ].join("\n");
29
32
  }
33
+
34
+ /** R6: these are operational assumptions, not measured freshness guarantees. */
35
+ export const ENVIRONMENT_TTL_MS = 5 * 60_000;
36
+ export const ENVIRONMENT_TIMEOUT_MS = 5_000;
37
+ export const ENVIRONMENT_OUTPUT_BYTES = 256 * 1024;
38
+ export const ENVIRONMENT_PARSE_BYTES = 64 * 1024;
39
+ /** Version reports are metadata; verbose command output remains in the original artifact. */
40
+ export const ENVIRONMENT_VERSION_BYTES = 4096;
41
+ export type EnvironmentProbe = "version" | "python";
42
+ export type EnvironmentSource = { host: string; workspace: string; cwd: string; runtime: string; inherited_sha256: string };
43
+ export type PythonCapability = { executable: string; prefix: string; base_prefix: string; version: string;
44
+ implementation: string; isolated: true; features: { tarfile_extractall_filter: boolean } };
45
+ export type EnvironmentReceipt = {
46
+ kind: "environment"; version: 1;
47
+ query: { program: string; probe: EnvironmentProbe };
48
+ source: EnvironmentSource;
49
+ observed_at: number; expires_at: number;
50
+ state: "observed" | "missing" | "opaque" | "failed" | "unknown";
51
+ shell: { executable: string; version: string; cwd: string; path_sha256: string } | null;
52
+ resolution: { kind: string; executable: string | null } | null;
53
+ reported_version: string | null;
54
+ python?: PythonCapability;
55
+ evidence: { output: ArtifactRef; exit: number; complete: boolean };
56
+ reason?: string;
57
+ };
58
+
59
+ const quote = (text: string) => "'" + text.replaceAll("'", "'\\''") + "'";
60
+ const marker = "\0XENO_ENVIRONMENT_V1\0";
61
+ const PYTHON_CAPABILITY_PROBE = [
62
+ "import inspect,json,platform,sys,tarfile",
63
+ "print(json.dumps({'executable':sys.executable,'prefix':sys.prefix,'base_prefix':sys.base_prefix,'version':platform.python_version(),'implementation':platform.python_implementation(),'isolated':bool(sys.flags.isolated),'features':{'tarfile_extractall_filter':'filter' in inspect.signature(tarfile.TarFile.extractall).parameters}},sort_keys=True))",
64
+ ].join(";");
65
+
66
+ /** Program names are data, including paths with quotes/newlines; no caller code is evaluated. */
67
+ export function environmentProbeCommand(program: unknown, probe: unknown = "version"): string {
68
+ if (typeof program !== "string" || !program || program.includes("\0") || new TextEncoder().encode(program).length > 4096) throw new Error("program must be a nonempty name or path up to 4096 bytes without NUL");
69
+ if (probe !== "version" && probe !== "python") throw new Error("probe must be version or python");
70
+ return [
71
+ "builtin set +e", `xeno_probe_program=${quote(program)}`,
72
+ 'xeno_probe_kind=$(builtin type -t -- "$xeno_probe_program" 2>/dev/null)',
73
+ 'xeno_probe_path=""',
74
+ 'if [ "$xeno_probe_kind" = file ]; then xeno_probe_path=$(builtin type -P -- "$xeno_probe_program" 2>/dev/null); fi',
75
+ // A single frame precedes the program output, which remains untrusted data.
76
+ `builtin printf '\\0XENO_ENVIRONMENT_V1\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0' "$xeno_probe_program" ${quote(probe)} "$xeno_probe_kind" "$xeno_probe_path" "$BASH" "$BASH_VERSION" "$PWD" "$PATH"`,
77
+ 'if [ -z "$xeno_probe_kind" ]; then builtin exit 127; fi',
78
+ 'if [ "$xeno_probe_kind" != file ] || [ -z "$xeno_probe_path" ]; then builtin exit 125; fi',
79
+ // Replace the discovery shell: the managed group leader and exit belong
80
+ // to the probed program, not an extra shell waiting for its child (R1).
81
+ probe === "python" ? `builtin exec -- "$xeno_probe_path" -I -c ${quote(PYTHON_CAPABILITY_PROBE)}` : 'builtin exec -- "$xeno_probe_path" --version',
82
+ ].join("\n");
83
+ }
84
+
85
+ export interface EnvironmentFrame { program: string; probe: EnvironmentProbe; kind: string; executable: string | null;
86
+ shell: string; shell_version: string; cwd: string; path: string; output: string }
87
+ export function parseEnvironmentFrame(text: string, program: string, probe: EnvironmentProbe): EnvironmentFrame | null {
88
+ const start = text.indexOf(marker);
89
+ if (start < 0) return null;
90
+ const fields: string[] = [];
91
+ let at = start + marker.length;
92
+ for (let i = 0; i < 8; i++) {
93
+ const end = text.indexOf("\0", at);
94
+ if (end < 0) return null;
95
+ fields.push(text.slice(at, end)); at = end + 1;
96
+ }
97
+ if (fields[0] !== program || fields[1] !== probe || !["", "file", "function", "alias", "builtin", "keyword"].includes(fields[2]!) ||
98
+ !fields[4] || !fields[5] || !fields[6] || (fields[2] === "file" && !fields[3])) return null;
99
+ return { program, probe, kind: fields[2]!, executable: fields[3] || null, shell: fields[4]!, shell_version: fields[5]!, cwd: fields[6]!, path: fields[7]!, output: text.slice(at) };
100
+ }
101
+
102
+ const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
103
+ const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
104
+ const strings = (value: Record<string, unknown>, keys: readonly string[]) => keys.every(key => typeof value[key] === "string" && value[key] !== "");
105
+ export function isPythonCapability(value: unknown): value is PythonCapability {
106
+ return object(value) && strings(value, ["executable", "prefix", "base_prefix", "version", "implementation"]) && value["isolated"] === true &&
107
+ object(value["features"]) && typeof value["features"]["tarfile_extractall_filter"] === "boolean";
108
+ }
109
+
110
+ /** D11: neither a version-looking line nor an interrupted exit zero proves a capability. */
111
+ export function environmentCapability(frame: EnvironmentFrame | null, exit: number, capture: {
112
+ complete: boolean; stopped?: "cancelled" | "timeout"; limited?: "output" | "parse";
113
+ }): Pick<EnvironmentReceipt, "state" | "reported_version" | "python" | "reason"> {
114
+ const unknown = (reason: string) => ({ state: "unknown" as const, reported_version: null, reason });
115
+ if (capture.limited) return unknown(`probe exceeded its ${capture.limited} budget; inspect the original output`);
116
+ if (capture.stopped) return unknown(`probe ${capture.stopped}; inspect partial effects before continuing`);
117
+ if (!capture.complete || !frame) return unknown("probe output is incomplete or its environment frame is invalid");
118
+ if (frame.kind === "" && exit === 127) return { state: "missing", reported_version: null, reason: "program was not found in the command environment" };
119
+ if (frame.kind !== "file") return { state: "opaque", reported_version: null, reason: "shell resolution is not an executable file; specify its exact path" };
120
+ if (exit !== 0) return { state: "failed", reported_version: null, reason: "program exited unsuccessfully; a version-like line is not capability evidence" };
121
+ const output = frame.output.trim();
122
+ if (!output || new TextEncoder().encode(output).length > ENVIRONMENT_VERSION_BYTES) return unknown("version report is empty or exceeds the metadata budget");
123
+ if (frame.probe === "version") return { state: "observed", reported_version: output };
124
+ try {
125
+ const value: unknown = JSON.parse(output);
126
+ if (isPythonCapability(value)) return { state: "observed", reported_version: value.version, python: value };
127
+ } catch { /* The complete untrusted report remains available in the artifact. */ }
128
+ return unknown("Python did not return a valid isolated capability report");
129
+ }
130
+ export function isEnvironmentReceipt(value: unknown): value is EnvironmentReceipt {
131
+ if (!object(value) || value["kind"] !== "environment" || value["version"] !== 1 || !object(value["query"]) ||
132
+ !strings(value["query"], ["program"]) || !["version", "python"].includes(String(value["query"]["probe"]))) return false;
133
+ const source = value["source"], evidence = value["evidence"], shell = value["shell"], resolution = value["resolution"];
134
+ if (!object(source) || !strings(source, ["host", "workspace", "cwd", "runtime"]) || !digest(source["inherited_sha256"]) ||
135
+ !object(evidence) || !isObservationArtifact(evidence["output"]) || !Number.isInteger(evidence["exit"]) || typeof evidence["complete"] !== "boolean") return false;
136
+ const observed = value["observed_at"], expires = value["expires_at"];
137
+ if (typeof observed !== "number" || !Number.isSafeInteger(observed) || observed < 0 || expires !== observed + ENVIRONMENT_TTL_MS ||
138
+ !["observed", "missing", "opaque", "failed", "unknown"].includes(String(value["state"])) ||
139
+ !(value["reported_version"] === null || typeof value["reported_version"] === "string") ||
140
+ !(value["reason"] === undefined || typeof value["reason"] === "string")) return false;
141
+ if (shell !== null && (!object(shell) || !strings(shell, ["executable", "version", "cwd"]) || !digest(shell["path_sha256"]))) return false;
142
+ if (resolution !== null && (!object(resolution) || typeof resolution["kind"] !== "string" ||
143
+ !(resolution["executable"] === null || typeof resolution["executable"] === "string"))) return false;
144
+ if (value["python"] !== undefined && !isPythonCapability(value["python"])) return false;
145
+ if (value["state"] === "observed" && (evidence["exit"] !== 0 || !evidence["complete"] || !object(shell) || !object(resolution) ||
146
+ resolution["kind"] !== "file" || !resolution["executable"] || !value["reported_version"] ||
147
+ value["query"]["probe"] === "python" && value["python"] === undefined)) return false;
148
+ return true;
149
+ }
150
+
151
+ export type EnvironmentValidity = { state: "recorded" | "expired" | "scope_changed" | "invalidated" | "unconfirmed";
152
+ reason: string; event?: string };
153
+ /** Recomputed on consumption: recent observations are never guarantees about a future invocation. */
154
+ export function environmentValidity(receipt: EnvironmentReceipt, source: EnvironmentSource, now: number, after: readonly LogEvent[]): EnvironmentValidity {
155
+ for (const event of after) {
156
+ if (event.t === "user_bash" || event.t === "snapshot" &&
157
+ (Object.keys(event.added).length > 0 || Object.keys(event.changed).length > 0 || event.removed.length > 0)) {
158
+ return { state: "invalidated", reason: "later command or workspace change requires environment revalidation", event: event.id };
159
+ }
160
+ if (event.t !== "tool_result") continue;
161
+ if (isEnvironmentReceipt(event.data) && event.data.query.program === receipt.query.program && event.data.query.probe === receipt.query.probe) {
162
+ return { state: "invalidated", reason: "a later probe supersedes this observation, including a failed probe", event: event.id };
163
+ }
164
+ if (event.effect?.kind === "edit" || event.effect?.kind === "command" || event.diagnostic?.code === "environment_missing" ||
165
+ event.diagnostic?.effects === "possible" && event.diagnostic.execution !== "not_started") {
166
+ return { state: "invalidated", reason: "later execution or mutation requires environment revalidation", event: event.id };
167
+ }
168
+ }
169
+ const keys: readonly (keyof EnvironmentSource)[] = ["host", "workspace", "cwd", "runtime", "inherited_sha256"];
170
+ if (keys.some(key => source[key] !== receipt.source[key])) return { state: "scope_changed", reason: "host, workspace, directory, runtime, or inherited environment changed" };
171
+ if (!Number.isFinite(now) || now < receipt.observed_at || now >= receipt.expires_at) return { state: "expired", reason: "observation time is outside its validity interval" };
172
+ if (receipt.state !== "observed") return { state: "unconfirmed", reason: "the probe did not establish this capability" };
173
+ return { state: "recorded", reason: "recent observation only; login startup files or external changes may require another probe" };
174
+ }
@@ -0,0 +1,74 @@
1
+ /** R12c: missing final rows cannot erase attempted work or measured charges. */
2
+ import type { EvalJob, EvalResult } from "./experiment.ts";
3
+ import { recoveryCase } from "./recovery-corpus.ts";
4
+ import { TOOL_MODE_FACTORS, isToolModeTask, isToolModeMetrics } from "./tool-mode-protocol.ts";
5
+
6
+ export interface BillingRecord {
7
+ kind: "reserved" | "settled"; request: number; inputSha256: string;
8
+ reservation: number; charge: number; measured: number | null; ledgerCharged: number;
9
+ }
10
+ export function requestAccounting(records: readonly BillingRecord[], before: number) {
11
+ const nonnegative = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n >= 0;
12
+ if (!nonnegative(before)) throw Error("invalid trial ledger start");
13
+ let charged = before, pending: BillingRecord | undefined, requests = 0, known = 0, unknown = 0;
14
+ for (const row of records) {
15
+ if (![row.reservation, row.charge, row.ledgerCharged].every(nonnegative) || row.measured !== null && !nonnegative(row.measured) ||
16
+ !Number.isSafeInteger(row.request) || row.request < 1 || !/^[a-f0-9]{64}$/.test(row.inputSha256)) throw Error("invalid billing record");
17
+ if (row.kind === "reserved") {
18
+ if (pending || row.request !== ++requests || row.measured !== null || row.charge !== row.reservation) throw Error("invalid reservation order");
19
+ pending = row; charged += row.reservation;
20
+ } else if (row.kind === "settled") {
21
+ if (!pending || row.request !== pending.request || row.inputSha256 !== pending.inputSha256 || row.reservation !== pending.reservation ||
22
+ row.charge !== (row.measured ?? row.reservation)) throw Error("invalid settlement identity");
23
+ charged += row.charge - pending.reservation;
24
+ if (row.measured === null) unknown += row.reservation; else known += row.measured;
25
+ pending = undefined;
26
+ } else throw Error("invalid billing record kind");
27
+ if (row.ledgerCharged !== charged) throw Error("billing ledger sequence changed");
28
+ }
29
+ return { requests, charged, knownCostSubtotal: known, unsettledRequests: pending ? 1 : 0,
30
+ unsettledReservation: pending?.reservation ?? 0, unknownSettlementReserve: unknown,
31
+ fullyMeasured: !pending && records.filter(row => row.kind === "settled").every(row => row.measured !== null) };
32
+ }
33
+ export type RequestAccounting = ReturnType<typeof requestAccounting>;
34
+ export type TrialCoverage = { id: string; accounting: RequestAccounting; tail: "complete" | "truncated";
35
+ result?: EvalResult; interruption: "deadline" | "caller" | null; partialMetrics?: EvalResult["recovery"] };
36
+
37
+ export function summarizeRecoveryCoverage(jobs: readonly EvalJob[], trials: readonly TrialCoverage[], mechanical: boolean) {
38
+ const ids = new Set(jobs.map(job => job.id)), byId = new Map(trials.map(trial => [trial.id, trial]));
39
+ if (ids.size !== jobs.length || byId.size !== trials.length || trials.some(trial => !ids.has(trial.id) || trial.result && trial.result.id !== trial.id)) throw Error("invalid recovery coverage identities");
40
+ const group = (selected: readonly EvalJob[]) => {
41
+ const rows = selected.flatMap(job => byId.has(job.id) ? [byId.get(job.id)!] : []), finalized = rows.filter(row => row.result);
42
+ const passed = finalized.filter(row => row.result!.scenarioPassed).length;
43
+ const unknown = rows.filter(row => !row.result || row.tail !== "complete" || !row.accounting.fullyMeasured).length;
44
+ const known = rows.reduce((n, row) => n + row.accounting.knownCostSubtotal, 0);
45
+ const cost = !mechanical && rows.length === selected.length && unknown === 0 ? known : null;
46
+ const metrics = rows.flatMap(row => row.result?.recovery ?? row.partialMetrics ?? []);
47
+ const modes = finalized.flatMap(row => row.result?.toolMode ?? []);
48
+ if (modes.some(mode => !isToolModeMetrics(mode))) throw Error("invalid tool mode measurements");
49
+ const toolMode = selected.some(job => isToolModeTask(job.task)) ? { observed: modes.length, missing: selected.length - modes.length,
50
+ queryCalls: modes.reduce((n, mode) => n + mode.queryCalls, 0),
51
+ primitiveCalls: modes.reduce((n, mode) => n + mode.primitiveCalls, 0), sourcesComplete: modes.filter(mode => mode.sourceComplete).length,
52
+ meanElapsedMs: modes.length === selected.length && modes.length > 0 ? modes.reduce((n, mode) => n + mode.elapsedMs, 0) / modes.length : null } : undefined;
53
+ return { ...(toolMode ? { toolMode } : {}), planned: selected.length, attempted: rows.length, finalized: finalized.length, incomplete: rows.length - finalized.length,
54
+ unattempted: selected.length - rows.length, failed: finalized.length - passed, passed, plannedPassFraction: selected.length ? passed / selected.length : null,
55
+ cancelled: rows.filter(row => row.interruption === "caller").length, timedOut: rows.filter(row => row.interruption === "deadline").length,
56
+ requests: rows.reduce((n, row) => n + row.accounting.requests, 0), truncatedRequestLogs: rows.filter(row => row.tail === "truncated").length,
57
+ knownCostSubtotal: mechanical ? null : known, costUsd: cost,
58
+ unsettledRequests: rows.reduce((n, row) => n + row.accounting.unsettledRequests, 0),
59
+ unsettledReservationUsd: mechanical ? null : rows.reduce((n, row) => n + row.accounting.unsettledReservation, 0),
60
+ unknownSettlementReserveUsd: mechanical ? null : rows.reduce((n, row) => n + row.accounting.unknownSettlementReserve, 0),
61
+ unknownCostTrials: mechanical ? rows.length : unknown, costPerScenarioSuccess: cost !== null && passed > 0 ? cost / passed : null,
62
+ unverifiedEndings: finalized.filter(row => row.result!.unverifiedEnding).length,
63
+ recovery: { observed: metrics.length, missing: selected.length - metrics.length,
64
+ firstToolSuccess: metrics.filter(row => row.firstToolSuccess === true).length,
65
+ firstToolUnknown: metrics.filter(row => row.firstToolSuccess === null).length,
66
+ firstPassTaskSuccess: metrics.filter(row => row.firstPassTaskSuccess === true).length,
67
+ ...Object.fromEntries((["toolCalls", "outerCalls", "nestedCalls", "unresolvedCalls", "toolFailures", "classifiedFailures", "unclassifiedFailures", "recoveryCalls", "unchangedFailureRetries", "originalEvidenceRetrievals", "unsupportedClaims"] as const)
68
+ .map(key => [key, metrics.reduce((n, row) => n + row[key], 0)])) } };
69
+ };
70
+ const toolModes = jobs.some(job => isToolModeTask(job.task));
71
+ return { ...group(jobs), arms: (toolModes ? TOOL_MODE_FACTORS : ["profile", "workingSet", "strictVerification"]).flatMap(factor => ["control", "treatment"].map(arm =>
72
+ ({ factor, arm, ...group(jobs.filter(job => job.factor === factor && job.arm === arm)) }))),
73
+ partitions: toolModes ? [] : ["development", "holdout"].map(partition => ({ partition, ...group(jobs.filter(job => recoveryCase(job.task)?.partition === partition)) })) };
74
+ }
@@ -3,6 +3,14 @@ import type { LogEvent, NodeRef } from "./events.ts";
3
3
  import { isExecutionOutcome } from "./outcome.ts";
4
4
  import { isJsonValue, isToolDiagnostic } from "./tool-contract.ts";
5
5
  import { validImageRef } from "./images.ts";
6
+ import { parseAcceptanceContract } from "./acceptance.ts";
7
+ import { validFileChanges } from "./checkpoints.ts";
8
+ import { isObservationReceipt } from "./read-observation.ts";
9
+ import { isSearchReceipt } from "./search-observation.ts";
10
+ import { isProcessReceipt } from "./process-receipt.ts";
11
+ import { isObservationArtifact } from "./observation-view.ts";
12
+ import { isCommandArtifactReceipt } from "./command-artifacts.ts";
13
+ import { isRuntimeTargetReceipt } from "./runtime-target.ts";
6
14
 
7
15
  export function isRecord(value: unknown): value is Record<string, unknown> {
8
16
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -11,6 +19,7 @@ const strings = (value: unknown): boolean => Array.isArray(value) && value.every
11
19
  const stringMap = (value: unknown): boolean => isRecord(value) && Object.values(value).every((v) => typeof v === "string");
12
20
  const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
13
21
  const natural = (value: unknown): boolean => Number.isSafeInteger(value) && Number(value) >= 0;
22
+ const eventRef = (value: unknown): boolean => isRecord(value) && natural(value["node"]) && typeof value["event"] === "string" && value["event"] !== "";
14
23
  export function validNodeRef(value: unknown): value is NodeRef {
15
24
  return isRecord(value) && Number.isSafeInteger(value["node"]) && Number(value["node"]) >= 0 &&
16
25
  typeof value["log_ref"] === "string" && typeof value["sha256"] === "string" &&
@@ -27,8 +36,39 @@ export function isLogEvent(value: unknown): value is LogEvent {
27
36
  const images = value["images"];
28
37
  if (images !== undefined && (!Array.isArray(images) || !images.every(validImageRef))) return false;
29
38
  switch (value["t"]) {
39
+ case "tool_invocation": return has("call_id", "name") && value["call_id"] !== "" && value["name"] !== "" &&
40
+ (value["origin"] === undefined || value["origin"] === "extension") &&
41
+ isObservationArtifact(value["input_ref"]) && (value["schema_ref"] === null || isObservationArtifact(value["schema_ref"])) &&
42
+ (value["input"] === undefined || isJsonValue(value["input"])) && optionalString(value["parent_call_id"]) &&
43
+ (value["parent_ref"] === undefined ? value["parent_call_id"] === undefined : eventRef(value["parent_ref"]) && value["parent_call_id"] !== undefined) &&
44
+ value["parent_call_id"] !== "" && value["parent_call_id"] !== value["call_id"];
45
+ case "checkpoint": return has("operation") && ["edit", "git", "restore"].includes(String(value["source"])) &&
46
+ ["prepared", "applied", "unavailable"].includes(String(value["phase"])) && validFileChanges(value["changes"]) &&
47
+ optionalString(value["tree"]) && optionalString(value["reason"]);
48
+ case "workspace_observation": return value["scope"] === "source-v1" && typeof value["changed"] === "boolean" && optionalString(value["reason"]) &&
49
+ (value["fingerprint"] === undefined || typeof value["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(value["fingerprint"]));
50
+ case "process_exit": {
51
+ const output = value["output"];
52
+ const receipt = value["receipt"];
53
+ if (receipt !== undefined && (!isProcessReceipt(receipt) || receipt.replayed || receipt.running !== false ||
54
+ receipt.session_id !== value["session"] || receipt.exit !== value["exit"] || !isRecord(output) ||
55
+ receipt.artifacts !== undefined && receipt.artifacts.command !== value["command"] ||
56
+ receipt.output === undefined || receipt.output.sha256 !== output["sha256"] || receipt.output.bytes !== output["bytes"])) return false;
57
+ return has("command") && natural(value["session"]) && Number(value["session"]) > 0 && Number.isInteger(value["exit"]) &&
58
+ value["reason"] === "disposed" && isRecord(output) && typeof output["path"] === "string" &&
59
+ typeof output["sha256"] === "string" && /^[a-f0-9]{64}$/.test(output["sha256"]) && natural(output["bytes"]);
60
+ }
61
+ case "skill_loaded": return has("name", "path", "sha256", "artifact") &&
62
+ ["sha256", "artifact"].every(key => /^[a-f0-9]{64}$/.test(String(value[key]))) &&
63
+ ["inline", "fork", "read"].includes(String(value["mode"])) && ["user", "model"].includes(String(value["origin"]));
64
+ case "acceptance_contract": {
65
+ try { parseAcceptanceContract(value["contract"]); return has("request"); } catch { return false; }
66
+ }
67
+ case "acceptance_decision": return has("request", "contract_id", "criterion") && value["actor"] === "user" &&
68
+ (value["target_binding"] === undefined ? value["target_observation"] === undefined : has("target_binding", "target_observation"));
69
+ case "target_observation": return isRuntimeTargetReceipt(value["observation"]);
30
70
  case "extension": return has("customType", "content") && typeof value["display"] === "boolean" && typeof value["inContext"] === "boolean" && (value["data"] === undefined || isJsonValue(value["data"]));
31
- case "user": return has("text");
71
+ case "user": return has("text") && (value["request"] === undefined || typeof value["request"] === "string" && value["request"] !== "");
32
72
  case "outcome": return has("reason") && isExecutionOutcome(value["outcome"]);
33
73
  case "user_bash": {
34
74
  const ref = value["content_ref"];
@@ -40,7 +80,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
40
80
  isRecord(ref) && typeof ref["sha256"] === "string" &&
41
81
  typeof ref["path"] === "string" && natural(ref["bytes"]);
42
82
  }
43
- case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline"].includes(String(value["reason"])) && strings(value["sources"]);
83
+ case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline", "runtime_acceptance"].includes(String(value["reason"])) && strings(value["sources"]);
44
84
  case "interrupt": return has("user_id");
45
85
  case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) &&
46
86
  (value["replay_key"] === undefined || typeof value["replay_key"] === "string" && /^[a-f0-9]{64}$/.test(value["replay_key"])) &&
@@ -51,8 +91,34 @@ export function isLogEvent(value: unknown): value is LogEvent {
51
91
  return b["type"] === "tool_use" && typeof b["call_id"] === "string" && typeof b["name"] === "string" && "input" in b;
52
92
  });
53
93
  case "tool_result": {
94
+ if (!optionalString(value["parent_call_id"]) || value["parent_call_id"] === "" || value["parent_call_id"] === value["call_id"]) return false;
95
+ if (value["origin"] !== undefined && value["origin"] !== "extension") return false;
96
+ if (value["invocation_ref"] !== undefined && !eventRef(value["invocation_ref"])) return false;
97
+ const observation = value["observation"];
98
+ if (observation !== undefined && (!isObservationReceipt(observation) ||
99
+ !isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== observation.artifact.sha256 ||
100
+ value["content_ref"]["bytes"] !== observation.artifact.bytes || value["content_sha256"] !== observation.artifact.sha256)) return false;
54
101
  if (value["data"] !== undefined && !isJsonValue(value["data"])) return false;
102
+ if (value["data_ref"] !== undefined && (!isObservationArtifact(value["data_ref"]) || value["data"] !== undefined)) return false;
103
+ if (isRecord(value["data"]) && value["data"]["kind"] === "process") {
104
+ const process = value["data"];
105
+ if (!isProcessReceipt(process)) return false;
106
+ if (process.output && (!isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== process.output.sha256 ||
107
+ value["content_ref"]["bytes"] !== process.output.bytes || value["content_sha256"] !== process.output.sha256)) return false;
108
+ if (process.replayed && value["effect"] !== undefined) return false;
109
+ if (value["effect"] !== undefined && (!isRecord(value["effect"]) || value["effect"]["kind"] !== "command" ||
110
+ process.running !== false || value["effect"]["exit"] !== process.exit ||
111
+ JSON.stringify(value["effect"]["artifacts"]) !== JSON.stringify(process.artifacts))) return false;
112
+ }
113
+ if (isObservationReceipt(observation) && observation.descriptor !== undefined &&
114
+ (!isRecord(value["data"]) || value["data"]["read_handle"] !== observation.descriptor.sha256)) return false;
115
+ if (isRecord(value["data"]) && value["data"]["kind"] === "search") {
116
+ const search = value["data"];
117
+ if (!isSearchReceipt(search) || !isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== search.artifact.sha256 ||
118
+ value["content_ref"]["bytes"] !== search.artifact.bytes || value["content_sha256"] !== search.artifact.sha256) return false;
119
+ }
55
120
  if (value["diagnostic"] !== undefined && !isToolDiagnostic(value["diagnostic"])) return false;
121
+ if (value["warnings"] !== undefined && (!Array.isArray(value["warnings"]) || !value["warnings"].every(isToolDiagnostic))) return false;
56
122
  if (!has("call_id", "content") || typeof value["ok"] !== "boolean") return false;
57
123
  if (!optionalString(value["content_sha256"])) return false;
58
124
  if (!optionalString(value["observation_sha256"])) return false;
@@ -63,14 +129,19 @@ export function isLogEvent(value: unknown): value is LogEvent {
63
129
  if (effect === undefined) return true;
64
130
  if (!isRecord(effect)) return false;
65
131
  if (effect["kind"] === "command") return typeof effect["cmd"] === "string" && Number.isInteger(effect["exit"]) &&
66
- optionalString(effect["gate"]) && optionalString(effect["workspace_sha256"]);
132
+ optionalString(effect["gate"]) && optionalString(effect["workspace_sha256"]) &&
133
+ (effect["artifacts"] === undefined || isCommandArtifactReceipt(effect["artifacts"]) &&
134
+ effect["artifacts"].exit === effect["exit"] && effect["artifacts"].command === effect["cmd"]) &&
135
+ (effect["workspace_scope"] === undefined || effect["workspace_scope"] === "source-v1") &&
136
+ (effect["workspace_observed"] === undefined || typeof effect["workspace_observed"] === "boolean");
67
137
  if (effect["kind"] === "read") return typeof effect["path"] === "string" && typeof effect["sha256"] === "string" &&
68
138
  (effect["ranges"] === undefined || (Array.isArray(effect["ranges"]) && effect["ranges"].every((r: unknown) =>
69
139
  Array.isArray(r) && r.length === 2 && r.every((n) => Number.isSafeInteger(n)) && r[0] >= 1 && r[1] >= r[0])));
70
140
  return effect["kind"] === "edit" && typeof effect["path"] === "string" && typeof effect["sha256_after"] === "string" &&
71
141
  (effect["sha256_before"] === null || typeof effect["sha256_before"] === "string");
72
142
  }
73
- case "cut": return has("handoff_ref") && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
143
+ case "cut": return has("handoff_ref") && (value["working_set_version"] === undefined || value["working_set_version"] === 1 || value["working_set_version"] === 2) && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
144
+ (value["guidance_tools"] === undefined || Array.isArray(value["guidance_tools"]) && value["guidance_tools"].every(item => typeof item === "string")) &&
74
145
  optionalString(value["retained_from"]) && optionalString(value["next_context_ref"]) &&
75
146
  (value["next_node"] === undefined || natural(value["next_node"])) && optionalString(value["next_model"]) &&
76
147
  (value["handoff_tokens"] === undefined || (typeof value["handoff_tokens"] === "number" && Number.isFinite(value["handoff_tokens"]) && value["handoff_tokens"] > 0)) &&
@@ -83,7 +154,9 @@ export function isLogEvent(value: unknown): value is LogEvent {
83
154
  case "pitfall": return has("tried", "failed_because");
84
155
  case "blocked": return has("what", "reason");
85
156
  case "question": return has("text");
86
- case "next_action": return has("what", "next_action");
157
+ case "next_action": return has("what", "next_action") && (value["depends_on"] === undefined || strings(value["depends_on"]));
158
+ case "work_dependencies": return has("work_id", "what") && strings(value["depends_on"]);
159
+ case "work_owner": return has("work_id", "what", "owner") && ["claim", "release"].includes(String(value["operation"]));
87
160
  case "evidence_link": return has("work", "what", "source") && optionalString(value["resource"]) && optionalString(value["version"]) &&
88
161
  optionalString(value["supersedes"]) && (value["observed_version"] === null || optionalString(value["observed_version"]));
89
162
  case "work_status": return has("work_id", "what") && ["unblock", "reopen", "cancel"].includes(String(value["operation"])) &&
@@ -93,6 +166,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
93
166
  strings(value["undeclared"]) && strings(value["declared_but_unchanged"]) && optionalString(value["workspace_sha256"]) &&
94
167
  (value["phase"] === undefined || value["phase"] === "turn" || value["phase"] === "idle");
95
168
  case "rewind": return ["both", "files", "context"].includes(String(value["mode"])) && validNodeRef(value["from"]) &&
169
+ (value["files_from"] === undefined || validNodeRef(value["files_from"])) &&
96
170
  has("prompt") && strings(value["restored"]) && strings(value["removed"]) && strings(value["unrecoverable"]) && strings(value["divergence"]);
97
171
  case "spawn": {
98
172
  const input = value["input"];
@@ -103,10 +177,11 @@ export function isLogEvent(value: unknown): value is LogEvent {
103
177
  if (!(workspace["kind"] === "shared_readonly" || (workspace["kind"] === "git_tree" &&
104
178
  typeof workspace["head"] === "string" && typeof workspace["tree"] === "string"))) return false;
105
179
  }
106
- return validNodeRef(value["branch"]) && has("purpose") && (value["workspace"] === undefined ||
180
+ return (value["background"] === undefined || value["background"] === true) && validNodeRef(value["branch"]) && has("purpose") && (value["workspace"] === undefined ||
107
181
  (isRecord(value["workspace"]) && typeof value["workspace"]["path"] === "string" && typeof value["workspace"]["branch"] === "string"));
108
182
  }
109
183
  case "joined": return validNodeRef(value["branch"]) && has("handoff_ref", "summary") && optionalString(value["model"]) &&
184
+ (value["receipt_ref"] === undefined || typeof value["receipt_ref"] === "string" && /^[a-f0-9]{64}$/.test(value["receipt_ref"])) &&
110
185
  (value["tip"] === undefined || validNodeRef(value["tip"]));
111
186
  case "adopt": return validNodeRef(value["branch"]) && (value["next_node"] === undefined || natural(value["next_node"])) &&
112
187
  (value["applied"] === undefined || value["applied"] === true);
@@ -21,6 +21,11 @@
21
21
  import type { ExecutionOutcome } from "./outcome.ts";
22
22
  import type { ImageRef } from "./images.ts";
23
23
  import type { JsonValue, ToolDiagnostic } from "./tool-contract.ts";
24
+ import type { AcceptanceContract } from "./acceptance.ts";
25
+ import type { CheckpointEvent, WorkspaceObservationEvent } from "./checkpoints.ts";
26
+ import type { ObservationReceipt } from "./read-observation.ts";
27
+ import type { CommandArtifactReceipt } from "./command-artifacts.ts";
28
+ import type { RuntimeTargetReceipt } from "./runtime-target.ts";
24
29
 
25
30
  export type EventId = string;
26
31
  export type Sha256 = string;
@@ -68,7 +73,7 @@ export type NodeRef = {
68
73
  export type Effect =
69
74
  | { kind: "read"; path: string; sha256: Sha256; ranges?: readonly LineRange[] }
70
75
  | { kind: "edit"; path: string; sha256_before: Sha256 | null; sha256_after: Sha256 }
71
- | { kind: "command"; cmd: string; exit: number; gate?: string; workspace_sha256?: Sha256 };
76
+ | { kind: "command"; cmd: string; exit: number; gate?: string; workspace_sha256?: Sha256; workspace_observed?: boolean; workspace_scope?: "source-v1"; artifacts?: CommandArtifactReceipt };
72
77
 
73
78
  // ---------------------------------------------------------------------------
74
79
  // Harness-derived events
@@ -79,6 +84,8 @@ export interface UserEvent {
79
84
  id: EventId;
80
85
  at: IsoTime;
81
86
  text: string;
87
+ /** Q23: in-flight steering belongs to this original user event; legacy events open a request. */
88
+ request?: EventId;
82
89
  images?: readonly ImageRef[];
83
90
  }
84
91
 
@@ -109,7 +116,7 @@ export interface GuidanceEvent {
109
116
  t: "guidance";
110
117
  id: EventId;
111
118
  at: IsoTime;
112
- reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline";
119
+ reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline" | "runtime_acceptance";
113
120
  text: string;
114
121
  sources: readonly EventId[];
115
122
  }
@@ -167,6 +174,11 @@ export interface ToolResultEvent {
167
174
  id: EventId;
168
175
  at: IsoTime;
169
176
  call_id: string;
177
+ /** Trusted host execution has no provider tool_use and is excluded from provider replay. */
178
+ origin?: "extension";
179
+ /** Pi A: nested observations remain evidence without creating provider-visible tool pairs. */
180
+ parent_call_id?: string;
181
+ invocation_ref?: { node: number; event: EventId };
170
182
  ok: boolean;
171
183
  /** What the model sees. Large payloads belong on disk, referenced by effect. */
172
184
  content: string;
@@ -175,17 +187,36 @@ export interface ToolResultEvent {
175
187
  content_sha256?: Sha256;
176
188
  /** O.3: semantic observation digest excludes presentation metadata. */
177
189
  observation_sha256?: Sha256;
190
+ /** R2: exact preview coverage and immutable source identity; old logs omit this receipt. */
191
+ observation?: ObservationReceipt;
178
192
  content_ref?: ArtifactRef;
179
193
  effect?: Effect;
180
194
  /** O.1: actual termination, independent of effect/verification; legacy logs omit it. */
181
195
  outcome?: ExecutionOutcome;
182
196
  /** O.3: machine-readable observations and recovery hints, not additional proof. */
183
197
  data?: JsonValue;
198
+ /** Pi A: large structured results stay recoverable without bloating each JSONL event. */
199
+ data_ref?: ArtifactRef;
184
200
  diagnostic?: ToolDiagnostic;
201
+ /** Post-operation observer failures cannot erase the operation's factual result. */
202
+ warnings?: readonly ToolDiagnostic[];
185
203
  }
186
204
 
187
205
  export type ArtifactRef = { sha256: Sha256; path: string; bytes: number };
188
206
 
207
+ /** Pi A: an invocation is durable before its admitted action may start. */
208
+ export interface ToolInvocationEvent {
209
+ t: "tool_invocation"; id: EventId; at: IsoTime;
210
+ call_id: string; name: string; parent_call_id?: string;
211
+ origin?: "extension";
212
+ parent_ref?: { node: number; event: EventId };
213
+ /** Original request and the exact declarative contract are independently recoverable. */
214
+ input_ref: ArtifactRef;
215
+ schema_ref: ArtifactRef | null;
216
+ /** Bounded actual arguments; absent for a direct unchanged request or an oversized refusal. */
217
+ input?: JsonValue;
218
+ }
219
+
189
220
  /** Terminates the segment it appears in. Carries refs only -- see rule 1 above. */
190
221
  export interface CutEvent {
191
222
  t: "cut";
@@ -205,6 +236,10 @@ export interface CutEvent {
205
236
  next_context_ref?: Sha256;
206
237
  /** V2 carried-state budget, fixed in the log for identical replay. */
207
238
  handoff_tokens?: number;
239
+ /** O.10: absent preserves the original D20 carried projection. */
240
+ working_set_version?: 1 | 2;
241
+ /** R10: declared builtin advice capabilities at this cut, not an authorization grant. Absent replays old text. */
242
+ guidance_tools?: readonly string[];
208
243
  /** N.8: first retained event on the effective ancestry; no copied transcript. */
209
244
  retained_from?: EventId;
210
245
  }
@@ -242,6 +277,7 @@ export interface DecisionEvent {
242
277
  chose: string;
243
278
  because: string;
244
279
  rejected?: readonly string[];
280
+ work_id?: string;
245
281
  /** Set when an adopt moved this event from another node. */
246
282
  carried_from?: CarriedFrom;
247
283
  }
@@ -252,6 +288,7 @@ export interface PitfallEvent {
252
288
  at: IsoTime;
253
289
  tried: string;
254
290
  failed_because: string;
291
+ work_id?: string;
255
292
  /** Set when an adopt moved this event from another node. */
256
293
  carried_from?: CarriedFrom;
257
294
  }
@@ -340,6 +377,8 @@ export interface RewindEvent {
340
377
  id: EventId;
341
378
  at: IsoTime;
342
379
  mode: RewindMode;
380
+ /** P.3: context navigation keeps the file history at this separate cursor. */
381
+ files_from?: NodeRef;
343
382
  /** Where things were moved back to. */
344
383
  from: NodeRef;
345
384
  /** The prompt that opened the exchange moved back to, for a human reader. */
@@ -368,6 +407,8 @@ export interface SpawnEvent {
368
407
  branch: NodeRef;
369
408
  /** What the branch is for, in one line. */
370
409
  purpose: string;
410
+ /** O.14: execution mode is durable; it never proves a live handle exists. */
411
+ background?: true;
371
412
  /** The isolated checkout belongs to this branch, including after restart. */
372
413
  workspace?: { path: string; branch: string };
373
414
  /** O.4: a task input envelope references the original request, never copies history. */
@@ -393,6 +434,8 @@ export interface JoinedEvent {
393
434
  branch: NodeRef;
394
435
  handoff_ref: string;
395
436
  summary: string;
437
+ /** O.9: artifact contains a deterministic evidence index, never raw history. */
438
+ receipt_ref?: Sha256;
396
439
  /** A branch can cut before joining; its last node need not be its root. */
397
440
  tip?: NodeRef;
398
441
  model?: string;
@@ -445,6 +488,7 @@ export interface NextActionEvent {
445
488
  what: string;
446
489
  work_id?: string;
447
490
  next_action: string;
491
+ depends_on?: readonly string[];
448
492
  /** Set when an adopt moved this event from another node. */
449
493
  carried_from?: CarriedFrom;
450
494
  }
@@ -462,6 +506,15 @@ export interface WorkStatusEvent {
462
506
  carried_from?: CarriedFrom;
463
507
  }
464
508
 
509
+ export interface WorkDependenciesEvent {
510
+ t: "work_dependencies"; id: EventId; at: IsoTime; work_id: string; what: string;
511
+ depends_on: readonly string[]; carried_from?: CarriedFrom;
512
+ }
513
+ export interface WorkOwnerEvent {
514
+ t: "work_owner"; id: EventId; at: IsoTime; work_id: string; what: string;
515
+ operation: "claim" | "release"; owner: string; carried_from?: CarriedFrom;
516
+ }
517
+
465
518
  /** O.6: a model associates an original observation with work; the runtime resolves its source. */
466
519
  export interface EvidenceLinkEvent {
467
520
  t: "evidence_link"; id: EventId; at: IsoTime;
@@ -479,7 +532,38 @@ export interface ExtensionEvent {
479
532
  data?: import("./tool-contract.ts").JsonValue;
480
533
  }
481
534
 
535
+ export interface AcceptanceContractEvent {
536
+ t: "acceptance_contract"; id: EventId; at: IsoTime; request: EventId;
537
+ contract: AcceptanceContract;
538
+ }
539
+ export interface AcceptanceDecisionEvent {
540
+ t: "acceptance_decision"; id: EventId; at: IsoTime; request: EventId;
541
+ contract_id: string; criterion: string; actor: "user";
542
+ target_binding?: string; target_observation?: EventId;
543
+ }
544
+ export interface TargetObservationEvent {
545
+ t: "target_observation"; id: EventId; at: IsoTime; observation: RuntimeTargetReceipt;
546
+ }
547
+
548
+ export interface SkillLoadedEvent {
549
+ t: "skill_loaded"; id: EventId; at: IsoTime; name: string; path: string;
550
+ sha256: Sha256; artifact: Sha256; mode: "inline" | "fork" | "read"; origin: "user" | "model";
551
+ }
552
+
553
+ /** O.14: owner cleanup is an actual process observation, not a model claim. */
554
+ export interface ProcessExitEvent {
555
+ t: "process_exit"; id: EventId; at: IsoTime; session: number;
556
+ command: string; exit: number; output: ArtifactRef; reason: "disposed";
557
+ receipt?: import("./process-receipt.ts").ProcessReceipt;
558
+ }
559
+
482
560
  export type LogEvent =
561
+ | ToolInvocationEvent
562
+ | ProcessExitEvent
563
+ | SkillLoadedEvent
564
+ | AcceptanceContractEvent
565
+ | AcceptanceDecisionEvent
566
+ | TargetObservationEvent
483
567
  | ExtensionEvent
484
568
  | UserEvent
485
569
  | UserBashEvent
@@ -491,6 +575,8 @@ export type LogEvent =
491
575
  | EvidenceLinkEvent
492
576
  | CutEvent
493
577
  | SnapshotEvent
578
+ | CheckpointEvent
579
+ | WorkspaceObservationEvent
494
580
  | RewindEvent
495
581
  | SpawnEvent
496
582
  | JoinedEvent
@@ -502,12 +588,21 @@ export type LogEvent =
502
588
  | BlockedEvent
503
589
  | QuestionEvent
504
590
  | NextActionEvent
505
- | WorkStatusEvent;
591
+ | WorkStatusEvent
592
+ | WorkDependenciesEvent
593
+ | WorkOwnerEvent;
594
+ // Work coordination is intent, never a substitute for command proof.
506
595
 
507
596
  export const HARNESS_EVENTS = [
597
+ "checkpoint", "workspace_observation",
598
+ "target_observation",
599
+ "process_exit",
600
+ "skill_loaded",
601
+ "acceptance_contract", "acceptance_decision",
508
602
  "user", "user_bash", "outcome", "guidance", "interrupt", "assistant", "tool_result", "cut", "snapshot", "rewind", "extension",
509
603
  ] as const;
510
604
  export const MODEL_EVENTS = [
605
+ "work_dependencies", "work_owner",
511
606
  "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
512
607
  "spawn", "joined", "adopt", "discard",
513
608
  ] as const;
@@ -519,6 +614,7 @@ export const MODEL_EVENTS = [
519
614
  * ancestry or are parent actions the branch should not inherit.
520
615
  */
521
616
  export const CARRYABLE_EVENTS = [
617
+ "work_dependencies", "work_owner",
522
618
  "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
523
619
  ] as const;
524
620