@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.
- package/CHANGELOG.md +11 -0
- package/NOTICE.md +38 -0
- package/README.md +118 -26
- package/dist/cli.js +6106 -912
- package/dist/terminal-pi.js +462 -11
- package/docs/README.md +12 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +87 -0
- package/docs/evidence/terminal-2026-10-17/120x40-completed-transcript.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-settings-interaction.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-settings-keyboard.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-welcome.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/verification.json +130 -0
- package/docs/evidence/terminal-2026-10-17-followup/120x40-completed-transcript.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-interaction.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-keyboard.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-welcome.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/verification.json +95 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/README.md +27 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command-80x24.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command.json +18 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/verification.json +95 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-final-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-scenario.json +60 -0
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/README.md +24 -0
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/complete-80x24.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/verification.json +57 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24-early.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-scenario.json +58 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-supplemental-static.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/rea-artifacts.json +320 -0
- package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.json +30 -0
- package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.md +133 -0
- package/docs/evidence/terminal-2026-10-17-followup/reference-source-observations.json +20 -0
- package/docs/evidence/terminal-2026-10-17-followup/verification.json +207 -0
- package/docs/terminal-tui.md +254 -0
- package/package.json +8 -2
- package/src/accounts/accounts.test.ts +110 -0
- package/src/accounts/accounts.ts +146 -10
- package/src/cli.ts +7 -3
- package/src/harness.ts +16 -3
- package/src/hub/catalog.json +123 -0
- package/src/hub/cli.ts +59 -0
- package/src/hub/hub.test.ts +205 -0
- package/src/hub/hub.ts +256 -0
- package/src/hub/schema.ts +81 -0
- package/src/index.ts +6 -1
- package/src/memory/api.ts +16 -2
- package/src/memory/blocks.ts +3 -1
- package/src/memory/caller.ts +13 -1
- package/src/memory/curation.test.ts +163 -0
- package/src/memory/curation.ts +64 -0
- package/src/memory/maintenance.ts +199 -11
- package/src/memory/operations.ts +12 -0
- package/src/memory/proposals.ts +16 -8
- package/src/memory/recall.test.ts +11 -0
- package/src/memory/recall.ts +9 -2
- package/src/server/methods.ts +37 -2
- package/src/sessions/contracts.ts +29 -3
- package/src/sessions/conversation.test.ts +27 -0
- package/src/sessions/policy.ts +4 -0
- package/src/sessions/questionBatches.test.ts +110 -0
- package/src/sessions/registry.ts +109 -22
- package/src/sessions/runner.ts +36 -1
- package/src/sessions/sessions.test.ts +95 -1
- package/src/sessions/sessions.ts +53 -9
- package/src/skills/registry.ts +16 -10
- package/src/terminal/accountsMenu.test.ts +225 -0
- package/src/terminal/accountsMenu.ts +382 -0
- package/src/terminal/control.test.ts +23 -0
- package/src/terminal/control.ts +23 -0
- package/src/terminal/controller.ts +68 -4
- package/src/terminal/hubMenu.ts +24 -0
- package/src/terminal/launch.test.ts +5 -14
- package/src/terminal/launch.ts +2 -42
- package/src/terminal/piExtension.lifecycle.test.ts +55 -0
- package/src/terminal/piExtension.ts +50 -1
- package/src/terminal/socket.ts +13 -2
- package/src/terminal/standalone.test.ts +37 -0
- package/src/terminal/standalone.ts +16 -7
- package/src/terminal/terminal.test.ts +70 -1
- package/src/terminal/terminals.ts +122 -12
- package/src/terminal/tui/application.test.ts +114 -0
- package/src/terminal/tui/application.ts +491 -0
- package/src/terminal/tui/commands.test.ts +218 -0
- package/src/terminal/tui/commands.ts +361 -0
- package/src/terminal/tui/dialogs.ts +301 -0
- package/src/terminal/tui/editor.test.ts +84 -0
- package/src/terminal/tui/editor.ts +79 -0
- package/src/terminal/tui/keybindings.test.ts +22 -0
- package/src/terminal/tui/keybindings.ts +50 -0
- package/src/terminal/tui/piAdapter.ts +62 -0
- package/src/terminal/tui/presentation.test.ts +79 -0
- package/src/terminal/tui/questions.test.ts +41 -0
- package/src/terminal/tui/questions.ts +47 -0
- package/src/terminal/tui/runtime.socket.test.ts +104 -0
- package/src/terminal/tui/runtime.test.ts +215 -0
- package/src/terminal/tui/runtime.ts +388 -0
- package/src/terminal/tui/sessionUi.ts +24 -0
- package/src/terminal/tui/settings.test.ts +150 -0
- package/src/terminal/tui/settings.ts +188 -0
- package/src/terminal/tui/status.test.ts +70 -0
- package/src/terminal/tui/status.ts +109 -0
- package/src/terminal/tui/theme.ts +174 -0
- package/src/terminal/tui/tools.test.ts +145 -0
- package/src/terminal/tui/tools.ts +142 -0
- package/src/terminal/tui/transcript.test.ts +25 -0
- package/src/terminal/tui/transcript.ts +124 -0
- package/src/terminal/tui/ui.test.ts +313 -0
- package/src/terminal/tui/ui.ts +166 -0
- package/src/terminal/tui/welcome.ts +102 -0
- package/src/tools/askUser.test.ts +69 -0
- package/src/tools/askUser.ts +31 -17
- package/src/tools/maintenance.ts +24 -0
- package/src/tools/memoryTools.ts +1 -1
- package/src/tools/registryWrite.test.ts +101 -0
- package/src/tools/registryWrite.ts +182 -0
- 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.
|
|
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)
|