@visual-z/xeno 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2066
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1713
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3001
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/canvas.ts +0 -96
  312. package/src/tui/ui/colors.ts +0 -70
  313. package/src/tui/ui/composer.ts +0 -200
  314. package/src/tui/ui/control-panel.ts +0 -25
  315. package/src/tui/ui/control-sidebar.ts +0 -49
  316. package/src/tui/ui/dialog.ts +0 -398
  317. package/src/tui/ui/draft.ts +0 -23
  318. package/src/tui/ui/duration.ts +0 -16
  319. package/src/tui/ui/edge.ts +0 -186
  320. package/src/tui/ui/image-size.ts +0 -44
  321. package/src/tui/ui/ink.ts +0 -210
  322. package/src/tui/ui/items.ts +0 -42
  323. package/src/tui/ui/keys.ts +0 -40
  324. package/src/tui/ui/layout.ts +0 -74
  325. package/src/tui/ui/list-model.ts +0 -86
  326. package/src/tui/ui/list-view.ts +0 -172
  327. package/src/tui/ui/mascot.ts +0 -67
  328. package/src/tui/ui/messages.ts +0 -518
  329. package/src/tui/ui/motion.ts +0 -28
  330. package/src/tui/ui/paths.ts +0 -10
  331. package/src/tui/ui/pictures.ts +0 -108
  332. package/src/tui/ui/screen.ts +0 -548
  333. package/src/tui/ui/status.ts +0 -403
  334. package/src/tui/ui/styled.ts +0 -85
  335. package/src/tui/ui/tool-card.ts +0 -150
  336. package/src/tui/width.ts +0 -567
  337. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  338. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  339. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  340. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  341. package/src/vendor/pi-rich/html.ts +0 -51
  342. package/src/vendor/pi-rich/image-convert.ts +0 -49
  343. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  344. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  345. package/src/vendor/pi-rich/image-resize.ts +0 -123
  346. package/src/vendor/pi-rich/mime.ts +0 -116
  347. package/src/vendor/pi-rich/photon.ts +0 -139
  348. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,78 +0,0 @@
1
- /** R4 / D8: a read descriptor names immutable version premises; it grants no authority. */
2
- import type { ObservationReceipt } from "./read-observation.ts";
3
- import { isObservationArtifact } from "./observation-view.ts";
4
- import type { LogEvent } from "./events.ts";
5
-
6
- export interface FileIdentity { host: string; workspace: string; resource: string }
7
- export interface FileReadDescriptor {
8
- kind: "file_read";
9
- version: 1;
10
- source: ObservationReceipt["source"];
11
- selection: ObservationReceipt["selection"];
12
- }
13
- export interface FilePrecondition {
14
- source: "read_handle" | "expected_sha256" | "session" | "create_only" | "unobserved";
15
- expected: string | null;
16
- read_handle?: string;
17
- }
18
- export type FilePreconditionDecision = { ok: true; precondition: FilePrecondition } |
19
- { ok: false; code: "invalid_input" | "version_mismatch"; message: string; precondition?: FilePrecondition };
20
-
21
- const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
22
- const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
23
- export const isFileDigest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
24
-
25
- /** Preserve chronology across path aliases; handoff anchors intentionally keep their legacy spelling. */
26
- export function latestObservedFileVersion(events: readonly LogEvent[], resource: string, identify: (path: string) => string): string | undefined {
27
- for (let index = events.length - 1; index >= 0; index--) {
28
- const event = events[index]!;
29
- // P.3: multi-file Patch and captured commands observe each actual resulting version.
30
- if (event.t === "checkpoint" && event.phase === "applied") {
31
- for (const [path, change] of Object.entries(event.changes))
32
- if (identify(path) === resource) return change.after?.sha256;
33
- continue;
34
- }
35
- if (event.t !== "tool_result") continue;
36
- const effect = event.effect;
37
- if ((effect?.kind === "read" || effect?.kind === "edit") && identify(effect.path) === resource)
38
- return effect.kind === "read" ? effect.sha256 : effect.sha256_after;
39
- }
40
- return undefined;
41
- }
42
-
43
- export function isFileReadDescriptor(value: unknown): value is FileReadDescriptor {
44
- if (!record(value) || value["kind"] !== "file_read" || value["version"] !== 1) return false;
45
- const source = value["source"], selection = value["selection"];
46
- return record(source) && ["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") &&
47
- isFileDigest(source["sha256"]) && isObservationArtifact(source["raw"]) && source["raw"].sha256 === source["sha256"] &&
48
- record(selection) && natural(selection["start"]) && selection["start"] >= 1 && natural(selection["end"]) &&
49
- natural(selection["total_lines"]) && selection["start"] <= selection["end"] && selection["end"] <= selection["total_lines"];
50
- }
51
-
52
- /** Explicit premises take precedence; an omitted premise may use the complete log projection. */
53
- export function checkFilePrecondition(identity: FileIdentity, current: string | null, input: {
54
- descriptor?: FileReadDescriptor; read_handle?: string; expected_sha256?: string; observed?: string; create_only?: boolean;
55
- }): FilePreconditionDecision {
56
- const invalid = (message: string): FilePreconditionDecision => ({ ok: false, code: "invalid_input", message });
57
- if (input.expected_sha256 !== undefined && !isFileDigest(input.expected_sha256)) return invalid("expected_sha256 must be a lowercase SHA-256 digest");
58
- if (input.observed !== undefined && !isFileDigest(input.observed)) return invalid("the observed file version is invalid; inspect the original log");
59
- if (input.create_only && (input.read_handle !== undefined || input.expected_sha256 !== undefined))
60
- return invalid("create_only cannot be combined with read_handle or expected_sha256");
61
- if ((input.read_handle === undefined) !== (input.descriptor === undefined)) return invalid("read_handle requires its verified descriptor");
62
- if (input.descriptor) {
63
- if (!isFileReadDescriptor(input.descriptor) || !isFileDigest(input.read_handle)) return invalid("invalid read_handle descriptor");
64
- for (const field of ["host", "workspace", "resource"] as const)
65
- if (input.descriptor.source[field] !== identity[field]) return invalid(`read_handle names another ${field}; read the intended file here`);
66
- if (input.expected_sha256 !== undefined && input.expected_sha256 !== input.descriptor.source.sha256)
67
- return invalid("read_handle and expected_sha256 name different versions");
68
- }
69
- const precondition: FilePrecondition = input.create_only ? { source: "create_only", expected: null }
70
- : input.descriptor ? { source: "read_handle", expected: input.descriptor.source.sha256, read_handle: input.read_handle! }
71
- : input.expected_sha256 !== undefined ? { source: "expected_sha256", expected: input.expected_sha256 }
72
- : input.observed !== undefined ? { source: "session", expected: input.observed }
73
- : { source: "unobserved", expected: null };
74
- if (precondition.source !== "unobserved" && precondition.expected !== current)
75
- return { ok: false, code: "version_mismatch", precondition,
76
- message: `file version changed; expected ${precondition.expected ?? "absent"}, current sha256: ${current ?? "absent"}. Read the current file before retrying.` };
77
- return { ok: true, precondition };
78
- }
@@ -1,35 +0,0 @@
1
- /** Explicit whitespace-delimited @ references; code literals and emails remain text. */
2
- export interface FileReference { readonly start: number; readonly end: number; readonly path: string }
3
-
4
- export function fileReferences(text: string): FileReference[] {
5
- const refs: FileReference[] = [];
6
- let codeTicks = 0;
7
- for (let i = 0; i < text.length; i++) {
8
- if (text[i] === "\\") { i++; continue; }
9
- if (text[i] === "`") {
10
- let end = i;
11
- while (text[end] === "`") end++;
12
- const count = end - i;
13
- if (codeTicks === 0) codeTicks = count;
14
- else if (count === codeTicks) codeTicks = 0;
15
- i = end - 1;
16
- continue;
17
- }
18
- if (codeTicks > 0 || text[i] !== "@" || (i > 0 && !/\s/.test(text[i - 1]!))) continue;
19
- const start = i;
20
- let end = i + 1;
21
- let path: string;
22
- if (text[end] === '"') {
23
- const close = text.indexOf('"', end + 1);
24
- if (close < 0 || text.slice(end, close).includes("\n")) continue;
25
- path = text.slice(end + 1, close);
26
- end = close + 1;
27
- } else {
28
- while (end < text.length && !/\s/.test(text[end]!)) end++;
29
- path = text.slice(start + 1, end);
30
- }
31
- if (path.length > 0) refs.push({ start, end, path });
32
- i = end - 1;
33
- }
34
- return refs;
35
- }
@@ -1,213 +0,0 @@
1
- /**
2
- * Restricted YAML frontmatter reader.
3
- *
4
- * Why hand-written instead of a YAML package: the emitter in yaml.ts already
5
- * made that call for writes (AGENTS.md cites it as the example). This is the
6
- * read side of the same choice. A full parser would pull in aliases, anchors
7
- * and tags on a path that reads user and project files.
8
- *
9
- * Supported: `key: value`, quoted strings, booleans, and `[a, b]` arrays.
10
- * Nested mappings and multiline blocks are named and skipped; siblings stay.
11
- * The function does not throw — a broken header must not take down startup.
12
- */
13
-
14
- export type FrontmatterValue = string | boolean | readonly string[];
15
-
16
- export interface FrontmatterParse {
17
- readonly fields: { readonly [key: string]: FrontmatterValue };
18
- readonly body: string;
19
- readonly diagnostics: readonly string[];
20
- }
21
-
22
- const KEY = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
23
- const BOOL = /^(?:true|false)$/i;
24
-
25
- export function parseFrontmatter(text: string): FrontmatterParse {
26
- try {
27
- return parseInner(text);
28
- } catch (err) {
29
- const message = err instanceof Error ? err.message : String(err);
30
- return { fields: {}, body: text, diagnostics: [`frontmatter: ${message}`] };
31
- }
32
- }
33
-
34
- function parseInner(text: string): FrontmatterParse {
35
- const normalized = stripBom(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
36
- const extracted = extract(normalized);
37
- if (extracted.yaml === null) {
38
- return { fields: {}, body: extracted.body, diagnostics: [] };
39
- }
40
-
41
- const fields: { [key: string]: FrontmatterValue } = {};
42
- const diagnostics: string[] = [];
43
- const lines = extracted.yaml.split("\n");
44
-
45
- for (let i = 0; i < lines.length; i++) {
46
- const line = lines[i]!;
47
- if (line.trim().length === 0 || line.trimStart().startsWith("#")) continue;
48
-
49
- if (/^\s/.test(line)) {
50
- diagnostics.push(`frontmatter: indented line is not a supported field`);
51
- continue;
52
- }
53
-
54
- const match = KEY.exec(line);
55
- if (!match) {
56
- diagnostics.push(`frontmatter: unparseable line ${JSON.stringify(line)}`);
57
- continue;
58
- }
59
-
60
- const key = match[1]!;
61
- const raw = match[2] ?? "";
62
-
63
- if (isNestedMarker(raw)) {
64
- diagnostics.push(`frontmatter: nested mapping "${key}" is not supported`);
65
- i = skipIndented(lines, i);
66
- continue;
67
- }
68
-
69
- const parsed = parseValue(raw);
70
- if (parsed === undefined) {
71
- diagnostics.push(`frontmatter: nested mapping "${key}" is not supported`);
72
- i = skipIndented(lines, i);
73
- continue;
74
- }
75
- fields[key] = parsed;
76
- }
77
-
78
- return { fields, body: extracted.body, diagnostics };
79
- }
80
-
81
- function extract(text: string): { yaml: string | null; body: string } {
82
- if (!text.startsWith("---")) return { yaml: null, body: text };
83
- const lines = text.split("\n");
84
- if (lines[0] !== "---") return { yaml: null, body: text };
85
- let end = -1;
86
- for (let i = 1; i < lines.length; i++) {
87
- if (lines[i] === "---") {
88
- end = i;
89
- break;
90
- }
91
- }
92
- if (end < 0) return { yaml: null, body: text };
93
- return {
94
- yaml: lines.slice(1, end).join("\n"),
95
- body: lines.slice(end + 1).join("\n").replace(/^\n+/, "").replace(/\s+$/, ""),
96
- };
97
- }
98
-
99
- function isNestedMarker(raw: string): boolean {
100
- const value = raw.trim();
101
- return value.length === 0 || value === "|" || value === "|-" || value === "|+" ||
102
- value === ">" || value === ">-" || value === ">+";
103
- }
104
-
105
- function parseValue(raw: string): FrontmatterValue | undefined {
106
- const trimmed = raw.trim();
107
- if (trimmed.startsWith("{")) return undefined;
108
- if (trimmed.startsWith("[")) return parseFlowArray(trimmed);
109
- if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
110
- const quoted = parseQuoted(trimmed);
111
- return quoted ?? trimmed;
112
- }
113
- const unquoted = stripInlineComment(trimmed);
114
- if (BOOL.test(unquoted)) return unquoted.toLowerCase() === "true";
115
- return unquoted;
116
- }
117
-
118
- /**
119
- * Flow arrays of scalars only. A mapping inside the brackets is a nested
120
- * structure we refuse, so the caller can name the field and keep going.
121
- */
122
- function parseFlowArray(raw: string): readonly string[] | undefined {
123
- if (!raw.startsWith("[") || !raw.endsWith("]")) return undefined;
124
- const inner = raw.slice(1, -1).trim();
125
- if (inner.length === 0) return [];
126
- if (inner.includes("{") || inner.includes("[") || /(?:^|,)\s*[A-Za-z_][A-Za-z0-9_-]*\s*:/.test(inner)) {
127
- return undefined;
128
- }
129
- const items: string[] = [];
130
- let buf = "";
131
- let quote: string | null = null;
132
- for (let i = 0; i < inner.length; i++) {
133
- const ch = inner[i]!;
134
- if (quote) {
135
- if (quote === "'" && ch === "'" && inner[i + 1] === "'") {
136
- buf += "'";
137
- i++;
138
- continue;
139
- }
140
- if (ch === quote && (quote === "'" || inner[i - 1] !== "\\")) {
141
- quote = null;
142
- continue;
143
- }
144
- buf += ch;
145
- continue;
146
- }
147
- if (ch === '"' || ch === "'") {
148
- quote = ch;
149
- continue;
150
- }
151
- if (ch === ",") {
152
- items.push(buf.trim());
153
- buf = "";
154
- continue;
155
- }
156
- buf += ch;
157
- }
158
- items.push(buf.trim());
159
- return items;
160
- }
161
-
162
- function parseQuoted(raw: string): string | undefined {
163
- const q = raw[0];
164
- if (q !== '"' && q !== "'") return undefined;
165
- let out = "";
166
- for (let i = 1; i < raw.length; i++) {
167
- const ch = raw[i]!;
168
- if (q === "'" && ch === "'" && raw[i + 1] === "'") {
169
- out += "'";
170
- i++;
171
- continue;
172
- }
173
- if (ch === q) {
174
- const rest = raw.slice(i + 1).trim();
175
- if (rest.length > 0 && !rest.startsWith("#")) return undefined;
176
- return out;
177
- }
178
- if (q === '"' && ch === "\\" && i + 1 < raw.length) {
179
- const next = raw[i + 1]!;
180
- out += next === "n" ? "\n" : next === "t" ? "\t" : next;
181
- i++;
182
- continue;
183
- }
184
- out += ch;
185
- }
186
- return undefined;
187
- }
188
-
189
- function stripInlineComment(value: string): string {
190
- const idx = value.indexOf(" #");
191
- return (idx >= 0 ? value.slice(0, idx) : value).trim();
192
- }
193
-
194
- function skipIndented(lines: readonly string[], i: number): number {
195
- let j = i + 1;
196
- while (j < lines.length) {
197
- const next = lines[j]!;
198
- if (next.trim().length === 0) {
199
- j++;
200
- continue;
201
- }
202
- if (/^\s/.test(next)) {
203
- j++;
204
- continue;
205
- }
206
- break;
207
- }
208
- return j - 1;
209
- }
210
-
211
- function stripBom(text: string): string {
212
- return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
213
- }
@@ -1,29 +0,0 @@
1
- /** Counts from Git porcelain v2, distinct from agent tool effects. No path decoding needed. */
2
- export interface GitStatus {
3
- readonly branch: string | null;
4
- readonly staged: number;
5
- readonly changed: number;
6
- readonly untracked: number;
7
- readonly conflicted: number;
8
- readonly ahead: number;
9
- readonly behind: number;
10
- }
11
- export function parseGitStatus(output: string): GitStatus {
12
- let branch: string | null = null, staged = 0, changed = 0, untracked = 0, conflicted = 0, ahead = 0, behind = 0;
13
- const records = output.split("\0");
14
- for (let index = 0; index < records.length; index++) {
15
- const line = records[index]!;
16
- if (line.startsWith("# branch.head ")) branch = line.slice(14).trim();
17
- else if (line.startsWith("# branch.ab ")) {
18
- const match = /^# branch.ab \+(\d+) -(\d+)$/.exec(line.trim());
19
- if (match) { ahead = Number(match[1]); behind = Number(match[2]); }
20
- } else if (line.startsWith("? ")) untracked++;
21
- else if (/^[12u] [A-Z?.]{2} /.test(line)) {
22
- if (line[2] !== ".") staged++;
23
- if (line[3] !== ".") changed++;
24
- if (line[0] === "u") conflicted++;
25
- if (line[0] === "2") index++; // Rename's next NUL field is a path, never a status record.
26
- }
27
- }
28
- return { branch, staged, changed, untracked, conflicted, ahead, behind };
29
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * What the sidebar shows about an explicit goal (feat-sidebar-goal-and-speed),
3
- * derived from the log alone so it reads the same after a restart.
4
- *
5
- * The block exists only while a /goal is set, and for one message after it is
6
- * met. Its verdicts are the request ledger's (D11); this only chooses words:
7
- * the phase answers "do I need to act, and is it still running", and each
8
- * accepted check says what it needs from the user.
9
- */
10
- import type { LogEvent } from "./events.ts";
11
- import type { RequestCriterion, RequestState } from "./request-state.ts";
12
-
13
- export type GoalPhase = "drafting" | "working" | "needs_you" | "met" | "stopped";
14
- export type GoalCheck = { id: string; kind: RequestCriterion["kind"]; status: RequestCriterion["status"]; guard?: true; exit?: number };
15
- /**
16
- * `startedAt` is when the goal was set; `endedAt` when it was met, absent while
17
- * it runs. Times, not a duration, so a caller can cache this per log length and
18
- * read the clock only when it paints.
19
- */
20
- export type GoalStatus = { phase: GoalPhase; objective: string; checks: GoalCheck[]; startedAt: number; endedAt?: number; requests: number };
21
-
22
- /**
23
- * The goal block for the end of an ancestry, or null when no goal applies.
24
- * `running` is whether a request is in flight now; `drafting` is the objective
25
- * of a /goal whose criteria are still being drafted.
26
- */
27
- export function goalStatus(events: readonly LogEvent[], state: RequestState | undefined,
28
- live: { running: boolean; drafting?: string }): GoalStatus | null {
29
- if (live.drafting !== undefined) return { phase: "drafting", objective: live.drafting, checks: [], startedAt: 0, requests: 0 };
30
- const lastSet = events.findLastIndex(event => event.t === "goal" && event.action === "set");
31
- if (lastSet < 0) return null;
32
- const set = events[lastSet]!;
33
- if (set.t !== "goal" || set.action !== "set") return null;
34
- const after = events.slice(lastSet + 1);
35
- // The last clear decides: a met goal the user then cleared is gone like any other cleared goal.
36
- const clear = after.findLastIndex(event => event.t === "goal" && event.action === "clear");
37
- const cleared = clear < 0 ? undefined : after[clear];
38
- // A goal the user cleared is gone; a met goal stays for one summary, until the next message opens a new request.
39
- if (cleared?.t === "goal" && cleared.action === "clear" && cleared.reason !== "met") return null;
40
- const recorded = cleared !== undefined;
41
- if (recorded && after.slice(clear + 1).some(event => event.t === "user" && event.request === undefined)) return null;
42
- // Only the goal's own request carries its contract; the ledger of any other request says nothing about it.
43
- const own = state?.acceptance?.contract_id === set.contract.id && state.acceptance.contract_event !== undefined
44
- && events.some((event, index) => index > lastSet && event.id === state.acceptance!.contract_event) ? state : undefined;
45
- // The ledger is the verdict (D11): verified is met even before the clear is recorded -- an /accept outside a
46
- // request, a log written before /accept recorded it -- and its clock stops at the last recorded fact.
47
- const met = recorded || own?.status === "verified";
48
- const end = recorded ? Date.parse(cleared.at) : met ? Date.parse(after.at(-1)?.at ?? set.at) : undefined;
49
- const checks: GoalCheck[] = set.contract.criteria.map(item => {
50
- const entry = own?.criteria.find(criterion => criterion.id === item.id);
51
- const run = item.kind === "command" ? own?.commands.findLast(command => command.command === item.command) : undefined;
52
- return { id: item.id, kind: item.kind, status: entry?.status ?? (item.kind === "human" ? "awaiting_human" : "not_run"),
53
- ...(item.kind === "command" && item.guard ? { guard: true as const } : {}),
54
- ...(entry?.status === "fail" && run ? { exit: run.exit } : {}) };
55
- });
56
- return { phase: met ? "met" : live.running ? "working" : own?.status === "awaiting_human" ? "needs_you" : "stopped",
57
- objective: set.objective, checks, startedAt: Date.parse(set.at), ...(end === undefined ? {} : { endedAt: end }),
58
- requests: (recorded ? after.slice(0, clear) : after).filter(event => event.t === "assistant").length };
59
- }
60
-
61
- /** The words the sidebar and the input edge use for a phase. */
62
- export function goalPhaseLabel(phase: GoalPhase): string {
63
- return phase === "needs_you" ? "needs you" : phase;
64
- }
package/src/core/goal.ts DELETED
@@ -1,200 +0,0 @@
1
- /**
2
- * Explicit goals (research doc S.2 / S.5).
3
- *
4
- * The model drafts acceptance criteria for a goal; the user accepts them one
5
- * by one; only the accepted ones become the goal's contract. Whether the goal
6
- * is met is never the model's call: it is the request ledger's status, which
7
- * only command receipts and the user's own acceptance decisions move (D11).
8
- * The draft is itself only a claim -- it parses into candidates, and a
9
- * candidate that does not validate is reported and sent back to the model to
10
- * correct, never repaired by guessing.
11
- */
12
- import { ACCEPTANCE_COMMAND_LIMIT, parseAcceptanceContract, type AcceptanceContract, type AcceptanceCriterion } from "./acceptance.ts";
13
- import type { GoalEvent, LogEvent } from "./events.ts";
14
- import type { RequestState } from "./request-state.ts";
15
-
16
- /**
17
- * Assumed, not measured: enough for a goal's distinct checks, few enough that
18
- * a person reads and decides on each one rather than accepting a wall.
19
- */
20
- export const GOAL_CRITERIA_LIMIT = 8;
21
-
22
- export function goalDraftRequest(objective: string): string {
23
- return [
24
- `Goal: ${objective}`,
25
- "",
26
- "Do not start the work yet. Draft the acceptance criteria that would show this goal is met, for the user to accept or reject one by one.",
27
- "You may read the workspace first (read, find, grep) to learn its real files and commands. You cannot change anything or run commands in this step.",
28
- `Then reply with only a JSON object: {"criteria":[{"id":"short-id","kind":"command","description":"what it shows","command":"exact shell command"}]}, at most ${GOAL_CRITERIA_LIMIT} items.`,
29
- "- kind \"command\": a command that exits 0 only when the criterion holds, and fails on the workspace as it is now if the goal is not yet done. Prefer checks that exercise new inputs over checks that only replay an example.",
30
- "- \"guard\": true on a command for what the goal must not break or change, such as files to leave untouched or behaviour that must keep working. A guard passes now and must still pass when the goal is done; every command without it must fail now.",
31
- "- kind \"human\" (no command): only for what a person must judge, such as tone or visual quality.",
32
- "- ids: letters, digits, dot, dash, underscore, colon; unique.",
33
- "If you cannot tell how to check something, make it a human criterion rather than inventing a command.",
34
- "If the goal cannot be checked or achieved in this workspace at all, reply {\"criteria\": [], \"blocked\": \"the specific reason\"}.",
35
- ].join("\n");
36
- }
37
-
38
- export type GoalDraft = { criteria: AcceptanceCriterion[]; errors: string[]; blocked?: string };
39
-
40
- /**
41
- * Assumed, not measured: one correction. Instructor's reask and Pydantic AI's
42
- * output retries likewise send the validation errors back for a bounded number
43
- * of attempts. The slip this exists for -- a real GLM draft missing its
44
- * closing brackets -- needs one; a model that fails the same schema twice is
45
- * not helped by a third full request, and the user can still redraft.
46
- */
47
- export const GOAL_DRAFT_RETRIES = 1;
48
-
49
- /** The correction request: the parser's own reasons, and the form the reply must take. */
50
- export function goalDraftRetry(errors: readonly string[]): string {
51
- return [
52
- "Harness draft check: your draft could not be used as written.",
53
- ...errors.map(error => `- ${error}`),
54
- "Reply again with only the complete corrected JSON object in the same form, keeping the criteria that were valid. Do not start the work.",
55
- ].join("\n");
56
- }
57
-
58
- /**
59
- * The draft among the events one draft run recorded: the last assistant reply
60
- * of the first request they open. A follow-up the user queued meanwhile opens
61
- * its own request and is never read as the draft; a run that recorded no
62
- * request has no draft.
63
- */
64
- export function draftReply(recorded: readonly LogEvent[]): { id: string; text: string } | null {
65
- const opens = (event: LogEvent) => event.t === "user" && event.request === undefined;
66
- const start = recorded.findIndex(opens);
67
- if (start < 0) return null;
68
- const next = recorded.findIndex((event, index) => index > start && opens(event));
69
- const reply = recorded.slice(start, next < 0 ? undefined : next).findLast(event => event.t === "assistant");
70
- return reply?.t === "assistant" ? { id: reply.id, text: reply.blocks.flatMap(block => block.type === "text" ? [block.text] : []).join("\n") } : null;
71
- }
72
-
73
- /** The first JSON object in a reply: a fenced block when present, else the outermost braces. */
74
- function jsonObject(text: string): unknown {
75
- const fenced = /```(?:json)?\s*\n([\s\S]*?)\n```/.exec(text)?.[1];
76
- const candidate = fenced ?? text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1);
77
- if (!candidate.trim()) throw Error("the reply holds no JSON object");
78
- return JSON.parse(candidate);
79
- }
80
-
81
- const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/;
82
-
83
- /**
84
- * Why one drafted item is not a criterion, in words the user can act on; null
85
- * when it is one. A human item's empty or null `command`, and a `guard` of
86
- * false or null, are only a model's way of saying "none" and are dropped
87
- * before this runs.
88
- */
89
- function draftItemError(item: Record<string, unknown>, seen: ReadonlySet<string>): string | null {
90
- const { id, kind, description, command, guard } = item;
91
- if (typeof id !== "string" || !id.trim()) return "has no id";
92
- if (!ID.test(id)) return `id ${JSON.stringify(id)} may use only letters, digits, dot, dash, underscore and colon (at most 80)`;
93
- if (seen.has(id)) return `repeats id ${id}`;
94
- if (typeof description !== "string" || !description.trim()) return "has no description";
95
- if (description.length > 300) return "has a description longer than 300 characters";
96
- if (kind === "human") return command !== undefined ? "is a human criterion with a command; make it a command criterion or drop the command"
97
- : guard !== undefined ? "is a human criterion marked as a guard; a guard needs a command" : null;
98
- if (kind !== "command") return `has kind ${JSON.stringify(kind)}; use "command" or "human"`;
99
- if (guard !== undefined && guard !== true) return `has guard ${JSON.stringify(guard)}; use true or leave it out`;
100
- if (typeof command !== "string" || !command.trim()) return "needs a command";
101
- if (command.length > ACCEPTANCE_COMMAND_LIMIT) return `has a command longer than ${ACCEPTANCE_COMMAND_LIMIT} characters`;
102
- return null;
103
- }
104
-
105
- export function parseGoalDraft(text: string): GoalDraft {
106
- let value: unknown;
107
- try { value = jsonObject(text); }
108
- catch (error) { return { criteria: [], errors: [`the draft is not readable JSON: ${error instanceof Error ? error.message : String(error)}`] }; }
109
- const object = value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
110
- const items = object?.["criteria"];
111
- if (!Array.isArray(items)) return { criteria: [], errors: ["the draft has no criteria array"] };
112
- const blockedText = object?.["blocked"];
113
- const blocked = typeof blockedText === "string" && blockedText.trim() ? blockedText.trim().slice(0, 500) : undefined;
114
- const criteria: AcceptanceCriterion[] = [], errors: string[] = [], seen = new Set<string>();
115
- items.forEach((raw, index) => {
116
- if (criteria.length >= GOAL_CRITERIA_LIMIT) { errors.push(`criterion ${index + 1}: more than ${GOAL_CRITERIA_LIMIT} criteria`); return; }
117
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { errors.push(`criterion ${index + 1} is not an object`); return; }
118
- const item = { ...(raw as Record<string, unknown>) };
119
- if (item["kind"] === "human" && (item["command"] === null || typeof item["command"] === "string" && !item["command"].trim())) delete item["command"];
120
- if (item["guard"] === false || item["guard"] === null) delete item["guard"];
121
- const problem = draftItemError(item, seen);
122
- if (problem) { errors.push(`criterion ${index + 1} ${problem}`); return; }
123
- // The one validator for criteria: a draft item is accepted exactly when a contract would accept it.
124
- try {
125
- const [criterion] = parseAcceptanceContract({ version: 1, id: "goal", criteria: [{ id: item["id"], kind: item["kind"], description: item["description"],
126
- ...(item["kind"] === "command" ? { command: item["command"], ...(item["guard"] === true ? { guard: true } : {}) } : {}) }] }).criteria;
127
- seen.add(criterion!.id); criteria.push(criterion!);
128
- } catch (error) { errors.push(`criterion ${index + 1}: ${error instanceof Error ? error.message : String(error)}`); }
129
- });
130
- return { criteria, errors, ...(blocked ? { blocked } : {}) };
131
- }
132
-
133
- /** Only accepted criteria, in draft order. Accepting none is not a goal. */
134
- export function goalContract(criteria: readonly AcceptanceCriterion[], accepted: readonly string[]): AcceptanceContract {
135
- const chosen = criteria.filter(item => accepted.includes(item.id));
136
- if (chosen.length === 0) throw Error("a goal needs at least one accepted criterion");
137
- return parseAcceptanceContract({ version: 1, id: "goal", scope: "request", criteria: chosen });
138
- }
139
-
140
- export type ActiveGoal = Extract<GoalEvent, { action: "set" }>;
141
-
142
- /** The goal in force at the end of an ancestry: the last set, unless a clear followed it. */
143
- export function activeGoal(events: readonly LogEvent[]): ActiveGoal | null {
144
- const last = events.findLast(event => event.t === "goal");
145
- return last?.t === "goal" && last.action === "set" ? last : null;
146
- }
147
-
148
- /**
149
- * Assumed, not measured: two goal checks answered without any tool use means
150
- * the model is talking past the goal, and more requests will not change that.
151
- * Claude Code's /goal stops on the same signal ("no tool use for several
152
- * turns in a row"). Too low would stop a model that needed one more reply to
153
- * plan; too high spends requests on replies that do nothing.
154
- */
155
- export const GOAL_IDLE_LIMIT = 2;
156
-
157
- /** Goal checks since the last tool result: each one the model answered without acting. */
158
- export function idleGoalChecks(events: readonly LogEvent[]): number {
159
- let count = 0;
160
- for (let i = events.length - 1; i >= 0; i--) {
161
- const event = events[i]!;
162
- if (event.t === "tool_result" || event.t === "user" && event.request === undefined) break;
163
- if (event.t === "guidance" && event.reason === "goal") count++;
164
- }
165
- return count;
166
- }
167
-
168
- export type GoalDecision =
169
- | { action: "met" }
170
- | { action: "await_human" }
171
- | { action: "stop"; reason: string }
172
- | { action: "continue"; text: string };
173
-
174
- /**
175
- * What a goal does when the model ends a turn. Reads the request ledger, so
176
- * the loop and every goal view agree. A reported blocker stops the loop --
177
- * spending more requests against a stated blocker is the user's call -- but
178
- * leaves the goal active: the statement is not evidence it cannot be met.
179
- */
180
- export function goalDecision(state: RequestState, remainingRequests: number, idleChecks = 0): GoalDecision {
181
- if (state.status === "verified") return { action: "met" };
182
- if (state.status === "awaiting_human") return { action: "await_human" };
183
- if (state.reported_blockers.length > 0) return { action: "stop", reason: `the model reported a blocker: ${state.reported_blockers.at(-1)!.reason}` };
184
- if (remainingRequests < 1) return { action: "stop", reason: "the request budget is spent" };
185
- if (idleChecks >= GOAL_IDLE_LIMIT) return { action: "stop", reason: `the model answered ${idleChecks} goal checks without acting` };
186
- const missing = state.criteria.filter(item => item.kind === "command" && item.status !== "pass");
187
- // An unchanged workspace failing the same way twice: rerunning is pointless,
188
- // and the accepted check may be what is wrong. The model may not rewrite it;
189
- // only the user can redraft a goal (S.2).
190
- const stuck = missing.filter(item => (item.repeated_failures ?? 0) >= 2), rerun = missing.filter(item => !stuck.includes(item));
191
- return { action: "continue", text: [
192
- `Harness goal check: the goal is not met yet. Unmet: ${state.unmet.join("; ")}.`,
193
- ...(rerun.length ? ["Run each missing check exactly, as a separate invocation:",
194
- ...rerun.map(item => `- ${item.id}${item.guard ? " (a guard: it must keep passing)" : ""}: ${JSON.stringify(item.command)}`)] : []),
195
- ...stuck.map(item => `- ${item.id} failed ${item.repeated_failures} times in a row against an unchanged workspace. Do not rerun it as is: change the work, ` +
196
- "or, if the check itself may itself be broken (a syntax error, a missing tool, or a test of the wrong thing), record which check is broken and why " +
197
- "with note(kind='blocked') and stop. Do not rewrite or replace an accepted check; the user can redraft the goal with /goal."),
198
- "Keep working toward the goal. If something outside your control blocks it, record the specific blocker with note(kind='blocked') and stop.",
199
- ].join("\n") };
200
- }