@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
@@ -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,6 +36,37 @@ 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
71
  case "user": return has("text");
32
72
  case "outcome": return has("reason") && isExecutionOutcome(value["outcome"]);
@@ -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
@@ -109,7 +114,7 @@ export interface GuidanceEvent {
109
114
  t: "guidance";
110
115
  id: EventId;
111
116
  at: IsoTime;
112
- reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline";
117
+ reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline" | "runtime_acceptance";
113
118
  text: string;
114
119
  sources: readonly EventId[];
115
120
  }
@@ -167,6 +172,11 @@ export interface ToolResultEvent {
167
172
  id: EventId;
168
173
  at: IsoTime;
169
174
  call_id: string;
175
+ /** Trusted host execution has no provider tool_use and is excluded from provider replay. */
176
+ origin?: "extension";
177
+ /** Pi A: nested observations remain evidence without creating provider-visible tool pairs. */
178
+ parent_call_id?: string;
179
+ invocation_ref?: { node: number; event: EventId };
170
180
  ok: boolean;
171
181
  /** What the model sees. Large payloads belong on disk, referenced by effect. */
172
182
  content: string;
@@ -175,17 +185,36 @@ export interface ToolResultEvent {
175
185
  content_sha256?: Sha256;
176
186
  /** O.3: semantic observation digest excludes presentation metadata. */
177
187
  observation_sha256?: Sha256;
188
+ /** R2: exact preview coverage and immutable source identity; old logs omit this receipt. */
189
+ observation?: ObservationReceipt;
178
190
  content_ref?: ArtifactRef;
179
191
  effect?: Effect;
180
192
  /** O.1: actual termination, independent of effect/verification; legacy logs omit it. */
181
193
  outcome?: ExecutionOutcome;
182
194
  /** O.3: machine-readable observations and recovery hints, not additional proof. */
183
195
  data?: JsonValue;
196
+ /** Pi A: large structured results stay recoverable without bloating each JSONL event. */
197
+ data_ref?: ArtifactRef;
184
198
  diagnostic?: ToolDiagnostic;
199
+ /** Post-operation observer failures cannot erase the operation's factual result. */
200
+ warnings?: readonly ToolDiagnostic[];
185
201
  }
186
202
 
187
203
  export type ArtifactRef = { sha256: Sha256; path: string; bytes: number };
188
204
 
205
+ /** Pi A: an invocation is durable before its admitted action may start. */
206
+ export interface ToolInvocationEvent {
207
+ t: "tool_invocation"; id: EventId; at: IsoTime;
208
+ call_id: string; name: string; parent_call_id?: string;
209
+ origin?: "extension";
210
+ parent_ref?: { node: number; event: EventId };
211
+ /** Original request and the exact declarative contract are independently recoverable. */
212
+ input_ref: ArtifactRef;
213
+ schema_ref: ArtifactRef | null;
214
+ /** Bounded actual arguments; absent for a direct unchanged request or an oversized refusal. */
215
+ input?: JsonValue;
216
+ }
217
+
189
218
  /** Terminates the segment it appears in. Carries refs only -- see rule 1 above. */
190
219
  export interface CutEvent {
191
220
  t: "cut";
@@ -205,6 +234,10 @@ export interface CutEvent {
205
234
  next_context_ref?: Sha256;
206
235
  /** V2 carried-state budget, fixed in the log for identical replay. */
207
236
  handoff_tokens?: number;
237
+ /** O.10: absent preserves the original D20 carried projection. */
238
+ working_set_version?: 1 | 2;
239
+ /** R10: declared builtin advice capabilities at this cut, not an authorization grant. Absent replays old text. */
240
+ guidance_tools?: readonly string[];
208
241
  /** N.8: first retained event on the effective ancestry; no copied transcript. */
209
242
  retained_from?: EventId;
210
243
  }
@@ -242,6 +275,7 @@ export interface DecisionEvent {
242
275
  chose: string;
243
276
  because: string;
244
277
  rejected?: readonly string[];
278
+ work_id?: string;
245
279
  /** Set when an adopt moved this event from another node. */
246
280
  carried_from?: CarriedFrom;
247
281
  }
@@ -252,6 +286,7 @@ export interface PitfallEvent {
252
286
  at: IsoTime;
253
287
  tried: string;
254
288
  failed_because: string;
289
+ work_id?: string;
255
290
  /** Set when an adopt moved this event from another node. */
256
291
  carried_from?: CarriedFrom;
257
292
  }
@@ -340,6 +375,8 @@ export interface RewindEvent {
340
375
  id: EventId;
341
376
  at: IsoTime;
342
377
  mode: RewindMode;
378
+ /** P.3: context navigation keeps the file history at this separate cursor. */
379
+ files_from?: NodeRef;
343
380
  /** Where things were moved back to. */
344
381
  from: NodeRef;
345
382
  /** The prompt that opened the exchange moved back to, for a human reader. */
@@ -368,6 +405,8 @@ export interface SpawnEvent {
368
405
  branch: NodeRef;
369
406
  /** What the branch is for, in one line. */
370
407
  purpose: string;
408
+ /** O.14: execution mode is durable; it never proves a live handle exists. */
409
+ background?: true;
371
410
  /** The isolated checkout belongs to this branch, including after restart. */
372
411
  workspace?: { path: string; branch: string };
373
412
  /** O.4: a task input envelope references the original request, never copies history. */
@@ -393,6 +432,8 @@ export interface JoinedEvent {
393
432
  branch: NodeRef;
394
433
  handoff_ref: string;
395
434
  summary: string;
435
+ /** O.9: artifact contains a deterministic evidence index, never raw history. */
436
+ receipt_ref?: Sha256;
396
437
  /** A branch can cut before joining; its last node need not be its root. */
397
438
  tip?: NodeRef;
398
439
  model?: string;
@@ -445,6 +486,7 @@ export interface NextActionEvent {
445
486
  what: string;
446
487
  work_id?: string;
447
488
  next_action: string;
489
+ depends_on?: readonly string[];
448
490
  /** Set when an adopt moved this event from another node. */
449
491
  carried_from?: CarriedFrom;
450
492
  }
@@ -462,6 +504,15 @@ export interface WorkStatusEvent {
462
504
  carried_from?: CarriedFrom;
463
505
  }
464
506
 
507
+ export interface WorkDependenciesEvent {
508
+ t: "work_dependencies"; id: EventId; at: IsoTime; work_id: string; what: string;
509
+ depends_on: readonly string[]; carried_from?: CarriedFrom;
510
+ }
511
+ export interface WorkOwnerEvent {
512
+ t: "work_owner"; id: EventId; at: IsoTime; work_id: string; what: string;
513
+ operation: "claim" | "release"; owner: string; carried_from?: CarriedFrom;
514
+ }
515
+
465
516
  /** O.6: a model associates an original observation with work; the runtime resolves its source. */
466
517
  export interface EvidenceLinkEvent {
467
518
  t: "evidence_link"; id: EventId; at: IsoTime;
@@ -479,7 +530,38 @@ export interface ExtensionEvent {
479
530
  data?: import("./tool-contract.ts").JsonValue;
480
531
  }
481
532
 
533
+ export interface AcceptanceContractEvent {
534
+ t: "acceptance_contract"; id: EventId; at: IsoTime; request: EventId;
535
+ contract: AcceptanceContract;
536
+ }
537
+ export interface AcceptanceDecisionEvent {
538
+ t: "acceptance_decision"; id: EventId; at: IsoTime; request: EventId;
539
+ contract_id: string; criterion: string; actor: "user";
540
+ target_binding?: string; target_observation?: EventId;
541
+ }
542
+ export interface TargetObservationEvent {
543
+ t: "target_observation"; id: EventId; at: IsoTime; observation: RuntimeTargetReceipt;
544
+ }
545
+
546
+ export interface SkillLoadedEvent {
547
+ t: "skill_loaded"; id: EventId; at: IsoTime; name: string; path: string;
548
+ sha256: Sha256; artifact: Sha256; mode: "inline" | "fork" | "read"; origin: "user" | "model";
549
+ }
550
+
551
+ /** O.14: owner cleanup is an actual process observation, not a model claim. */
552
+ export interface ProcessExitEvent {
553
+ t: "process_exit"; id: EventId; at: IsoTime; session: number;
554
+ command: string; exit: number; output: ArtifactRef; reason: "disposed";
555
+ receipt?: import("./process-receipt.ts").ProcessReceipt;
556
+ }
557
+
482
558
  export type LogEvent =
559
+ | ToolInvocationEvent
560
+ | ProcessExitEvent
561
+ | SkillLoadedEvent
562
+ | AcceptanceContractEvent
563
+ | AcceptanceDecisionEvent
564
+ | TargetObservationEvent
483
565
  | ExtensionEvent
484
566
  | UserEvent
485
567
  | UserBashEvent
@@ -491,6 +573,8 @@ export type LogEvent =
491
573
  | EvidenceLinkEvent
492
574
  | CutEvent
493
575
  | SnapshotEvent
576
+ | CheckpointEvent
577
+ | WorkspaceObservationEvent
494
578
  | RewindEvent
495
579
  | SpawnEvent
496
580
  | JoinedEvent
@@ -502,12 +586,21 @@ export type LogEvent =
502
586
  | BlockedEvent
503
587
  | QuestionEvent
504
588
  | NextActionEvent
505
- | WorkStatusEvent;
589
+ | WorkStatusEvent
590
+ | WorkDependenciesEvent
591
+ | WorkOwnerEvent;
592
+ // Work coordination is intent, never a substitute for command proof.
506
593
 
507
594
  export const HARNESS_EVENTS = [
595
+ "checkpoint", "workspace_observation",
596
+ "target_observation",
597
+ "process_exit",
598
+ "skill_loaded",
599
+ "acceptance_contract", "acceptance_decision",
508
600
  "user", "user_bash", "outcome", "guidance", "interrupt", "assistant", "tool_result", "cut", "snapshot", "rewind", "extension",
509
601
  ] as const;
510
602
  export const MODEL_EVENTS = [
603
+ "work_dependencies", "work_owner",
511
604
  "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
512
605
  "spawn", "joined", "adopt", "discard",
513
606
  ] as const;
@@ -519,6 +612,7 @@ export const MODEL_EVENTS = [
519
612
  * ancestry or are parent actions the branch should not inherit.
520
613
  */
521
614
  export const CARRYABLE_EVENTS = [
615
+ "work_dependencies", "work_owner",
522
616
  "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
523
617
  ] as const;
524
618
 
@@ -36,6 +36,9 @@ export function observedVersion(events: readonly LogEvent[], resource: string):
36
36
  if (event.removed.map(resourceKey).includes(resource)) version = null;
37
37
  for (const [path, value] of Object.entries({ ...event.added, ...event.changed })) if (resourceKey(path) === resource) version = value;
38
38
  }
39
+ if (event.t === "checkpoint" && event.phase === "applied") for (const [path, change] of Object.entries(event.changes)) {
40
+ if (resourceKey(path) === resource) version = change.after?.sha256 ?? null;
41
+ }
39
42
  }
40
43
  return version;
41
44
  }
@@ -65,6 +68,10 @@ export function deriveEvidenceLinks(previous: readonly EvidenceLink[], events: r
65
68
  if (resourceKey(path) === link.resource) link.freshness = version === link.version ? "current" : "stale";
66
69
  }
67
70
  }
71
+ if (event.t === "checkpoint" && event.phase === "applied") for (const [path, change] of Object.entries(event.changes)) {
72
+ if (resourceKey(path) === link.resource) link.freshness = change.after?.sha256 === link.version ? "current" : "stale";
73
+ }
74
+ if (event.t === "workspace_observation" && event.changed || event.t === "checkpoint" && event.phase === "unavailable") link.freshness = "unknown";
68
75
  }
69
76
  }
70
77
  return links;
@@ -1,6 +1,7 @@
1
1
  /** Shared executed-evidence rules for handoff derivation and the turn watchdog. */
2
2
  import type { EvidenceQuery, LogEvent, ToolResultEvent } from "./events.ts";
3
3
  import type { Evidence } from "./handoff.ts";
4
+ import { changedPaths } from "./checkpoints.ts";
4
5
 
5
6
  type CommandEffect = Extract<NonNullable<ToolResultEvent["effect"]>, { kind: "command" }>;
6
7
  export type CommandResult = ToolResultEvent & { effect: CommandEffect };
@@ -15,9 +16,14 @@ export function evidenceStale(events: readonly LogEvent[], evidence: Evidence, i
15
16
  let stale = initially;
16
17
  for (const event of events.slice(start + 1)) {
17
18
  if (event.t === "tool_result" && event.effect?.kind === "edit") stale = true;
18
- const sha = event.t === "snapshot" ? event.workspace_sha256
19
+ if (event.t === "checkpoint" && (event.phase === "unavailable" || event.phase === "applied" && changedPaths(event.changes).length > 0)) stale = true;
20
+ if (event.t === "workspace_observation" && event.fingerprint === undefined ||
21
+ event.t === "tool_result" && event.effect?.kind === "command" && event.effect.workspace_observed === false || event.t === "user_bash") stale = true;
22
+ const sha = event.t === "workspace_observation" ? event.fingerprint : event.t === "snapshot" ? event.workspace_sha256
19
23
  : event.t === "tool_result" && event.effect?.kind === "command" ? event.effect.workspace_sha256 : undefined;
20
- if (sha !== undefined && evidence.workspace_sha256 !== undefined) stale = sha !== evidence.workspace_sha256;
24
+ const scope = event.t === "workspace_observation" ? event.scope : event.t === "tool_result" && event.effect?.kind === "command" ? event.effect.workspace_scope : undefined;
25
+ if (sha !== undefined && evidence.workspace_sha256 !== undefined) stale = evidence.workspace_scope === "source-v1"
26
+ ? stale || scope !== evidence.workspace_scope || sha !== evidence.workspace_sha256 : scope !== undefined || sha !== evidence.workspace_sha256;
21
27
  else if (event.t === "snapshot" && (Object.keys(event.added).length + Object.keys(event.changed).length + event.removed.length > 0)) stale = true;
22
28
  }
23
29
  return stale;
@@ -33,7 +39,9 @@ export function resolveEvidence(events: readonly LogEvent[], query: EvidenceQuer
33
39
  const result = events.slice(start).findLast(event => isCommandResult(event) && matchesQuery(query, event.effect));
34
40
  if (!result || !isCommandResult(result)) return { ok: false, reason: "no_matching_event" };
35
41
  if (result.effect.exit !== 0) return { ok: false, reason: "nonzero_exit", exit: result.effect.exit };
42
+ if (result.effect.workspace_observed === false) return { ok: false, reason: "stale_workspace" };
36
43
  const evidence: Evidence = { command: result.effect.cmd, exit: result.effect.exit, at: result.at, event_id: result.id,
44
+ ...(result.effect.workspace_scope ? { workspace_scope: result.effect.workspace_scope } : {}),
37
45
  ...(result.effect.gate !== undefined ? { gate: result.effect.gate } : {}),
38
46
  ...(result.effect.workspace_sha256 ? { workspace_sha256: result.effect.workspace_sha256 } : {}) };
39
47
  return evidenceStale(events, evidence) ? { ok: false, reason: "stale_workspace" } : { ok: true, evidence };
@@ -44,8 +52,9 @@ export function eligibleProgressChecks(events: readonly LogEvent[], claimed: Rea
44
52
  for (const event of events) {
45
53
  if (!isCommandResult(event)) continue;
46
54
  const gate = event.effect.gate;
47
- if (event.effect.exit !== 0 || (gate === undefined && !claimed.has(event.id)) || (gate !== undefined && failingGates.has(gate))) continue;
55
+ if (event.effect.exit !== 0 || event.effect.workspace_observed === false || (gate === undefined && !claimed.has(event.id)) || (gate !== undefined && failingGates.has(gate))) continue;
48
56
  if (!evidenceStale(events, { command: event.effect.cmd, exit: 0, at: event.at, event_id: event.id,
57
+ ...(event.effect.workspace_scope ? { workspace_scope: event.effect.workspace_scope } : {}),
49
58
  ...(event.effect.workspace_sha256 === undefined ? {} : { workspace_sha256: event.effect.workspace_sha256 }) })) eligible.add(event.id);
50
59
  }
51
60
  return eligible;
@@ -74,6 +83,7 @@ export function classifyGate(gates: Readonly<Record<string, string>>, command: s
74
83
  if (tokens.some((t) => informational.includes(t))) return undefined;
75
84
  let i = 0;
76
85
  const first = tokens[0];
86
+ if (["bun", "npm", "pnpm", "yarn"].includes(first ?? "") && tokens[1] === "test") return gates["test"];
77
87
  if (first === "python" || first === "python3") {
78
88
  if (tokens[1] !== "-m") return undefined;
79
89
  i = 2;
@@ -1,11 +1,17 @@
1
1
  /** D5/D11: incomplete execution is unknown, never an instruction to run twice. */
2
2
  import type { LogEvent, AssistantBlock } from "./events.ts";
3
3
 
4
- export function pendingToolCalls(events: readonly LogEvent[]): Extract<AssistantBlock, { type: "tool_use" }>[] {
5
- const pending = new Map<string, Extract<AssistantBlock, { type: "tool_use" }>>();
4
+ export type PendingToolCall = Extract<AssistantBlock, { type: "tool_use" }> & { parent_call_id?: string; invocation_id?: string; origin?: "extension" };
5
+ export function pendingToolCalls(events: readonly LogEvent[]): PendingToolCall[] {
6
+ const pending = new Map<string, PendingToolCall>();
6
7
  for (const event of events) {
7
8
  if (event.t === "assistant") {
8
9
  for (const block of event.blocks) if (block.type === "tool_use") pending.set(block.call_id, block);
10
+ } else if (event.t === "tool_invocation") {
11
+ const existing = pending.get(event.call_id);
12
+ pending.set(event.call_id, { type: "tool_use", call_id: event.call_id, name: event.name, input: event.input ?? existing?.input ?? null,
13
+ invocation_id: event.id, ...(event.origin ? { origin: event.origin } : {}),
14
+ ...(event.parent_call_id ? { parent_call_id: event.parent_call_id } : {}) });
9
15
  } else if (event.t === "tool_result") pending.delete(event.call_id);
10
16
  }
11
17
  return [...pending.values()];
@@ -1,5 +1,9 @@
1
1
  /** Experimental projections never mutate the durable event stream (D8, L). */
2
2
  import type { Message } from "./project.ts";
3
+ import { isTrialLifecycle, type TrialLifecycle } from "./trial-lifecycle.ts";
4
+ import { RECOVERY_TASKS, recoveryCase, type RecoveryTask } from "./recovery-corpus.ts";
5
+ import { isRecoveryMetrics, type RecoveryMetrics } from "./recovery-evaluation.ts";
6
+ import { TOOL_MODE_FACTORS, TOOL_MODE_TASKS, isToolModeTask, isToolModeMetrics, type ToolModeTask, type ToolModeMetrics } from "./tool-mode-protocol.ts";
3
7
 
4
8
  export type ContextArm = "node" | "replay" | "truncate" | "mask";
5
9
 
@@ -38,3 +42,132 @@ export function shuffled<T>(items: readonly T[], seed: number): T[] {
38
42
  }
39
43
  return out;
40
44
  }
45
+
46
+ /** O.16: each contrast changes exactly one policy, with paired task repeats. */
47
+ export const EVAL_FACTORS = ["profile", "workingSet", "strictVerification"] as const;
48
+ export const EVAL_TASKS = ["short", "boundary-recall", "false-completion", "human-boundary"] as const;
49
+ const supportedFactors = [...EVAL_FACTORS, ...TOOL_MODE_FACTORS] as const;
50
+ export type EvalFactor = typeof supportedFactors[number];
51
+ export type EvalTask = typeof EVAL_TASKS[number] | RecoveryTask | ToolModeTask;
52
+ const supportedTasks:readonly EvalTask[]=[...EVAL_TASKS,...RECOVERY_TASKS,...TOOL_MODE_TASKS];
53
+ export type EvalArm = "control" | "treatment";
54
+ export type EvalPolicy = { profile: "full" | "coding"; workingSet: boolean; strictVerification: boolean; querySyntax?: "json" | "sql" };
55
+ export type EvalJob = { id: string; factor: EvalFactor; task: EvalTask; repeat: number; arm: EvalArm; policy: EvalPolicy };
56
+ export function evalPolicy(factor: EvalFactor, arm: EvalArm): EvalPolicy {
57
+ const policy: EvalPolicy = { profile: "coding", workingSet: true, strictVerification: true };
58
+ if (factor === "querySyntax") return { ...policy, querySyntax: arm === "treatment" ? "sql" : "json" };
59
+ if (arm === "control") {
60
+ if (factor === "profile") policy.profile = "full";
61
+ else policy[factor] = false;
62
+ }
63
+ return policy;
64
+ }
65
+ export function singleFactorSchedule(seed: number, repeats = 1, tasks: readonly EvalTask[] = EVAL_TASKS, factors: readonly EvalFactor[] = EVAL_FACTORS): EvalJob[] {
66
+ if (!Number.isSafeInteger(seed) || !Number.isSafeInteger(repeats) || repeats < 1 || repeats > 20 || !tasks.length ||
67
+ new Set(tasks).size !== tasks.length || tasks.some(task => !supportedTasks.includes(task)) || !factors.length || new Set(factors).size !== factors.length ||
68
+ factors.some(factor => !supportedFactors.includes(factor) || tasks.some(task => isToolModeTask(task) !== (TOOL_MODE_FACTORS as readonly string[]).includes(factor)))) throw new RangeError("invalid eval schedule");
69
+ const pairs = shuffled(factors.flatMap(factor => [...tasks].sort().flatMap(task =>
70
+ Array.from({ length: repeats }, (_, repeat) => ({ factor, task, repeat })))), seed);
71
+ // Alternate first position before shuffling: adjacent LCG seeds are correlated.
72
+ const positions = new Map<EvalFactor, number>();
73
+ const order = shuffled(["control", "treatment"] as const, seed + 1);
74
+ return pairs.flatMap(pair => {
75
+ const offset = positions.get(pair.factor) ?? 0; positions.set(pair.factor, offset + 1);
76
+ return order.map((_, i) => {
77
+ const arm = order[(offset + i) % 2]!;
78
+ return { ...pair, arm, id: `${pair.factor}/${pair.task}/${pair.repeat}/${arm}`, policy: evalPolicy(pair.factor, arm) };
79
+ });
80
+ });
81
+ }
82
+ export function validateEvalJobs(jobs: readonly EvalJob[]): void {
83
+ const seen = new Set<string>();
84
+ for (const job of jobs) {
85
+ if (!supportedFactors.includes(job.factor) || !supportedTasks.includes(job.task) || !["control", "treatment"].includes(job.arm) ||
86
+ isToolModeTask(job.task) !== (TOOL_MODE_FACTORS as readonly string[]).includes(job.factor) ||
87
+ !Number.isSafeInteger(job.repeat) || job.repeat < 0 || job.id !== `${job.factor}/${job.task}/${job.repeat}/${job.arm}` || seen.has(job.id) ||
88
+ JSON.stringify(job.policy) !== JSON.stringify(evalPolicy(job.factor, job.arm))) throw new Error("invalid or confounded eval job");
89
+ seen.add(job.id);
90
+ }
91
+ for (const job of jobs) if (!seen.has(`${job.factor}/${job.task}/${job.repeat}/${job.arm === "control" ? "treatment" : "control"}`)) throw new Error("unpaired eval job");
92
+ }
93
+ export interface EvalResult {
94
+ toolMode?: ToolModeMetrics;
95
+ recovery?: RecoveryMetrics;
96
+ /** v2 separates an unmet final state from a machine-readable unsupported claim. */
97
+ unverifiedEnding?: boolean;
98
+ /** Absent in the frozen O.16 archives; absence is unknown, not an uninterrupted run. */
99
+ lifecycle?: TrialLifecycle;
100
+ id: string;
101
+ status: "finished" | "error" | "budget" | "stopped";
102
+ scenarioPassed: boolean;
103
+ artifactCorrect: boolean;
104
+ verified: boolean;
105
+ awaitingHuman: boolean;
106
+ falseCompletion: boolean;
107
+ requests: number;
108
+ toolFailures: number;
109
+ repeatedReads: number;
110
+ promptTokens: number | null;
111
+ cacheTokens: number | null;
112
+ outputTokens: number | null;
113
+ costUsd: number | null;
114
+ reservedUnknownUsd: number;
115
+ }
116
+ export function summarizeSingleFactor(jobs: readonly EvalJob[], results: readonly EvalResult[]) {
117
+ validateEvalJobs(jobs);
118
+ const byId = new Map<string, EvalResult>(), planned = new Set(jobs.map(job => job.id));
119
+ for (const row of results) {
120
+ if (row.toolMode !== undefined && (!isToolModeMetrics(row.toolMode) || row.scenarioPassed && !row.toolMode.sourceComplete)) throw Error("invalid tool mode metrics");
121
+ if (row.unverifiedEnding !== undefined && typeof row.unverifiedEnding !== "boolean") throw Error("invalid ending metric");
122
+ if(row.recovery!==undefined&&(!isRecoveryMetrics(row.recovery)||row.recovery.finalTaskSuccess!==row.scenarioPassed||row.recovery.toolFailures!==row.toolFailures||row.falseCompletion!==(row.recovery.unsupportedClaims>0)))throw Error("invalid recovery metrics");
123
+ if (row.lifecycle !== undefined && (!isTrialLifecycle(row.lifecycle) || row.lifecycle.backendRequests > row.requests ||
124
+ row.lifecycle.interruption !== null && row.status === "finished" || row.lifecycle.cleanup === "failed" && row.status !== "error"))
125
+ throw new Error("invalid eval lifecycle result");
126
+ if (!planned.has(row.id) || byId.has(row.id) || !["finished", "error", "budget", "stopped"].includes(row.status) ||
127
+ [row.scenarioPassed, row.artifactCorrect, row.verified, row.awaitingHuman, row.falseCompletion].some(value => typeof value !== "boolean") ||
128
+ row.scenarioPassed && (row.status !== "finished" || !row.verified || row.falseCompletion && row.recovery === undefined) ||
129
+ [row.requests, row.toolFailures, row.repeatedReads].some(n => !Number.isSafeInteger(n) || n < 0) ||
130
+ [row.promptTokens, row.cacheTokens, row.outputTokens].some(n => n !== null && (!Number.isSafeInteger(n) || n < 0)) ||
131
+ [row.costUsd, row.reservedUnknownUsd].some(n => n !== null && (!Number.isFinite(n) || n < 0)) ||
132
+ row.cacheTokens !== null && row.promptTokens !== null && row.cacheTokens > row.promptTokens) throw new Error("invalid or duplicate eval result");
133
+ byId.set(row.id, row);
134
+ }
135
+ const factors = supportedFactors.filter(factor => jobs.some(job => job.factor === factor));
136
+ const arms = factors.flatMap(factor => (["control", "treatment"] as const).map(arm => {
137
+ const selected = jobs.filter(job => job.factor === factor && job.arm === arm), rows = selected.flatMap(job => byId.has(job.id) ? [byId.get(job.id)!] : []);
138
+ const complete = rows.length === selected.length, scenarioPassed = rows.filter(row => row.scenarioPassed).length;
139
+ const knownCostSubtotal = rows.reduce((sum, row) => sum + (row.costUsd ?? 0), 0);
140
+ // Missing jobs and unknown charges cannot become a zero-priced success.
141
+ const costUsd = complete && rows.length > 0 && rows.every(row => row.costUsd !== null) ? knownCostSubtotal : null;
142
+ const total = (field: "promptTokens" | "cacheTokens" | "outputTokens") => complete && rows.length > 0 && rows.every(row => row[field] !== null)
143
+ ? rows.reduce((sum, row) => sum + row[field]!, 0) : null;
144
+ const recovery=selected.some(job=>recoveryCase(job.task)||isToolModeTask(job.task))?{observed:rows.filter(row=>row.recovery).length,
145
+ missing:selected.length-rows.filter(row=>row.recovery).length,
146
+ firstToolSuccess:rows.filter(row=>row.recovery?.firstToolSuccess===true).length,
147
+ firstPassTaskSuccess:rows.filter(row=>row.recovery?.firstPassTaskSuccess===true).length,
148
+ ...Object.fromEntries((["toolCalls","outerCalls","nestedCalls","unresolvedCalls","recoveryCalls","unchangedFailureRetries","originalEvidenceRetrievals","unsupportedClaims"] as const)
149
+ .map(key=>[key,rows.reduce((n,row)=>n+(row.recovery?.[key]??0),0)]))}:undefined;
150
+ return { factor, arm, ...(recovery?{recovery}:{}), planned: selected.length, attempted: rows.length, missing: selected.length - rows.length,
151
+ failed: rows.filter(row => !row.scenarioPassed).length, scenarioPassed, verified: rows.filter(row => row.verified).length,
152
+ awaitingHuman: rows.filter(row => row.awaitingHuman).length, falseCompletions: rows.filter(row => row.falseCompletion).length,
153
+ plannedPassFraction: selected.length ? scenarioPassed / selected.length : null, complete,
154
+ requests: rows.reduce((n, row) => n + row.requests, 0), toolFailures: rows.reduce((n, row) => n + row.toolFailures, 0),
155
+ repeatedReads: rows.reduce((n, row) => n + row.repeatedReads, 0), promptTokens: total("promptTokens"), cacheTokens: total("cacheTokens"), outputTokens: total("outputTokens"),
156
+ knownCostSubtotal, costUsd, unknownCostJobs: rows.filter(row => row.costUsd === null).length,
157
+ reservedUnknownUsd: rows.reduce((n, row) => n + row.reservedUnknownUsd, 0),
158
+ costPerScenarioSuccess: costUsd !== null && scenarioPassed > 0 ? costUsd / scenarioPassed : null };
159
+ }));
160
+ const comparisons = factors.map(factor => {
161
+ const pairs = jobs.filter(job => job.factor === factor && job.arm === "control");
162
+ const clusters = [...new Set(pairs.map(job => job.task))].sort().map(task => {
163
+ const matched = pairs.filter(job => job.task === task).flatMap(job => {
164
+ const control = byId.get(job.id), treatment = byId.get(`${factor}/${task}/${job.repeat}/treatment`);
165
+ return control && treatment ? [{ delta: Number(treatment.scenarioPassed) - Number(control.scenarioPassed) }] : [];
166
+ });
167
+ return { task, matchedPairs: matched.length, passDelta: matched.length ? matched.reduce((n, pair) => n + pair.delta, 0) / matched.length : null };
168
+ });
169
+ const matchedPairs = clusters.reduce((n, row) => n + row.matchedPairs, 0);
170
+ return { factor, matchedPairs, missingPairs: pairs.length - matchedPairs, clusters };
171
+ });
172
+ return { arms, comparisons };
173
+ }