@jmoyers/harness 0.1.10 → 0.1.20

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 (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
package/README.md CHANGED
@@ -1,17 +1,22 @@
1
1
  # Harness
2
2
 
3
- Harness is a terminal-native workspace for running multiple coding-agent threads in parallel, without losing project context.
3
+ Harness is minimal agent orchestration in the terminal.
4
4
 
5
- It is built for people who want to move faster than a single chat window: implementation, review, and follow-up work can run side by side in one keyboard-first interface.
5
+ Programmable, agent-agnostic, threaded, ergonomic, and fast.
6
6
 
7
7
  ## What matters most
8
8
 
9
9
  - Parallel threads across `codex`, `claude`, `cursor`, `terminal`, and `critique`.
10
- - One command palette (`ctrl+p` / `cmd+p`) to jump threads, run actions, and control workflow quickly.
11
- - Long-running work survives reconnects through a detached gateway.
12
- - Gateway control is resilient: lifecycle operations are lock-serialized per session, and missing stale records can be recovered automatically.
13
- - Fast left-rail navigation with automatic, readable thread titles.
14
- - Built-in GitHub PR actions (`Open PR` / `Create PR`) from inside Harness.
10
+ - One command palette (`ctrl+p`) to jump threads, run actions, and control workflow quickly.
11
+ - Toggle the bottom debug bar with `cmd+p` when you need runtime launch/auth context.
12
+ - Codex, Claude Code, and Cursor together in one workspace.
13
+ - Diff with Critique, with integrated terminals (`harness diff` + critique actions).
14
+ - Detached gateway sessions keep long-running work alive through reconnects.
15
+ - Storage lifecycle maintenance remains paused in interactive runtime paths; use `harness gateway gc` as a manual offline truncation/compaction escape hatch.
16
+ - Storage lifecycle policy updates from `harness.config.jsonc` still apply without restart.
17
+ - Command palette can open a GitHub thread entry in the left rail for the active project, then show full tracked-branch PR/review details in the main panel.
18
+ - Open the active project directly in local tools (`iTerm2`, `Ghostty`, `Zed`, `Cursor`, `VSCode`, `Warp`, `Finder`) or copy its path from the palette.
19
+ - Command-click links inside conversation terminal output: URLs open in browser, file-like paths open in your configured editor/open-in command.
15
20
 
16
21
  ## Demo
17
22
 
@@ -44,34 +49,25 @@ Use a named session when you want isolated state:
44
49
  harness --session my-session
45
50
  ```
46
51
 
47
- For restart/load diagnostics, use a named session with a non-default gateway port so you do not disrupt your active workspace gateway.
52
+ Named sessions automatically fall back to an available gateway port when the preferred port is already occupied. For deterministic restart/load diagnostics, you can still set an explicit non-default gateway port.
48
53
 
49
- ## Typical workflow
54
+ Standalone diff viewer (phase 1):
50
55
 
51
- 1. Open Harness in your repository.
52
- 2. Start separate threads for implementation and review.
53
- 3. Use `ctrl+p` / `cmd+p` to switch context and run project actions.
54
- 4. Open or create a PR from the same workspace.
55
-
56
- ## User details
57
-
58
- - Thread-scoped command palette (`[+ thread]`) can launch/install supported agent CLIs per project.
59
- - Critique review actions are available from the global palette and run in a terminal thread.
60
- - `ctrl+g` opens the project’s critique thread (or creates one if needed).
61
- - Theme selection is built in (`Set a Theme`) with OpenCode-compatible presets and live preview.
62
- - PR actions use either `GITHUB_TOKEN` or an authenticated `gh` CLI session.
63
-
64
- ## Configuration
65
-
66
- Runtime behavior is controlled by `harness.config.jsonc`.
67
-
68
- Common customizations:
69
-
70
- - Set install commands for `codex`, `claude`, `cursor`, and `critique`.
71
- - Configure critique launch defaults.
72
- - Customize keybindings.
73
- - Choose a theme preset or custom OpenCode-compatible theme file.
74
-
75
- ## License
56
+ ```bash
57
+ harness diff --help
58
+ ```
76
59
 
77
- MIT (`LICENSE`)
60
+ ## Architecture (VTE path)
61
+
62
+ ```mermaid
63
+ flowchart LR
64
+ U[Keyboard + Command Palette] --> UI[Harness TUI]
65
+ UI --> CP[Control Plane Stream API]
66
+ CP --> TM[Thread Manager]
67
+ TM --> AG[Agent Threads<br/>Codex / Claude / Cursor]
68
+ TM --> PTY[Integrated PTY Terminals]
69
+ PTY --> VTE[VTE Parser + Screen Model]
70
+ VTE --> R[Terminal Renderer]
71
+ TM --> DF[harness diff + Critique]
72
+ DF --> R
73
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmoyers/harness",
3
- "version": "0.1.10",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,11 +19,16 @@
19
19
  "scripts/cursor-hook-relay.ts",
20
20
  "scripts/harness-animate.ts",
21
21
  "scripts/harness-bin.js",
22
+ "scripts/harness-commands.ts",
22
23
  "scripts/harness-core.ts",
23
- "scripts/harness-inspector.ts",
24
+ "scripts/harness-runtime.ts",
24
25
  "scripts/harness.ts",
26
+ "scripts/nim-tui-smoke.ts",
25
27
  "packages/harness-ai/src",
26
- "scripts/terminal-recording-gif-lib.ts",
28
+ "packages/harness-ui/src",
29
+ "packages/nim-core/src",
30
+ "packages/nim-ui-core/src",
31
+ "packages/nim-test-tui/src",
27
32
  "scripts/require-bun.js",
28
33
  "native/ptyd/Cargo.lock",
29
34
  "native/ptyd/Cargo.toml",
@@ -35,6 +40,13 @@
35
40
  "publishConfig": {
36
41
  "access": "public"
37
42
  },
43
+ "oclif": {
44
+ "bin": "harness",
45
+ "commands": {
46
+ "strategy": "explicit",
47
+ "target": "scripts/harness-commands.ts"
48
+ }
49
+ },
38
50
  "scripts": {
39
51
  "migrate:bun": "bash ./scripts/migrate-to-bun.sh",
40
52
  "harness": "bun scripts/harness.ts",
@@ -68,28 +80,36 @@
68
80
  "perf:codex:startup:loop": "bun scripts/perf-codex-startup-loop.ts",
69
81
  "loc": "bun scripts/loc-report.ts",
70
82
  "loc:verify": "bun scripts/check-max-loc.ts --max-loc 2000",
71
- "loc:verify:enforce": "bun scripts/check-max-loc.ts --max-loc 2000 --enforce",
83
+ "loc:verify:enforce": "bun scripts/check-max-loc.ts --max-loc 2000 --enforce --baseline harness.max-loc-baseline.json",
72
84
  "deadcode": "bun scripts/check-dead-code.ts",
73
85
  "coverage:check": "bun scripts/check-coverage.ts --lcov .harness/coverage-bun/lcov.info --config harness.coverage.jsonc",
74
86
  "release": "bun scripts/release.ts",
75
- "lint": "oxlint --deny-warnings --disable-unicorn-plugin -c .oxlintrc.json src test scripts packages/harness-ai/src",
76
- "format": "oxfmt --config .oxfmtrc.json src test scripts packages/harness-ai/src",
77
- "format:check": "oxfmt --check --config .oxfmtrc.json src test scripts packages/harness-ai/src",
87
+ "lint": "oxlint --deny-warnings --disable-unicorn-plugin -c .oxlintrc.json src test scripts packages/harness-ai/src packages/harness-ui/src packages/nim-core/src packages/nim-ui-core/src packages/nim-test-tui/src",
88
+ "format": "oxfmt --config .oxfmtrc.json src test scripts packages/harness-ai/src packages/harness-ui/src packages/nim-core/src packages/nim-ui-core/src packages/nim-test-tui/src",
89
+ "format:check": "oxfmt --check --config .oxfmtrc.json src test scripts packages/harness-ai/src packages/harness-ui/src packages/nim-core/src packages/nim-ui-core/src packages/nim-test-tui/src",
78
90
  "typecheck": "tsc --noEmit",
79
- "test": "bun run build:ptyd && bun test",
91
+ "test": "bun run build:ptyd && bun test --concurrent",
92
+ "test:serial": "bun run build:ptyd && bun test",
80
93
  "test:integration:codex-status": "bun scripts/integration-codex-status-sequence.ts",
81
94
  "test:integration:codex-status:long": "bun scripts/integration-codex-status-sequence.ts --timeout-ms 180000 --prompt \"Write three short poems titled Dawn, Voltage, and Orbit. Before each poem, perform one repository inspection action and include one factual line from that action. Use at least three total tool actions and do not edit any files.\"",
82
95
  "test:integration:agent-prompts": "bun scripts/integration-agent-prompt-parity.ts",
83
96
  "test:integration:agent-prompts:long": "bun scripts/integration-agent-prompt-parity.ts --timeout-ms 180000",
97
+ "test:integration:nim:haiku": "bun scripts/integration-nim-haiku.ts",
98
+ "nim:tui": "bun scripts/nim-tui-smoke.ts",
84
99
  "smoke:harness-ai": "bun scripts/harness-ai-smoke.ts",
85
100
  "smoke:harness-ai:parity": "bun scripts/harness-ai-parity-smoke.mts",
86
- "test:coverage": "bun run build:ptyd && bun test --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
101
+ "test:coverage": "bun run build:ptyd && bun test --concurrent --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
102
+ "test:coverage:serial": "bun run build:ptyd && bun test --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
87
103
  "verify": "bun run format:check && bun run lint && bun run typecheck && bun run deadcode && bun run test:coverage",
88
- "verify:strict": "bun run verify && bun run loc:verify:enforce"
104
+ "verify:coverage-gate": "bun run format:check && bun run lint && bun run typecheck && bun run deadcode && bun run test:coverage",
105
+ "verify:strict": "bun run verify:coverage-gate && bun run loc:verify:enforce"
89
106
  },
90
107
  "dependencies": {
108
+ "@linear/sdk": "^75.0.0",
91
109
  "@napi-rs/canvas": "^0.1.92",
92
- "gifenc": "^1.0.3"
110
+ "@oclif/core": "^4.8.0",
111
+ "gifenc": "^1.0.3",
112
+ "zustand": "^5.0.11"
93
113
  },
94
114
  "devDependencies": {
95
115
  "@ai-sdk/anthropic": "^3.0.45",
@@ -308,79 +308,79 @@ function parseContentBlock(value: unknown): AnthropicContentBlock | null {
308
308
  };
309
309
  }
310
310
 
311
- if (type === 'web_fetch_tool_result') {
312
- const toolUseId = asString(record['tool_use_id']);
313
- const contentRecord = asRecord(record['content']);
314
- if (toolUseId === null || contentRecord === null) {
311
+ if (type !== 'web_fetch_tool_result') {
312
+ return null;
313
+ }
314
+
315
+ const toolUseId = asString(record['tool_use_id']);
316
+ const contentRecord = asRecord(record['content']);
317
+ if (toolUseId === null || contentRecord === null) {
318
+ return null;
319
+ }
320
+
321
+ const contentType = asString(contentRecord['type']);
322
+ if (contentType === null) {
323
+ return null;
324
+ }
325
+
326
+ if (contentType === 'web_fetch_result') {
327
+ const innerContent = asRecord(contentRecord['content']);
328
+ const source = innerContent === null ? null : asRecord(innerContent['source']);
329
+ if (innerContent === null || source === null) {
315
330
  return null;
316
331
  }
317
332
 
318
- const contentType = asString(contentRecord['type']);
319
- if (contentType === null) {
333
+ const url = asString(contentRecord['url']);
334
+ const sourceType = asString(source['type']);
335
+ const sourceMediaType = asString(source['media_type']);
336
+ const sourceData = asString(source['data']);
337
+ const contentBlockType = asString(innerContent['type']);
338
+ if (
339
+ url === null ||
340
+ sourceType === null ||
341
+ sourceMediaType === null ||
342
+ sourceData === null ||
343
+ contentBlockType === null
344
+ ) {
320
345
  return null;
321
346
  }
322
347
 
323
- if (contentType === 'web_fetch_result') {
324
- const innerContent = asRecord(contentRecord['content']);
325
- const source = innerContent === null ? null : asRecord(innerContent['source']);
326
- if (innerContent === null || source === null) {
327
- return null;
328
- }
329
-
330
- const url = asString(contentRecord['url']);
331
- const sourceType = asString(source['type']);
332
- const sourceMediaType = asString(source['media_type']);
333
- const sourceData = asString(source['data']);
334
- const contentBlockType = asString(innerContent['type']);
335
- if (
336
- url === null ||
337
- sourceType === null ||
338
- sourceMediaType === null ||
339
- sourceData === null ||
340
- contentBlockType === null
341
- ) {
342
- return null;
343
- }
344
-
345
- const retrievedAt = asString(contentRecord['retrieved_at']);
346
- const title = asString(innerContent['title']);
347
- const citations = Array.isArray(innerContent['citations'])
348
- ? (innerContent['citations'] as unknown[])
349
- : null;
350
-
351
- return {
352
- type,
353
- tool_use_id: toolUseId,
354
- content: {
355
- type: 'web_fetch_result',
356
- url,
357
- ...(retrievedAt !== null ? { retrieved_at: retrievedAt } : {}),
358
- content: {
359
- type: contentBlockType,
360
- ...(title !== null ? { title } : {}),
361
- source: {
362
- type: sourceType,
363
- media_type: sourceMediaType,
364
- data: sourceData,
365
- },
366
- ...(citations !== null ? { citations } : {}),
367
- },
368
- },
369
- };
370
- }
348
+ const retrievedAt = asString(contentRecord['retrieved_at']);
349
+ const title = asString(innerContent['title']);
350
+ const citations = Array.isArray(innerContent['citations'])
351
+ ? (innerContent['citations'] as unknown[])
352
+ : null;
371
353
 
372
- const errorCode = asString(contentRecord['error_code']);
373
354
  return {
374
355
  type,
375
356
  tool_use_id: toolUseId,
376
357
  content: {
377
- type: contentType,
378
- ...(errorCode !== null ? { error_code: errorCode } : {}),
358
+ type: 'web_fetch_result',
359
+ url,
360
+ ...(retrievedAt !== null ? { retrieved_at: retrievedAt } : {}),
361
+ content: {
362
+ type: contentBlockType,
363
+ ...(title !== null ? { title } : {}),
364
+ source: {
365
+ type: sourceType,
366
+ media_type: sourceMediaType,
367
+ data: sourceData,
368
+ },
369
+ ...(citations !== null ? { citations } : {}),
370
+ },
379
371
  },
380
372
  };
381
373
  }
382
374
 
383
- return null;
375
+ const errorCode = asString(contentRecord['error_code']);
376
+ return {
377
+ type,
378
+ tool_use_id: toolUseId,
379
+ content: {
380
+ type: contentType,
381
+ ...(errorCode !== null ? { error_code: errorCode } : {}),
382
+ },
383
+ };
384
384
  }
385
385
 
386
386
  export function parseAnthropicStreamChunk(value: unknown): AnthropicStreamChunk | null {
@@ -550,18 +550,18 @@ export function parseAnthropicStreamChunk(value: unknown): AnthropicStreamChunk
550
550
  };
551
551
  }
552
552
 
553
- if (type === 'error') {
554
- const errorRecord = asRecord(record['error']);
555
- if (errorRecord === null) {
556
- return null;
557
- }
558
- return {
559
- type,
560
- error: errorRecord,
561
- };
553
+ if (type !== 'error') {
554
+ return null;
562
555
  }
563
556
 
564
- return null;
557
+ const errorRecord = asRecord(record['error']);
558
+ if (errorRecord === null) {
559
+ return null;
560
+ }
561
+ return {
562
+ type,
563
+ error: errorRecord,
564
+ };
565
565
  }
566
566
 
567
567
  export function mapAnthropicStopReason(reason: string | null | undefined): FinishReason {
@@ -37,31 +37,6 @@ import type {
37
37
  TypedToolResult,
38
38
  } from './types.ts';
39
39
 
40
- interface Deferred<T> {
41
- readonly promise: Promise<T>;
42
- resolve(value: T): void;
43
- reject(error: unknown): void;
44
- }
45
-
46
- function createDeferred<T>(): Deferred<T> {
47
- let resolveFn: ((value: T) => void) | null = null;
48
- let rejectFn: ((error: unknown) => void) | null = null;
49
- const promise = new Promise<T>((resolve, reject) => {
50
- resolveFn = resolve;
51
- rejectFn = reject;
52
- });
53
-
54
- return {
55
- promise,
56
- resolve(value) {
57
- resolveFn?.(value);
58
- },
59
- reject(error) {
60
- rejectFn?.(error);
61
- },
62
- };
63
- }
64
-
65
40
  interface ContentBlockTextState {
66
41
  readonly kind: 'text' | 'reasoning';
67
42
  readonly id: string;
@@ -144,10 +119,6 @@ function normalizeMessages(
144
119
  }
145
120
 
146
121
  function toAnthropicMessageContent(message: ModelMessage): Array<Record<string, unknown>> {
147
- if (message.role === 'system') {
148
- return [{ type: 'text', text: message.content }];
149
- }
150
-
151
122
  if (typeof message.content === 'string') {
152
123
  return [{ type: 'text', text: message.content }];
153
124
  }
@@ -319,27 +290,15 @@ interface StepRunResult<TOOLS extends ToolSet> {
319
290
  readonly assistantText: string;
320
291
  }
321
292
 
322
- function asRecord(value: unknown): Record<string, unknown> | null {
323
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
324
- return null;
325
- }
326
- return value as Record<string, unknown>;
327
- }
328
-
329
- function mapWebSearchResult(result: AnthropicContentBlock):
293
+ function mapWebSearchResult(
294
+ result: Extract<AnthropicContentBlock, { type: 'web_search_tool_result' }>,
295
+ ):
330
296
  | {
331
297
  readonly ok: true;
332
298
  readonly output: unknown[];
333
299
  readonly sources: Array<{ url: string; title?: string; pageAge?: string }>;
334
300
  }
335
301
  | { readonly ok: false; readonly error: unknown } {
336
- if (result.type !== 'web_search_tool_result') {
337
- return {
338
- ok: false,
339
- error: { message: 'invalid web search result payload' },
340
- };
341
- }
342
-
343
302
  if (Array.isArray(result.content)) {
344
303
  const mapped = result.content.map((entry) => ({
345
304
  type: entry.type,
@@ -371,17 +330,10 @@ function mapWebSearchResult(result: AnthropicContentBlock):
371
330
  }
372
331
 
373
332
  function mapWebFetchResult(
374
- result: AnthropicContentBlock,
333
+ result: Extract<AnthropicContentBlock, { type: 'web_fetch_tool_result' }>,
375
334
  ):
376
335
  | { readonly ok: true; readonly output: unknown }
377
336
  | { readonly ok: false; readonly error: unknown } {
378
- if (result.type !== 'web_fetch_tool_result') {
379
- return {
380
- ok: false,
381
- error: { message: 'invalid web fetch result payload' },
382
- };
383
- }
384
-
385
337
  if ('url' in result.content && 'content' in result.content) {
386
338
  const content = result.content.content;
387
339
  return {
@@ -718,10 +670,7 @@ async function runSingleStep<TOOLS extends ToolSet>(
718
670
  providerResults.push(error);
719
671
  options.emit({ type: 'tool-error', ...error });
720
672
  }
721
- continue;
722
673
  }
723
-
724
- continue;
725
674
  }
726
675
 
727
676
  if (chunk.type === 'content_block_delta') {
@@ -798,8 +747,7 @@ async function runSingleStep<TOOLS extends ToolSet>(
798
747
  }
799
748
 
800
749
  if (chunk.type === 'error') {
801
- const errorRecord = asRecord(chunk.error);
802
- const message = errorRecord?.['message'];
750
+ const message = chunk.error['message'];
803
751
  options.emit({
804
752
  type: 'error',
805
753
  error:
@@ -809,9 +757,7 @@ async function runSingleStep<TOOLS extends ToolSet>(
809
757
  continue;
810
758
  }
811
759
 
812
- if (chunk.type === 'message_stop') {
813
- break;
814
- }
760
+ if (chunk.type === 'message_stop') break;
815
761
  }
816
762
  } finally {
817
763
  reader.releaseLock();
@@ -1262,41 +1208,17 @@ export function streamText<TOOLS extends ToolSet>(
1262
1208
  const uiMessageStream = createUIMessageStream(uiBranchA);
1263
1209
  const uiResponseStream = createUIMessageStream(uiBranchB);
1264
1210
 
1265
- const textDeferred = createDeferred<string>();
1266
- const toolCallsDeferred = createDeferred<TypedToolCall<TOOLS>[]>();
1267
- const toolResultsDeferred =
1268
- createDeferred<Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>>>();
1269
- const finishReasonDeferred = createDeferred<FinishReason>();
1270
- const usageDeferred = createDeferred<LanguageModelUsage>();
1271
- const responseDeferred = createDeferred<LanguageModelResponseMetadata>();
1272
-
1273
- collectResultFromStream(collectorBranch)
1274
- .then((collected) => {
1275
- textDeferred.resolve(collected.text);
1276
- toolCallsDeferred.resolve(collected.toolCalls);
1277
- toolResultsDeferred.resolve(collected.toolResults);
1278
- finishReasonDeferred.resolve(collected.finishReason);
1279
- usageDeferred.resolve(collected.usage);
1280
- responseDeferred.resolve(collected.response);
1281
- })
1282
- .catch((error) => {
1283
- textDeferred.reject(error);
1284
- toolCallsDeferred.reject(error);
1285
- toolResultsDeferred.reject(error);
1286
- finishReasonDeferred.reject(error);
1287
- usageDeferred.reject(error);
1288
- responseDeferred.reject(error);
1289
- });
1211
+ const collectedPromise = collectResultFromStream(collectorBranch);
1290
1212
 
1291
1213
  return {
1292
1214
  fullStream,
1293
1215
  textStream,
1294
- text: textDeferred.promise,
1295
- toolCalls: toolCallsDeferred.promise,
1296
- toolResults: toolResultsDeferred.promise,
1297
- finishReason: finishReasonDeferred.promise,
1298
- usage: usageDeferred.promise,
1299
- response: responseDeferred.promise,
1216
+ text: collectedPromise.then((collected) => collected.text),
1217
+ toolCalls: collectedPromise.then((collected) => collected.toolCalls),
1218
+ toolResults: collectedPromise.then((collected) => collected.toolResults),
1219
+ finishReason: collectedPromise.then((collected) => collected.finishReason),
1220
+ usage: collectedPromise.then((collected) => collected.usage),
1221
+ response: collectedPromise.then((collected) => collected.response),
1300
1222
  toUIMessageStream() {
1301
1223
  return uiMessageStream;
1302
1224
  },
@@ -0,0 +1,158 @@
1
+ export interface RenderCursorStyle {
2
+ readonly shape: 'block' | 'underline' | 'bar';
3
+ readonly blinking: boolean;
4
+ }
5
+
6
+ interface DiffRenderedRowsResult {
7
+ readonly output: string;
8
+ readonly nextRows: readonly string[];
9
+ readonly changedRows: readonly number[];
10
+ }
11
+
12
+ export function diffRenderedRows(
13
+ currentRows: readonly string[],
14
+ previousRows: readonly string[],
15
+ ): DiffRenderedRowsResult {
16
+ const changedRows: number[] = [];
17
+ let output = '';
18
+ const rowCount = Math.max(currentRows.length, previousRows.length);
19
+ const nextRows: string[] = [];
20
+ for (let row = 0; row < rowCount; row += 1) {
21
+ const current = currentRows[row] ?? '';
22
+ const previous = previousRows[row] ?? '';
23
+ nextRows.push(current);
24
+ if (current === previous) {
25
+ continue;
26
+ }
27
+ changedRows.push(row);
28
+ output += `\u001b[${String(row + 1)};1H\u001b[2K${current}`;
29
+ }
30
+ return {
31
+ output,
32
+ nextRows,
33
+ changedRows,
34
+ };
35
+ }
36
+
37
+ export function cursorStyleToDecscusr(style: RenderCursorStyle): string {
38
+ if (style.shape === 'block') {
39
+ return style.blinking ? '\u001b[1 q' : '\u001b[2 q';
40
+ }
41
+ if (style.shape === 'underline') {
42
+ return style.blinking ? '\u001b[3 q' : '\u001b[4 q';
43
+ }
44
+ return style.blinking ? '\u001b[5 q' : '\u001b[6 q';
45
+ }
46
+
47
+ export function cursorStyleEqual(
48
+ left: RenderCursorStyle | null,
49
+ right: RenderCursorStyle,
50
+ ): boolean {
51
+ if (left === null) {
52
+ return false;
53
+ }
54
+ return left.shape === right.shape && left.blinking === right.blinking;
55
+ }
56
+
57
+ type ScanResult =
58
+ | {
59
+ readonly valid: true;
60
+ }
61
+ | {
62
+ readonly valid: false;
63
+ readonly reason: string;
64
+ };
65
+
66
+ function scanAnsiText(text: string): ScanResult {
67
+ let index = 0;
68
+ while (index < text.length) {
69
+ const code = text.codePointAt(index)!;
70
+ const char = String.fromCodePoint(code);
71
+ const width = code > 0xffff ? 2 : 1;
72
+ if (char !== '\u001b') {
73
+ index += width;
74
+ continue;
75
+ }
76
+
77
+ const next = text[index + 1];
78
+ if (next === undefined) {
79
+ return {
80
+ valid: false,
81
+ reason: 'dangling ESC at end of row',
82
+ };
83
+ }
84
+
85
+ if (next === '[') {
86
+ let csiIndex = index + 2;
87
+ let foundFinal = false;
88
+ while (csiIndex < text.length) {
89
+ const csiCode = text.codePointAt(csiIndex)!;
90
+ if (csiCode >= 0x40 && csiCode <= 0x7e) {
91
+ foundFinal = true;
92
+ csiIndex += 1;
93
+ break;
94
+ }
95
+ if (csiCode < 0x20 || csiCode > 0x3f) {
96
+ return {
97
+ valid: false,
98
+ reason: `invalid CSI byte 0x${csiCode.toString(16)}`,
99
+ };
100
+ }
101
+ csiIndex += 1;
102
+ }
103
+ if (!foundFinal) {
104
+ return {
105
+ valid: false,
106
+ reason: 'unterminated CSI sequence',
107
+ };
108
+ }
109
+ index = csiIndex;
110
+ continue;
111
+ }
112
+
113
+ if (next === ']') {
114
+ let oscIndex = index + 2;
115
+ let terminated = false;
116
+ while (oscIndex < text.length) {
117
+ const oscCode = text.codePointAt(oscIndex)!;
118
+ if (oscCode === 0x07) {
119
+ terminated = true;
120
+ oscIndex += 1;
121
+ break;
122
+ }
123
+ if (oscCode === 0x1b && text[oscIndex + 1] === '\\') {
124
+ terminated = true;
125
+ oscIndex += 2;
126
+ break;
127
+ }
128
+ oscIndex += oscCode > 0xffff ? 2 : 1;
129
+ }
130
+ if (!terminated) {
131
+ return {
132
+ valid: false,
133
+ reason: 'unterminated OSC sequence',
134
+ };
135
+ }
136
+ index = oscIndex;
137
+ continue;
138
+ }
139
+
140
+ index += 2;
141
+ }
142
+
143
+ return {
144
+ valid: true,
145
+ };
146
+ }
147
+
148
+ export function findAnsiIntegrityIssues(rows: readonly string[]): readonly string[] {
149
+ const issues: string[] = [];
150
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
151
+ const row = rows[rowIndex] ?? '';
152
+ const result = scanAnsiText(row);
153
+ if (!result.valid) {
154
+ issues.push(`row ${String(rowIndex + 1)}: ${result.reason}`);
155
+ }
156
+ }
157
+ return issues;
158
+ }