@xingular/harness 0.1.5 → 0.1.7

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 (133) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/NOTICE.md +38 -0
  3. package/README.md +118 -26
  4. package/dist/cli.js +6106 -912
  5. package/dist/terminal-pi.js +462 -11
  6. package/docs/README.md +12 -0
  7. package/docs/decisions/ADR-0001-owned-terminal.md +87 -0
  8. package/docs/evidence/terminal-2026-10-17/120x40-completed-transcript.ansi.gz +0 -0
  9. package/docs/evidence/terminal-2026-10-17/40x20-settings-interaction.ansi.gz +0 -0
  10. package/docs/evidence/terminal-2026-10-17/40x20-settings-keyboard.ansi.gz +0 -0
  11. package/docs/evidence/terminal-2026-10-17/40x20-welcome.ansi.gz +0 -0
  12. package/docs/evidence/terminal-2026-10-17/80x24-approval-0.ansi.gz +0 -0
  13. package/docs/evidence/terminal-2026-10-17/80x24-question.ansi.gz +0 -0
  14. package/docs/evidence/terminal-2026-10-17/verification.json +130 -0
  15. package/docs/evidence/terminal-2026-10-17-followup/120x40-completed-transcript.ansi.gz +0 -0
  16. package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-interaction.ansi.gz +0 -0
  17. package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-keyboard.ansi.gz +0 -0
  18. package/docs/evidence/terminal-2026-10-17-followup/40x20-welcome.ansi.gz +0 -0
  19. package/docs/evidence/terminal-2026-10-17-followup/80x24-approval-0.ansi.gz +0 -0
  20. package/docs/evidence/terminal-2026-10-17-followup/80x24-plugin-light.ansi.gz +0 -0
  21. package/docs/evidence/terminal-2026-10-17-followup/80x24-question-batch.ansi.gz +0 -0
  22. package/docs/evidence/terminal-2026-10-17-followup/80x24-question.ansi.gz +0 -0
  23. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-approval-0.ansi.gz +0 -0
  24. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-plugin-light.ansi.gz +0 -0
  25. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question-batch.ansi.gz +0 -0
  26. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question.ansi.gz +0 -0
  27. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/verification.json +95 -0
  28. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-approval-0.ansi.gz +0 -0
  29. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-plugin-light.ansi.gz +0 -0
  30. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question-batch.ansi.gz +0 -0
  31. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question.ansi.gz +0 -0
  32. package/docs/evidence/terminal-2026-10-17-followup/global-install/README.md +27 -0
  33. package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command-80x24.ansi.gz +0 -0
  34. package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command.json +18 -0
  35. package/docs/evidence/terminal-2026-10-17-followup/global-install/verification.json +95 -0
  36. package/docs/evidence/terminal-2026-10-17-followup/harness-reference-80x24.json.gz +0 -0
  37. package/docs/evidence/terminal-2026-10-17-followup/harness-reference-final-80x24.json.gz +0 -0
  38. package/docs/evidence/terminal-2026-10-17-followup/harness-reference-scenario.json +60 -0
  39. package/docs/evidence/terminal-2026-10-17-followup/live-codex/README.md +24 -0
  40. package/docs/evidence/terminal-2026-10-17-followup/live-codex/complete-80x24.ansi.gz +0 -0
  41. package/docs/evidence/terminal-2026-10-17-followup/live-codex/verification.json +57 -0
  42. package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24-early.json.gz +0 -0
  43. package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24.json.gz +0 -0
  44. package/docs/evidence/terminal-2026-10-17-followup/omp-reference-scenario.json +58 -0
  45. package/docs/evidence/terminal-2026-10-17-followup/omp-supplemental-static.json.gz +0 -0
  46. package/docs/evidence/terminal-2026-10-17-followup/rea-artifacts.json +320 -0
  47. package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.json +30 -0
  48. package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.md +133 -0
  49. package/docs/evidence/terminal-2026-10-17-followup/reference-source-observations.json +20 -0
  50. package/docs/evidence/terminal-2026-10-17-followup/verification.json +207 -0
  51. package/docs/terminal-tui.md +254 -0
  52. package/package.json +8 -2
  53. package/src/accounts/accounts.test.ts +110 -0
  54. package/src/accounts/accounts.ts +146 -10
  55. package/src/cli.ts +7 -3
  56. package/src/harness.ts +16 -3
  57. package/src/hub/catalog.json +123 -0
  58. package/src/hub/cli.ts +59 -0
  59. package/src/hub/hub.test.ts +205 -0
  60. package/src/hub/hub.ts +256 -0
  61. package/src/hub/schema.ts +81 -0
  62. package/src/index.ts +6 -1
  63. package/src/memory/api.ts +16 -2
  64. package/src/memory/blocks.ts +3 -1
  65. package/src/memory/caller.ts +13 -1
  66. package/src/memory/curation.test.ts +163 -0
  67. package/src/memory/curation.ts +64 -0
  68. package/src/memory/maintenance.ts +199 -11
  69. package/src/memory/operations.ts +12 -0
  70. package/src/memory/proposals.ts +16 -8
  71. package/src/memory/recall.test.ts +11 -0
  72. package/src/memory/recall.ts +9 -2
  73. package/src/server/methods.ts +37 -2
  74. package/src/sessions/contracts.ts +29 -3
  75. package/src/sessions/conversation.test.ts +27 -0
  76. package/src/sessions/policy.ts +4 -0
  77. package/src/sessions/questionBatches.test.ts +110 -0
  78. package/src/sessions/registry.ts +109 -22
  79. package/src/sessions/runner.ts +36 -1
  80. package/src/sessions/sessions.test.ts +95 -1
  81. package/src/sessions/sessions.ts +53 -9
  82. package/src/skills/registry.ts +16 -10
  83. package/src/terminal/accountsMenu.test.ts +225 -0
  84. package/src/terminal/accountsMenu.ts +382 -0
  85. package/src/terminal/control.test.ts +23 -0
  86. package/src/terminal/control.ts +23 -0
  87. package/src/terminal/controller.ts +68 -4
  88. package/src/terminal/hubMenu.ts +24 -0
  89. package/src/terminal/launch.test.ts +5 -14
  90. package/src/terminal/launch.ts +2 -42
  91. package/src/terminal/piExtension.lifecycle.test.ts +55 -0
  92. package/src/terminal/piExtension.ts +50 -1
  93. package/src/terminal/socket.ts +13 -2
  94. package/src/terminal/standalone.test.ts +37 -0
  95. package/src/terminal/standalone.ts +16 -7
  96. package/src/terminal/terminal.test.ts +70 -1
  97. package/src/terminal/terminals.ts +122 -12
  98. package/src/terminal/tui/application.test.ts +114 -0
  99. package/src/terminal/tui/application.ts +491 -0
  100. package/src/terminal/tui/commands.test.ts +218 -0
  101. package/src/terminal/tui/commands.ts +361 -0
  102. package/src/terminal/tui/dialogs.ts +301 -0
  103. package/src/terminal/tui/editor.test.ts +84 -0
  104. package/src/terminal/tui/editor.ts +79 -0
  105. package/src/terminal/tui/keybindings.test.ts +22 -0
  106. package/src/terminal/tui/keybindings.ts +50 -0
  107. package/src/terminal/tui/piAdapter.ts +62 -0
  108. package/src/terminal/tui/presentation.test.ts +79 -0
  109. package/src/terminal/tui/questions.test.ts +41 -0
  110. package/src/terminal/tui/questions.ts +47 -0
  111. package/src/terminal/tui/runtime.socket.test.ts +104 -0
  112. package/src/terminal/tui/runtime.test.ts +215 -0
  113. package/src/terminal/tui/runtime.ts +388 -0
  114. package/src/terminal/tui/sessionUi.ts +24 -0
  115. package/src/terminal/tui/settings.test.ts +150 -0
  116. package/src/terminal/tui/settings.ts +188 -0
  117. package/src/terminal/tui/status.test.ts +70 -0
  118. package/src/terminal/tui/status.ts +109 -0
  119. package/src/terminal/tui/theme.ts +174 -0
  120. package/src/terminal/tui/tools.test.ts +145 -0
  121. package/src/terminal/tui/tools.ts +142 -0
  122. package/src/terminal/tui/transcript.test.ts +25 -0
  123. package/src/terminal/tui/transcript.ts +124 -0
  124. package/src/terminal/tui/ui.test.ts +313 -0
  125. package/src/terminal/tui/ui.ts +166 -0
  126. package/src/terminal/tui/welcome.ts +102 -0
  127. package/src/tools/askUser.test.ts +69 -0
  128. package/src/tools/askUser.ts +31 -17
  129. package/src/tools/maintenance.ts +24 -0
  130. package/src/tools/memoryTools.ts +1 -1
  131. package/src/tools/registryWrite.test.ts +101 -0
  132. package/src/tools/registryWrite.ts +182 -0
  133. package/src/tools/spawnSubagent.ts +92 -0
@@ -0,0 +1,207 @@
1
+ {
2
+ "passed": true,
3
+ "implementation": "production standalone CLI + real Pi SDK + loopback OpenAI SSE transport",
4
+ "cli": "/Users/tanny/Documents/Projects/Xingular/Xingular Agent/Harness/dist/cli.js",
5
+ "cliSha256": "2f5df943661a1e89440d0e81d59cd4a5bdb7aaf087a2a930e232b6743d5db093",
6
+ "verifierSha256": "6b7652f0093b12a37bd975fcffa7accc0f34d627022397b7e45923be270abc85",
7
+ "provider": "controlled loopback OpenAI-compatible streaming transport; no external model requests or user credentials",
8
+ "requestCount": 39,
9
+ "profiles": [
10
+ {
11
+ "size": "40x20",
12
+ "checks": [
13
+ "welcome and real configured model",
14
+ "actual settings tabs, section focus, keyboard search/control and Escape return",
15
+ "command autocomplete and real accounts selector",
16
+ "suspend/resume restores cooked/raw terminal modes and command input",
17
+ "Unicode, real streaming/thinking/read/bash/approval/question, expansion, resizing and scrolling",
18
+ "trusted plugin custom overlay, widget, status and disposal",
19
+ "grouped question progress and atomic choice/text answers through real registry",
20
+ "approval deny and Escape cancellation prevent actual tool execution",
21
+ "question Escape cancellation settles pending tools and interrupts run",
22
+ "new session, explicit scoped resume and session naming",
23
+ "actual stream cancellation and interrupted run record",
24
+ "explicit account switching, process restart and real run on newly pinned account",
25
+ "fatal asynchronous plugin failure exits 1 and restores terminal modes",
26
+ "plugin startup failure exits 1 and restores terminal modes"
27
+ ],
28
+ "persistedRunCount": 7,
29
+ "exitCode": 0,
30
+ "termiosRestored": true,
31
+ "bracketedPasteDisabled": true,
32
+ "cursorShown": true,
33
+ "fatalCleanup": {
34
+ "exitCode": 1,
35
+ "termiosRestored": true,
36
+ "bracketedPasteDisabled": true,
37
+ "cursorShown": true
38
+ },
39
+ "startupCleanup": {
40
+ "exitCode": 1,
41
+ "termiosRestored": true,
42
+ "bracketedPasteDisabled": true,
43
+ "cursorShown": true
44
+ }
45
+ },
46
+ {
47
+ "size": "80x24",
48
+ "checks": [
49
+ "welcome and real configured model",
50
+ "actual settings tabs, section focus, keyboard search/control and Escape return",
51
+ "command autocomplete and real accounts selector",
52
+ "suspend/resume restores cooked/raw terminal modes and command input",
53
+ "Unicode, real streaming/thinking/read/bash/approval/question, expansion, resizing and scrolling",
54
+ "trusted plugin custom overlay, widget, status and disposal",
55
+ "grouped question progress and atomic choice/text answers through real registry",
56
+ "approval deny and Escape cancellation prevent actual tool execution",
57
+ "question Escape cancellation settles pending tools and interrupts run",
58
+ "new session, explicit scoped resume and session naming",
59
+ "actual stream cancellation and interrupted run record",
60
+ "explicit account switching, process restart and real run on newly pinned account",
61
+ "fatal asynchronous plugin failure exits 1 and restores terminal modes",
62
+ "plugin startup failure exits 1 and restores terminal modes"
63
+ ],
64
+ "persistedRunCount": 7,
65
+ "exitCode": 0,
66
+ "termiosRestored": true,
67
+ "bracketedPasteDisabled": true,
68
+ "cursorShown": true,
69
+ "fatalCleanup": {
70
+ "exitCode": 1,
71
+ "termiosRestored": true,
72
+ "bracketedPasteDisabled": true,
73
+ "cursorShown": true
74
+ },
75
+ "startupCleanup": {
76
+ "exitCode": 1,
77
+ "termiosRestored": true,
78
+ "bracketedPasteDisabled": true,
79
+ "cursorShown": true
80
+ }
81
+ },
82
+ {
83
+ "size": "120x40",
84
+ "checks": [
85
+ "welcome and real configured model",
86
+ "actual settings tabs, section focus, keyboard search/control and Escape return",
87
+ "command autocomplete and real accounts selector",
88
+ "suspend/resume restores cooked/raw terminal modes and command input",
89
+ "Unicode, real streaming/thinking/read/bash/approval/question, expansion, resizing and scrolling",
90
+ "trusted plugin custom overlay, widget, status and disposal",
91
+ "grouped question progress and atomic choice/text answers through real registry",
92
+ "approval deny and Escape cancellation prevent actual tool execution",
93
+ "question Escape cancellation settles pending tools and interrupts run",
94
+ "new session, explicit scoped resume and session naming",
95
+ "actual stream cancellation and interrupted run record",
96
+ "explicit account switching, process restart and real run on newly pinned account",
97
+ "fatal asynchronous plugin failure exits 1 and restores terminal modes",
98
+ "plugin startup failure exits 1 and restores terminal modes"
99
+ ],
100
+ "persistedRunCount": 7,
101
+ "exitCode": 0,
102
+ "termiosRestored": true,
103
+ "bracketedPasteDisabled": true,
104
+ "cursorShown": true,
105
+ "fatalCleanup": {
106
+ "exitCode": 1,
107
+ "termiosRestored": true,
108
+ "bracketedPasteDisabled": true,
109
+ "cursorShown": true
110
+ },
111
+ "startupCleanup": {
112
+ "exitCode": 1,
113
+ "termiosRestored": true,
114
+ "bracketedPasteDisabled": true,
115
+ "cursorShown": true
116
+ }
117
+ }
118
+ ],
119
+ "captures": [
120
+ {
121
+ "file": "40x20-welcome.ansi.gz",
122
+ "kind": "actual PTY cumulative raw ANSI stream",
123
+ "columns": 40,
124
+ "rows": 20,
125
+ "rawBytes": 3085,
126
+ "rawSha256": "5ad9d6b7743730c00331865a713ade17a96d8a82254073eff8f7123f78ba0dfd",
127
+ "archiveBytes": 647,
128
+ "archiveSha256": "4e1d09e9e2b8ab6dd716c27342cb427e6d327b4517b2d5a1f9d6abe507ae6150"
129
+ },
130
+ {
131
+ "file": "40x20-settings-interaction.ansi.gz",
132
+ "kind": "actual PTY cumulative raw ANSI stream",
133
+ "columns": 40,
134
+ "rows": 20,
135
+ "rawBytes": 10579,
136
+ "rawSha256": "6e8cb197fd6444abd5b34500d14d36def96d945f6ffe75b70c107511a2fc6f92",
137
+ "archiveBytes": 1250,
138
+ "archiveSha256": "33227bf1354591624311e69f016862cea964aeb0d2cf6af0274bc8c49b6aa39c"
139
+ },
140
+ {
141
+ "file": "40x20-settings-keyboard.ansi.gz",
142
+ "kind": "actual PTY cumulative raw ANSI stream",
143
+ "columns": 40,
144
+ "rows": 20,
145
+ "rawBytes": 12779,
146
+ "rawSha256": "6071b778e316c86fdc6976ac024faffcf75221ba1043dfbd524e32cfcdff5e79",
147
+ "archiveBytes": 1387,
148
+ "archiveSha256": "722b555638b343520b7896527fb9287c71a2a9ba55e9a2f7c1ea573058db3a22"
149
+ },
150
+ {
151
+ "file": "80x24-approval-0.ansi.gz",
152
+ "kind": "actual PTY cumulative raw ANSI stream",
153
+ "columns": 80,
154
+ "rows": 24,
155
+ "rawBytes": 63082,
156
+ "rawSha256": "d182c53020bc8f9dae3b3f910ab78bbccae229b41fb927983b26b9352e13c7d1",
157
+ "archiveBytes": 5420,
158
+ "archiveSha256": "c00dd2d226ff8d0d8455311b903791dff1a4fc2b9fc4921ffc08837290570c99"
159
+ },
160
+ {
161
+ "file": "80x24-question.ansi.gz",
162
+ "kind": "actual PTY cumulative raw ANSI stream",
163
+ "columns": 80,
164
+ "rows": 24,
165
+ "rawBytes": 73627,
166
+ "rawSha256": "50c7fc4da8748ddcd2868007cf944e2294a9d55d035c60ad30cbe8820068682e",
167
+ "archiveBytes": 5834,
168
+ "archiveSha256": "d5371974389a6f99758d3d4e3bc820a69a48aa05fe344b3d0f4d3143cdd44298"
169
+ },
170
+ {
171
+ "file": "80x24-plugin-light.ansi.gz",
172
+ "kind": "actual PTY cumulative raw ANSI stream",
173
+ "columns": 120,
174
+ "rows": 30,
175
+ "rawBytes": 118891,
176
+ "rawSha256": "089d74da63cf763e1a06faf0264966d5b8d788518eda880ed91fb556fe844ab5",
177
+ "archiveBytes": 7618,
178
+ "archiveSha256": "55fe9f7742387b7e10692711daef697fe1f55af76486554c40290eb039c4a1fa"
179
+ },
180
+ {
181
+ "file": "80x24-question-batch.ansi.gz",
182
+ "kind": "actual PTY cumulative raw ANSI stream",
183
+ "columns": 120,
184
+ "rows": 30,
185
+ "rawBytes": 169983,
186
+ "rawSha256": "aee0560c4fb6bb878d187827ab8c40ceee81dda1022a98f663f8bb079e9bc2f6",
187
+ "archiveBytes": 8742,
188
+ "archiveSha256": "11ae7c4c7b3991014dbef17d126608104661c0776198e51e519ba35ad557dd35"
189
+ },
190
+ {
191
+ "file": "120x40-completed-transcript.ansi.gz",
192
+ "kind": "actual PTY cumulative raw ANSI stream",
193
+ "columns": 120,
194
+ "rows": 40,
195
+ "rawBytes": 140149,
196
+ "rawSha256": "abdbba2e7db4a94d25b82826539e1acbb9e59be7bb9d15167eeeee8f3838644d",
197
+ "archiveBytes": 7935,
198
+ "archiveSha256": "2f79e0637ed13a1fbf69d237eb2e644939fd4e3bb5cb257e5b6910fb19981012"
199
+ }
200
+ ],
201
+ "limits": [
202
+ "The AgentSession, frontend, native tools, authorization and run writer are production code.",
203
+ "The controlled local transport supplies deterministic model responses and usage fixtures.",
204
+ "These functional checks do not establish external provider OAuth/service availability or visual equivalence to OMP.",
205
+ "Each compressed capture is a cumulative actual ANSI stream from PTY startup through the named interaction. Replay at its recorded dimensions."
206
+ ]
207
+ }
@@ -0,0 +1,254 @@
1
+ # Harness terminal frontend
2
+
3
+ Status: implemented in the working tree; publication is separate. Last updated: 2026-10-17.
4
+ Decision: [ADR-0001](decisions/ADR-0001-owned-terminal.md).
5
+
6
+ ## Ownership and process boundary
7
+
8
+ Harness owns its interactive terminal application in [`src/terminal/tui/`](../src/terminal/tui/).
9
+ [`TerminalApplication`](../src/terminal/tui/application.ts) owns terminal lifecycle, screen
10
+ composition, focus, input dispatch, scrolling, editor mounting, dialogs and keyboard actions.
11
+ It does not construct Pi's `InteractiveMode`. Pi 1.2.0 supplies the persistent agent session,
12
+ model and auth runtime, native JSONL history, tool execution and rich content components.
13
+
14
+ The parent process still calls `Terminals.prepare()`, spawns its returned specification in a
15
+ PTY or inherited terminal, and eventually calls `dispose()`. Its default command is the active
16
+ Node runtime followed by the existing Harness CLI and `terminal-frontend`; it requires no global
17
+ `pi` command. `TerminalAssets` remains `{ node, cli, piExtension }`, and the existing
18
+ `prepare`, `send`, `state`, `input` and `dispose` contracts remain in place. The `piExtension`
19
+ path remains the final prepared extension argument. The owned runtime recognizes that bridge
20
+ position and loads its trusted, bundled factory inline after the selected disk extensions.
21
+
22
+ ```text
23
+ Harness parent: account + compiled context + trusted resources + run writer
24
+ | launch specification / existing session socket
25
+ Harness terminal child: TerminalApplication + persistent Pi AgentSessionRuntime
26
+ | typed native events / plugin UI callbacks
27
+ Harness screen: welcome, transcript, status band, editor, dialogs and overlays
28
+ ```
29
+
30
+ [`runtime.ts`](../src/terminal/tui/runtime.ts) builds a public `AgentSessionRuntime` around
31
+ `createAgentSession`, `ModelRuntime`, `SessionManager` and `DefaultResourceLoader`. Events and
32
+ subscriptions survive session replacement. Before replacement it detaches extension UI; after
33
+ replacement it binds a fresh `ExtensionUIContext` in `tui` mode. Presentation reads frozen
34
+ `TerminalSnapshot` values; submission, interruption, navigation, decision polling and decision
35
+ responses pass through `TerminalRuntime`. Native components retain a scoped SDK adapter for
36
+ rich Markdown, tool and extension rendering. Native history remains owned by
37
+ Pi; Harness run records remain written through the existing controller/run store.
38
+
39
+ A persistent child retains queues, session-local extension state and interactive UI between
40
+ prompts. Reusing `sessions.run` for each prompt would recreate and dispose a session for every
41
+ turn and overlap the terminal controller's run lifecycle. That API remains the embedding and
42
+ unattended execution path.
43
+
44
+ ## Reference and reproduced presentation
45
+
46
+ The reference is **Oh My Pi 18.11.3**, identified from npm and GitHub on 2026-10-17; its release
47
+ publication timestamp was 2026-10-16T21:45:36Z. Inspection used the shipped npm package sources,
48
+ not an arbitrary checkout of upstream main. The source repository is
49
+ [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi).
50
+
51
+ | Inspected artifact | SHA-256 |
52
+ |---|---|
53
+ | `@oh-my-pi/pi-coding-agent@18.11.3` npm tarball | `b4a4eaf8057a7591f4ca83be293780489fd72b174e6a629d4704320165177f09` |
54
+ | `@oh-my-pi/pi-tui@18.11.3` npm tarball | `d28351b1309474eaf02f9be0fdd59c099ba9b8baeec811ef61349facf7a3565c` |
55
+ | Native `omp` artifact used for observation | `d518310d647d467a2c5c3f2d38e34489a761996d766395c03b37c61a2203d02b` |
56
+
57
+ In the extracted `pi-tui` package, `src/theme/defaults/titanium.json` establishes the titanium
58
+ palette and `src/prompt/welcome.ts` establishes the five-row Pi logo, exact diagonal RGB stops
59
+ and indexed-color fallback, centered lockup and narrow-width behavior. Harness adapts these
60
+ into [`theme.ts`](../src/terminal/tui/theme.ts) and [`welcome.ts`](../src/terminal/tui/welcome.ts),
61
+ with its own product name, package version and existing-command tips. The status band and
62
+ curved `╰─` input framing are implemented in [`status.ts`](../src/terminal/tui/status.ts) and
63
+ [`editor.ts`](../src/terminal/tui/editor.ts). Startup selects the Harness titanium presentation
64
+ without rewriting the user's saved theme settings. The shipped OMP light palette is also
65
+ available as `light`; native rich components and the status band switch together. Missing
66
+ Pi-specific color tokens use documented Harness fallbacks in `theme.ts`.
67
+
68
+ The frontend exposes capabilities already present in Harness/Pi. It does not import OMP's
69
+ agent runtime, tools, providers, MCP implementation, task system or other product features.
70
+ Harness-owned composition reuses public Pi Markdown, message, tree/model selector and rich
71
+ tool components. Tool diffs, expandable output, images and extension renderers therefore retain
72
+ the existing Pi interpretation. This is an implementation of the requested terminal presentation;
73
+ complete visual or behavioral parity with every OMP surface has not been established.
74
+
75
+ Full upstream copyright and MIT permission notices are in [NOTICE.md](../NOTICE.md).
76
+
77
+ ## Runtime compatibility adapter
78
+
79
+ Most integration uses public Pi exports. [`piAdapter.ts`](../src/terminal/tui/piAdapter.ts)
80
+ contains the remaining implementation dependencies and requires exactly Pi **1.2.0** before
81
+ loading them:
82
+
83
+ | Internal dependency | Reason |
84
+ |---|---|
85
+ | `dist/extensions/index.js` builtin inventory | Preserve selected `llama.cpp`, `codemode`, `tool-search` and MCP builtins with the CLI's replacement semantics |
86
+ | `dist/modes/interactive/theme/theme.js:setThemeInstance` | Keep public native rich components on the same active theme as the Harness screen |
87
+ | `dist/core/tools/renderers/index.js:withBuiltInRenderers` | Preserve existing read/edit/bash/write formatting without replacing tool execution |
88
+ | `dist/utils/clipboard.js` and `clipboard-image.js` read helpers | Preserve explicit user clipboard paste behavior; these are not called during inspection or startup |
89
+
90
+ The adapter is initialized once; its global theme setter and renderer lookup are synchronous
91
+ thereafter. A Pi dependency upgrade must update this adapter deliberately and rerun the
92
+ terminal compatibility tests. These module imports do not instantiate `InteractiveMode`.
93
+
94
+ ## Accounts, history and resource trust
95
+
96
+ The child requires the prepared absolute `PI_CODING_AGENT_DIR`; there is no fallback to a
97
+ personal Pi profile. Provider authentication and model catalogs use that account's files.
98
+ The selected account stays pinned. The existing `/accounts` command requests a parent-managed
99
+ restart to use a different account while resuming the conversation.
100
+
101
+ An initial `--session` or `/resume` target must resolve inside the prepared history directory,
102
+ including symlink targets, and its stored absolute working folder must match the terminal's
103
+ canonical working folder. `/import` may copy an explicitly selected external native JSONL file
104
+ only when its recorded working folder matches. A caller cannot override a foreign recorded
105
+ folder to borrow the current project's permissions. Lifecycle navigation and reload refuse an
106
+ active response; cancel it first. Opening a conversation from another working folder requires
107
+ opening that folder's terminal and preparing a fresh context/resource scope.
108
+
109
+ Navigation revalidates resource trust before tearing down the current runtime. The resource
110
+ loader suppresses its own discovery and uses only the prepared extensions,
111
+ skills, prompts and themes. Before **every** loader reload, including startup and native
112
+ session replacements, the child calls protected `terminal.resources`. The parent reruns
113
+ `Plugins.loading` for the original scope and compares selected paths and content/trust
114
+ identities with the preparation snapshot. Changed, detached or revoked code requires review
115
+ and a fresh terminal. Explicit `/reload` validates before invalidating the current session.
116
+ The Harness approval extension remains last, so it gates the final tool arguments.
117
+
118
+ ## Private control and user decisions
119
+
120
+ The environment contains a session socket address and bearer token. The frontend claims a
121
+ second key once with `control.claim`, before plugin code loads. [`control.ts`](../src/terminal/control.ts)
122
+ retains it in a process-local promise shared with the bridge; it is never written to disk or
123
+ placed in the environment. It survives `/new`, resume, fork and reload.
124
+
125
+ The private key is required for `terminal.pending`, `terminal.approve`, `terminal.answer`,
126
+ `terminal.draft`, `terminal.interrupt` and `terminal.resources`, as well as existing account and
127
+ plugin mutations. A model-spawned shell with only the environment token cannot answer its own
128
+ pending questions or approvals. The application renders decisions obtained from the existing
129
+ controller and returns the person's answer through these calls.
130
+
131
+ The editor sends exact draft state to `terminal.draft`; host delivery waits for that state
132
+ instead of overwriting a draft. Interruption resolves pending socket-backed questions and
133
+ approvals in the run registry before awaiting Pi abort. Disposal uses that order too, avoiding
134
+ a tool waiting forever on a question after the screen has closed. Native `session_shutdown`
135
+ clears controller readiness and stops that session's poller; the replacement's `session_start`
136
+ restores readiness while retaining the same process control claim.
137
+
138
+ The bridge exposes the shared `ask_user` batch tool alongside the existing single-question
139
+ `ask_user_question` contract. Registry events project the actual batch IDs, order, summaries,
140
+ choices and descriptions into the terminal inbox. The frontend stages answers locally and
141
+ submits them together through protected `terminal.answers`; the registry validates the entire
142
+ batch before resolving any question. Escape interrupts a pending question; cancelling optional
143
+ text entry returns to the choices and preserves selections. Disappearing requests abort their
144
+ open dialogs. Labels are numbered so a real choice cannot collide with a dialog action.
145
+
146
+ Plugin UI methods carry a generation guard, including methods a plugin retained before an
147
+ `await`. Session replacement, reload and disposal revoke the old generation. Outgoing native
148
+ event subscriptions and custom transcript components detach before native invalidation.
149
+ Cleanup failures cannot prevent terminal restoration. Fatal asynchronous errors restore the
150
+ terminal and exit with failure; startup failures and suspend/resume use the same lifecycle.
151
+
152
+ The command registry supplies help and completion for every pinned Pi built-in command name,
153
+ plus Harness commands supplied by the bridge. `/trust` opens the existing protected code-review
154
+ workflow in `/plugins`; trust changes require a freshly prepared terminal. `/bug` opens a
155
+ reviewable Harness issue draft after confirmation, without attaching history. `/changelog`
156
+ reads bundled Harness change notes. `/reload` also applies the isolated account's edited
157
+ keyboard bindings through the public TUI manager.
158
+
159
+ ## Verification and remaining limits
160
+
161
+ Harness typecheck, **419 passing tests / two platform skips**, build and standalone proof
162
+ passed on 2026-10-17. A final cleanup-order adjustment was rechecked with all **91 frontend
163
+ tests**, typecheck, build and standalone proof. Coverage includes the real isolated persistent SDK/socket boundary,
164
+ exact history scope and symlink rejection, resource-trust refusal before new/reload, event and
165
+ UI rebinding, retained stale plugin callbacks, immutable snapshots, commands, keybinding reload,
166
+ modal/focus behavior, grouped answers, Unicode widths and cleanup failures. Tests make no
167
+ external provider request.
168
+
169
+ The controlled production PTY verifier exercises the real CLI, persistent SDK, native tools,
170
+ final gate/socket and run writer using a loopback OpenAI-compatible SSE fixture. See the
171
+ [retained result](evidence/terminal-2026-10-17-followup/verification.json) and its compressed
172
+ ANSI captures. The earlier [three-size result](evidence/terminal-2026-10-17/verification.json)
173
+ is retained separately. Fixture responses verify transport and actual runtime/tool behavior;
174
+ they do not establish commercial-provider or OAuth availability.
175
+
176
+ The final three-size acceptance run covers 40×20, 80×24 and 120×40, live resizing, streaming,
177
+ thinking, actual read/bash execution, approval allow/deny/Escape, grouped and single questions,
178
+ question cancellation, account switching/restart, new/resume, trusted plugin custom overlays,
179
+ widgets/status, light-theme switching, tool expansion and scrolling. It also checks
180
+ suspend/resume, fatal asynchronous plugin failure and plugin startup failure. Normal exits
181
+ restore terminal attributes, bracketed paste and cursor visibility; failure cases restore them
182
+ and exit 1. Each report binds the exact CLI and verifier SHA-256 digests.
183
+
184
+ After the user reported that the global command still displayed Pi, inspection found an older
185
+ installed 0.1.6 bundle despite the checkout sharing that version. The checkout build was packed
186
+ and installed locally, without publication or account-data changes. The installed CLI now matches
187
+ the tested build byte for byte and passed all 14 isolated PTY groups at 80×24. See the
188
+ [global installation evidence](evidence/terminal-2026-10-17-followup/global-install/README.md).
189
+ Already-running sessions need to exit and relaunch to load the replacement.
190
+
191
+ A direct REA comparison of the pinned OMP executable and Harness at 80×24 captures startup and
192
+ settings without output truncation; both exit 0. The [readable comparison](evidence/terminal-2026-10-17-followup/reference-comparison.md)
193
+ retains exact frame/evidence IDs. It established matching centered welcome/composer geometry,
194
+ rounded settings framing and the need for flat settings rows at 80 columns; the latter was
195
+ corrected using the shipped SettingsList's minimum editable-pane width. Product lockups, tips,
196
+ capability tabs, setting names, paths and unavailable-model guidance differ deliberately.
197
+ This is bounded visual review, not proof of equivalence for every OMP surface.
198
+
199
+ The App sidecar build passed, and its actual bundled `dist/cli.js` passed the same **14 PTY
200
+ interaction groups at 80×24**, including account restart, suspension and both failure cleanup
201
+ cases. Its [separate report](evidence/terminal-2026-10-17-followup/app-bundle/verification.json)
202
+ binds that bundle digest. App typecheck, layer/UI checks, all **29 sidecar tests**, and **215 Rust tests** passed (one Rust
203
+ test ignored). The full renderer run remains blocked by the existing untracked
204
+ `PendingRequestDock.test.ts:68` expectation for `Planner needs your answer`; those App UI files
205
+ were preserved. The combined App test script stops there, so sidecar tests were run separately.
206
+
207
+ The user-authorized personal Codex live attempt used a disposable Harness profile and only
208
+ an unexpired copied access token. Codex returned **“The usage limit has been reached”** before
209
+ a successful response or tool execution. Harness persisted the failed run and restored the
210
+ terminal; the source sign-in file was unchanged and the disposable credentials were deleted.
211
+ The [live report and capture](evidence/terminal-2026-10-17-followup/live-codex/README.md) retain
212
+ this quota failure explicitly. Successful live streaming/tool acceptance remains open until
213
+ account capacity is available. GitHub gist publication, actual OS clipboard/image transfer,
214
+ Windows behavior and every terminal emulator remain unverified. Rich image/diff rendering and clipboard adapters have
215
+ focused compatibility tests; that does not establish OS interaction. These working-tree changes
216
+ are not a published release and the complete plan's acceptance remains open while required
217
+ integration/provider evidence is outstanding.
218
+
219
+ To reproduce the controlled scenario after building:
220
+
221
+ ```sh
222
+ npm run build
223
+ python3 scripts/verify-terminal.py --save-evidence docs/evidence/terminal-2026-10-17-followup
224
+ ```
225
+
226
+ Complete REA records are retained as compressed JSON in the follow-up evidence directory; the
227
+ [evidence manifest](evidence/terminal-2026-10-17-followup/rea-artifacts.json) records their IDs,
228
+ digests, coverage and sizes. The earlier REA IDs below are inherited ledger references; only
229
+ new portable records in that manifest are independently available from this checkout.
230
+
231
+ ### REA evidence ledger
232
+
233
+ - `ev_f372e317bc5780ef895531421d5ec29e62fcea219ed9e8f792bc132805ac4f07`:
234
+ shipped JavaScript static analysis; 13 selected files, 935 unselected. Semantic limits were
235
+ 3,000 per file and 25,000 globally. This is bounded static evidence, not whole-program or runtime coverage.
236
+ - `ev_3f4ebe0f81050df8144f3fb02fb712753350635dffbe1f37c1606fc45cca610f`:
237
+ focused shipped welcome implementation evidence.
238
+ - `ev_5d7ec70517e2e0da4258e5236ab93b9f4c269086dd7664c8b760e0375fba512b`:
239
+ native OMP observation; animation caused output truncation, so it is incomplete runtime evidence.
240
+ - `ev_12f82d325a8a2cf48c1f1783b4737696caf3ec98c7d8063910c9ddece4ad4028`:
241
+ owned frontend 80×24 menu/startup capture described above; no output truncation.
242
+
243
+ Evidence IDs refer to the investigation's retained REA ledger. The package digests and source
244
+ paths above allow artifact identity to be re-established; a fresh session does not implicitly
245
+ inherit that retained ledger.
246
+
247
+ - `ev_4438bf7b94a78ee2d13d0fe3b7a22238671d48b29a68f8beafbf942b9cee930c`:
248
+ supplemental static inspection of nine selected shipped files: default band composer,
249
+ composer integration, light palette, selector, approval/question dialogs, errors and generic
250
+ tool rendering. 939 inventoried files remain outside scope; semantic limits were 2,500 per
251
+ file and 20,000 globally. Source parsing did not execute the package.
252
+ - The portable OMP/Harness runtime evidence IDs and selected frame numbers are recorded in
253
+ the readable comparison and manifest linked above. An earlier OMP capture sent input before
254
+ startup completed; it is retained as incomplete evidence and excluded from the comparison.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingular/harness",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Xingular Harness — a Pi agent harness with multi-account sign-in, a compiled system prompt, a memory lifecycle (Wiki), tools, MCP, and skills",
6
6
  "repository": {
@@ -19,13 +19,16 @@
19
19
  },
20
20
  "scripts": {
21
21
  "build": "esbuild src/cli.ts --bundle --platform=node --target=node22 --format=esm --packages=external --outfile=dist/cli.js && esbuild src/terminal/piExtension.ts --bundle --platform=node --target=node22 --format=esm --packages=external --outfile=dist/terminal-pi.js",
22
+ "dev": "npm run build && node dist/cli.js",
22
23
  "prepack": "npm run build",
24
+ "ci": "bash scripts/ci.sh",
23
25
  "typecheck": "tsc --noEmit -p tsconfig.json",
24
26
  "test": "vitest run",
25
27
  "prove": "npm run build && node scripts/prove-standalone.mjs"
26
28
  },
27
29
  "dependencies": {
28
30
  "@earendil-works/pi-coding-agent": "1.2.0",
31
+ "@earendil-works/pi-tui": "1.2.0",
29
32
  "@modelcontextprotocol/client": "^2.2.0",
30
33
  "@modelcontextprotocol/server": "^2.2.0",
31
34
  "typebox": "^1.3.7",
@@ -45,6 +48,9 @@
45
48
  "src",
46
49
  "dist",
47
50
  "builtin",
48
- "README.md"
51
+ "README.md",
52
+ "NOTICE.md",
53
+ "CHANGELOG.md",
54
+ "docs"
49
55
  ]
50
56
  }
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSyn
2
2
  import { homedir, tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { expect, test } from 'vitest'
5
+ import type { ModelRuntime } from '@earendil-works/pi-coding-agent'
5
6
 
6
7
  import { Bus, type Notice } from '../core/bus'
7
8
  import { Accounts, classifyMessage } from './accounts'
@@ -21,6 +22,100 @@ function service(dir = temp()) {
21
22
  return { dir, secrets, notices, accounts: new Accounts({ dir, secrets, bus }) }
22
23
  }
23
24
 
25
+ function loginService(login: (interaction: {
26
+ signal: AbortSignal
27
+ notify: (notification: Record<string, unknown>) => void
28
+ prompt: (prompt: { type: string; message: string; placeholder?: string; options?: unknown; signal?: AbortSignal }) => Promise<string>
29
+ }) => Promise<void>) {
30
+ const dir = temp()
31
+ const secrets = memorySecretStore()
32
+ const notices: Notice[] = []
33
+ const bus = new Bus()
34
+ bus.on((notice) => void notices.push(notice))
35
+ let loginOptions: { getDeviceId?: () => string } | undefined
36
+ let loginCalls = 0
37
+ const runtime = {
38
+ login: (_provider: string, _method: string, interaction: Parameters<typeof login>[0], options?: { getDeviceId?: () => string }) => {
39
+ loginCalls += 1
40
+ loginOptions = options
41
+ return login(interaction)
42
+ }
43
+ }
44
+ class LoginAccounts extends Accounts {
45
+ override runtime(account: Parameters<Accounts['runtime']>[0]): Promise<ModelRuntime> {
46
+ void account
47
+ return Promise.resolve(runtime as unknown as ModelRuntime)
48
+ }
49
+ }
50
+ const accounts = new LoginAccounts({ dir, secrets, bus })
51
+ const account = accounts.create('Login test', 'subscription').accounts[0]
52
+ return { accounts, account, notices, loginOptions: () => loginOptions, loginCalls: () => loginCalls }
53
+ }
54
+
55
+ async function waitFor(predicate: () => boolean): Promise<void> {
56
+ for (let attempt = 0; attempt < 100; attempt++) {
57
+ if (predicate()) return
58
+ await new Promise((resolve) => setTimeout(resolve, 5))
59
+ }
60
+ throw new Error('Timed out waiting for the account operation.')
61
+ }
62
+
63
+ test('OpenAI login uses one persisted Harness installation device ID across accounts', async () => {
64
+ const { accounts, account, loginOptions, loginCalls } = loginService(async () => {})
65
+ const deviceId = await accounts.installationDeviceId()
66
+ const second = accounts.create('Second account', 'subscription').accounts.find((item) => item.id !== account.id)!
67
+
68
+ await accounts.loginStart(account.id, 'openai', 'oauth')
69
+ await waitFor(() => loginCalls() === 1)
70
+ expect(loginOptions()?.getDeviceId?.()).toBe(deviceId)
71
+
72
+ await accounts.loginStart(second.id, 'openai', 'oauth')
73
+ await waitFor(() => loginCalls() === 2)
74
+ expect(loginOptions()?.getDeviceId?.()).toBe(deviceId)
75
+ expect(JSON.parse(readFileSync(join(sharedDir(accounts.dir), 'settings.json'), 'utf8')).deviceId).toBe(deviceId)
76
+ expect(deviceId).toMatch(/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i)
77
+ })
78
+
79
+ test('device-code sign-in progress survives listener detachment and is available from status', async () => {
80
+ const { accounts, account, notices } = loginService((interaction) => new Promise<void>((_resolve, reject) => {
81
+ interaction.notify({ type: 'device_code', userCode: 'ABCD-EFGH', verificationUri: 'https://example.test/device' })
82
+ interaction.signal.addEventListener('abort', () => reject(new Error('aborted')), { once: true })
83
+ }))
84
+
85
+ const { loginId } = await accounts.loginStart(account.id, 'openai-codex', 'oauth')
86
+ await waitFor(() => (accounts.loginStatus(account.id)?.events.length ?? 0) > 0)
87
+ const detached = accounts.loginStatus(account.id)
88
+ expect(detached).toMatchObject({ loginId, status: 'running', events: [{ type: 'device_code', userCode: 'ABCD-EFGH' }] })
89
+ expect(notices.find((notice) => notice.type === 'accounts.loginEvent')).toMatchObject({ revision: 1, loginId })
90
+
91
+ // Asking again models a renderer returning to Accounts while the Harness stays alive.
92
+ expect(accounts.loginStatus(account.id)).toEqual(detached)
93
+ accounts.loginCancel(account.id, loginId)
94
+ await waitFor(() => accounts.loginStatus(account.id)?.status === 'cancelled')
95
+ const ended = accounts.loginStatus(account.id)
96
+ expect(ended).toMatchObject({ loginId, status: 'cancelled', events: [], prompt: null, success: false })
97
+ expect(JSON.stringify(ended)).not.toContain('ABCD-EFGH')
98
+ })
99
+
100
+ test('login status exposes the active prompt but never retains its response after completion', async () => {
101
+ const { accounts, account, notices } = loginService(async (interaction) => {
102
+ const value = await interaction.prompt({ type: 'secret', message: 'Enter the one-time value' })
103
+ expect(value).toBe('private-response')
104
+ })
105
+
106
+ const { loginId } = await accounts.loginStart(account.id, 'provider-with-prompt', 'oauth')
107
+ await waitFor(() => accounts.loginStatus(account.id)?.prompt !== null)
108
+ const waiting = accounts.loginStatus(account.id)
109
+ expect(waiting).toMatchObject({ loginId, status: 'running', prompt: { type: 'prompt', prompt: { type: 'secret' } } })
110
+ const promptId = String(waiting?.prompt?.promptId)
111
+ accounts.loginRespond(loginId, promptId, 'private-response')
112
+ await waitFor(() => accounts.loginStatus(account.id)?.status === 'completed')
113
+ const ended = accounts.loginStatus(account.id)
114
+ expect(ended).toMatchObject({ loginId, status: 'completed', success: true, events: [], prompt: null })
115
+ expect(JSON.stringify(ended)).not.toContain('private-response')
116
+ expect(notices.filter((notice) => notice.type === 'accounts.loginEvent' || notice.type === 'accounts.login').map((notice) => notice.revision)).toEqual([1, 2, 3])
117
+ })
118
+
24
119
  test('health reflects sign-in, errors and limits', () => {
25
120
  const state = { ...emptyState(), signedIn: true }
26
121
  expect(deriveHealth(state)).toBe('ok')
@@ -157,6 +252,21 @@ test('provider errors are classified', () => {
157
252
  expect(classifyMessage('the model said no')).toBeNull()
158
253
  })
159
254
 
255
+ test('a rejected OAuth refresh requests reauthentication and keeps the credential', async () => {
256
+ const { dir, accounts } = service()
257
+ const account = accounts.create('OpenAI', 'subscription').accounts[0]
258
+ await accounts.writeCredential(account.id, 'openai', { type: 'oauth', access: 'old-access', refresh: 'old-refresh', expires: 0 })
259
+ accounts.runtime = async () => ({
260
+ listCredentials: async () => [{ providerId: 'openai', type: 'oauth' }],
261
+ getAvailable: async () => { throw new Error('OAuth refresh failed for openai: OpenAI OAuth token request failed (400): {"error":"invalid_grant"}') }
262
+ } as unknown as ModelRuntime)
263
+
264
+ await accounts.refreshOne(account)
265
+
266
+ expect(loadState(dir, account.id)).toMatchObject({ health: 'auth_expired', lastError: { category: 'auth_expired' } })
267
+ expect(await accounts.readCredential(account.id, 'openai')).toMatchObject({ refresh: 'old-refresh' })
268
+ })
269
+
160
270
  test('native config lists what is shared and what exists', () => {
161
271
  const dir = temp()
162
272
  const before = nativeConfig(dir)