@robota-sdk/agent-ui-terminal 3.0.0-beta.85 → 3.0.0-beta.87

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 (66) hide show
  1. package/CHANGELOG.md +68 -14
  2. package/README.md +2 -2
  3. package/dist/node/index.d.ts +8 -2
  4. package/dist/node/index.d.ts.map +1 -1
  5. package/dist/node/index.js +14 -14
  6. package/dist/node/index.js.map +1 -1
  7. package/package.json +19 -20
  8. package/src/SupervisedSessionView.tsx +12 -2
  9. package/src/__tests__/SupervisedSessionView.test.tsx +9 -5
  10. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
  11. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +1 -1
  12. package/src/__tests__/UpdateNotice.test.tsx +2 -2
  13. package/src/__tests__/attached-app-session-switch.test.tsx +1 -1
  14. package/src/__tests__/attention-bell-and-marks.test.ts +5 -5
  15. package/src/__tests__/channel-factory-integration.test.ts +3 -3
  16. package/src/__tests__/checkpoint-store-per-channel.test.ts +1 -1
  17. package/src/__tests__/cjk-fallback-render.test.tsx +2 -2
  18. package/src/__tests__/command-handoff-pty-e2e.test.ts +2 -2
  19. package/src/__tests__/command-result-handler.test.ts +3 -3
  20. package/src/__tests__/fork-attach.test.tsx +3 -3
  21. package/src/__tests__/helpers/terminal-profiles.ts +4 -4
  22. package/src/__tests__/humanize-tool-name.test.ts +1 -1
  23. package/src/__tests__/org-policy-reaches-the-channel.test.ts +5 -5
  24. package/src/__tests__/product-display-name.test.tsx +5 -5
  25. package/src/__tests__/pty/ask-user-question.ptytest.ts +2 -2
  26. package/src/__tests__/pty/background-work-switcher.ptytest.ts +3 -3
  27. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +9 -9
  28. package/src/__tests__/pty/flag-tui.ptytest.ts +2 -2
  29. package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +53 -24
  30. package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +59 -14
  31. package/src/__tests__/pty/ime-cursor.ptytest.ts +4 -4
  32. package/src/__tests__/pty/isolated-home.ts +37 -4
  33. package/src/__tests__/pty/org-policy.ptytest.ts +3 -3
  34. package/src/__tests__/pty/provider-setup.ptytest.ts +2 -2
  35. package/src/__tests__/pty/pty-driver.ts +8 -6
  36. package/src/__tests__/pty/replay-conversation.ptytest.ts +2 -2
  37. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +2 -2
  38. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +4 -4
  39. package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +3 -3
  40. package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +2 -2
  41. package/src/__tests__/pty/screen-2002-themes.ptytest.ts +3 -3
  42. package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +3 -3
  43. package/src/__tests__/pty/screen-reader-mode.ptytest.ts +16 -16
  44. package/src/__tests__/pty/session-attach.ptytest.ts +66 -28
  45. package/src/__tests__/pty/settings-screen.ptytest.ts +2 -2
  46. package/src/__tests__/pty/spawn-pty.ts +3 -3
  47. package/src/__tests__/pty/terminal-handoff.ptytest.ts +2 -2
  48. package/src/__tests__/pty/trust-prompt.ptytest.ts +5 -5
  49. package/src/__tests__/pty/tui-pty.ptytest.ts +2 -2
  50. package/src/__tests__/screen-reader-labels.test.ts +1 -1
  51. package/src/__tests__/screen-reader-pacing.test.ts +3 -3
  52. package/src/__tests__/screen-reader-render-options.test.ts +3 -3
  53. package/src/__tests__/session-switch-channel.test.tsx +1 -1
  54. package/src/__tests__/terminal-capabilities.test.ts +9 -9
  55. package/src/__tests__/terminal-handoff-pty-e2e.test.ts +1 -1
  56. package/src/__tests__/tui-channel-init-failure.test.ts +1 -1
  57. package/src/__tests__/tui-state-manager.test.ts +2 -2
  58. package/src/__tests__/wire-tui-channel.test.ts +2 -2
  59. package/src/keybindings/__tests__/keybinding-registry.test.ts +2 -2
  60. package/src/keybindings/__tests__/node-keybindings-source.test.ts +8 -1
  61. package/src/keybindings/node-keybindings-source.ts +6 -6
  62. package/src/render.tsx +7 -1
  63. package/src/terminal-capabilities.ts +1 -1
  64. package/src/tui-channel-options.ts +2 -0
  65. package/src/tui-session-options.ts +4 -0
  66. package/src/utils/__tests__/tool-call-extractor.test.ts +1 -1
@@ -31,7 +31,9 @@ import { spawnTui } from './pty-driver.js';
31
31
  import type { IPtySession } from './pty-driver.js';
32
32
 
33
33
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
34
- const ROBOTA_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/robota.cjs');
34
+ const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
35
+ const CLI_NAME = 'fixture-agent';
36
+ const PROTOCOL_SCHEME = 'fixture-agent';
35
37
  const WAIT_MS = 20_000;
36
38
  const CASE_TIMEOUT_MS = 120_000;
37
39
  const PROMPT = 'Summarize the README in one sentence';
@@ -42,11 +44,12 @@ interface IFixture {
42
44
  trusted: string;
43
45
  untrusted: string;
44
46
  elsewhere: string;
47
+ userStateRoot: string;
45
48
  env: NodeJS.ProcessEnv;
46
49
  }
47
50
 
48
51
  function writeProviderSettings(home: string): void {
49
- const dir = join(home, '.robota');
52
+ const dir = join(home, 'state');
50
53
  mkdirSync(dir, { recursive: true });
51
54
  // Skip the first-run welcome: it is an onboarding screen, not what this scenario observes.
52
55
  writeFileSync(join(dir, 'onboarded'), new Date().toISOString(), 'utf8');
@@ -68,7 +71,7 @@ function initRepo(path: string, env: NodeJS.ProcessEnv): void {
68
71
  execFileSync('git', args, { cwd: path, env, stdio: ['ignore', 'ignore', 'pipe'] });
69
72
  };
70
73
  run('init', '-q', '-b', 'main');
71
- run('config', 'user.name', 'Robota Scenario');
74
+ run('config', 'user.name', 'Scenario Fixture');
72
75
  run('config', 'user.email', 'scenario@example.invalid');
73
76
  writeFileSync(join(path, 'README.md'), 'fixture\n');
74
77
  run('add', '--', 'README.md');
@@ -76,13 +79,37 @@ function initRepo(path: string, env: NodeJS.ProcessEnv): void {
76
79
  }
77
80
 
78
81
  function makeFixture(): IFixture {
79
- const root = realpathSync(mkdtempSync(join(tmpdir(), 'robota-flow2006-')));
82
+ const root = realpathSync(mkdtempSync(join(tmpdir(), 'agent-flow2006-')));
80
83
  const home = join(root, 'home');
81
84
  mkdirSync(home, { recursive: true });
82
85
  writeProviderSettings(home);
86
+ const userStateRoot = join(home, 'state');
83
87
  const env: NodeJS.ProcessEnv = {
84
88
  PATH: process.env['PATH'] ?? '',
85
89
  HOME: home,
90
+ PRODUCT_ID: CLI_NAME,
91
+ PRODUCT_DISPLAY_NAME: 'Fixture Agent',
92
+ PRODUCT_CLI_NAME: CLI_NAME,
93
+ PRODUCT_ENV_PREFIX: 'FIXTURE_AGENT_',
94
+ PRODUCT_PACKAGE_SCOPE: '@fixture-agent',
95
+ PRODUCT_APP_ID: 'org.example.fixture-agent',
96
+ PRODUCT_PROTOCOL_SCHEME: PROTOCOL_SCHEME,
97
+ PRODUCT_TELEMETRY_SERVICE_NAME: 'fixture-agent',
98
+ PRODUCT_DAEMON_NAMESPACE: 'fixture-agent-daemon',
99
+ PRODUCT_DESKTOP_EXECUTABLE: 'fixture-robota',
100
+ PRODUCT_MCP_CLIENT_NAME: 'fixture-agent-client',
101
+ PRODUCT_MODEL_TOOL_PREFIX: 'fixture_agent_command_',
102
+ PRODUCT_PROMPT_TAG: 'fixture_agent_references',
103
+ PRODUCT_EDITOR_TEMP_PREFIX: 'fixture-agent-editor-',
104
+ PRODUCT_USER_STATE_DIR: userStateRoot,
105
+ PRODUCT_PROJECT_STATE_DIR: '.fixture-agent',
106
+ PRODUCT_CACHE_DIR: join(userStateRoot, 'cache'),
107
+ PRODUCT_LOG_DIR: join(userStateRoot, 'logs'),
108
+ PRODUCT_BROWSER_NAMESPACE: 'fixture-agent-browser',
109
+ PRODUCT_BROWSER_CREDENTIAL_DATABASE: 'fixture-agent-credentials',
110
+ PRODUCT_CREDENTIAL_SERVICE: 'org.example.fixture-agent.credentials',
111
+ PRODUCT_CRYPTO_NAMESPACE: 'fixture-agent-crypto',
112
+ SECURITY_MASTER_KEY_DERIVATION_PATH: '[123,0]',
86
113
  TERM: 'xterm-256color',
87
114
  GIT_CONFIG_NOSYSTEM: '1',
88
115
  GIT_CONFIG_GLOBAL: join(home, 'gitconfig'),
@@ -94,13 +121,14 @@ function makeFixture(): IFixture {
94
121
  trusted: join(root, 'trusted'),
95
122
  untrusted: join(root, 'untrusted'),
96
123
  elsewhere: join(root, 'elsewhere'),
124
+ userStateRoot,
97
125
  env,
98
126
  };
99
127
  initRepo(fixture.trusted, env);
100
128
  initRepo(fixture.untrusted, env);
101
129
  mkdirSync(fixture.elsewhere, { recursive: true });
102
130
  // Grant trust to the one repository a link is allowed to open.
103
- const granted = spawnSync(process.execPath, [ROBOTA_BIN, 'trust', '--yes'], {
131
+ const granted = spawnSync(process.execPath, [AGENT_BIN, 'trust', '--yes'], {
104
132
  cwd: fixture.trusted,
105
133
  env,
106
134
  encoding: 'utf8',
@@ -110,16 +138,16 @@ function makeFixture(): IFixture {
110
138
  }
111
139
 
112
140
  function link(fixture: IFixture, extra = ''): string {
113
- return `robota://open?v=1&prompt=${encodeURIComponent(PROMPT)}&cwd=${fixture.trusted}${extra}`;
141
+ return `${PROTOCOL_SCHEME}://open?v=1&prompt=${encodeURIComponent(PROMPT)}&cwd=${fixture.trusted}${extra}`;
114
142
  }
115
143
 
116
- /** Run `robota open <url>` as a process, headless — used for the refusal scenarios. */
144
+ /** Run the fixture CLI's `open` command as a process, headless — used for the refusal scenarios. */
117
145
  function openHeadless(
118
146
  fixture: IFixture,
119
147
  url: string,
120
148
  cwd = fixture.elsewhere,
121
149
  ): ReturnType<typeof spawnSync> {
122
- return spawnSync(process.execPath, [ROBOTA_BIN, 'open', url], {
150
+ return spawnSync(process.execPath, [AGENT_BIN, 'open', url], {
123
151
  cwd,
124
152
  env: { ...fixture.env, TERM: 'dumb' },
125
153
  encoding: 'utf8',
@@ -134,7 +162,7 @@ function openHeadless(
134
162
  */
135
163
  function sessionRecords(home: string): readonly string[] {
136
164
  try {
137
- return readdirSync(join(home, '.robota', 'peers')).sort();
165
+ return readdirSync(join(home, 'state', 'peers')).sort();
138
166
  } catch {
139
167
  return [];
140
168
  }
@@ -143,7 +171,7 @@ function sessionRecords(home: string): readonly string[] {
143
171
  /** The trust store's bytes — a refused link must not add, remove or touch a grant. */
144
172
  function trustStoreDigest(home: string): string {
145
173
  return createHash('sha256')
146
- .update(readFileSync(join(home, '.robota', 'workspace-trust.json')))
174
+ .update(readFileSync(join(home, 'state', 'workspace-trust.json')))
147
175
  .digest('hex');
148
176
  }
149
177
 
@@ -155,19 +183,19 @@ function malformedLinks(fixture: IFixture): readonly (readonly [string, string])
155
183
  return [
156
184
  [link(fixture, '&provider=anthropic'), 'provider'],
157
185
  [link(fixture, `&prompt=${encodeURIComponent('second')}`), 'more than once'],
158
- [`robota://open?prompt=hi&cwd=${fixture.trusted}`, 'no `v`'],
159
- [`robota://open?v=2&prompt=hi&cwd=${fixture.trusted}`, 'version 1'],
186
+ [`${PROTOCOL_SCHEME}://open?prompt=hi&cwd=${fixture.trusted}`, 'no `v`'],
187
+ [`${PROTOCOL_SCHEME}://open?v=2&prompt=hi&cwd=${fixture.trusted}`, 'version 1'],
160
188
  [
161
- `robota://open?v=1&prompt=${encodeURIComponent('/mode bypassPermissions')}&cwd=${fixture.trusted}`,
189
+ `${PROTOCOL_SCHEME}://open?v=1&prompt=${encodeURIComponent('/mode bypassPermissions')}&cwd=${fixture.trusted}`,
162
190
  'not a command',
163
191
  ],
164
- ['robota://open?v=1&prompt=hi&cwd=relative/path', 'absolute'],
165
- [`robota://open?v=1&prompt=hi&cwd=${fixture.trusted}/../untrusted`, '`..`'],
166
- [`robota://open?v=1&prompt=hi&cwd=${join(fixture.root, 'absent')}`, 'does not exist'],
192
+ [`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=relative/path`, 'absolute'],
193
+ [`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=${fixture.trusted}/../untrusted`, '`..`'],
194
+ [`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=${join(fixture.root, 'absent')}`, 'does not exist'],
167
195
  ];
168
196
  }
169
197
 
170
- describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
198
+ describe('configured open deep link through the CLI binary (FLOW-2006 TC-09)', () => {
171
199
  let fixture: IFixture | undefined;
172
200
  let session: IPtySession | undefined;
173
201
 
@@ -191,6 +219,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
191
219
  projectDir: current.elsewhere,
192
220
  homeDir: current.home,
193
221
  args: ['open', link(current), '--name', 'flow2006'],
222
+ env: current.env as Record<string, string>,
194
223
  });
195
224
 
196
225
  // The PREFILLED TEXT is the arrival signal, not the empty-composer placeholder (which never
@@ -227,7 +256,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
227
256
  const current = fixture;
228
257
  if (!current) throw new Error('fixture');
229
258
  const recordsBefore = sessionRecords(current.home);
230
- const settingsBefore = readFileSync(join(current.home, '.robota', 'settings.json'), 'utf8');
259
+ const settingsBefore = readFileSync(join(current.userStateRoot, 'settings.json'), 'utf8');
231
260
  const cases = malformedLinks(current);
232
261
  for (const [url, expected] of cases) {
233
262
  const result = openHeadless(current, url);
@@ -238,7 +267,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
238
267
  // A SECOND LINK after the first is refused — the argv-appending shape the platforms document.
239
268
  const twoArgs = spawnSync(
240
269
  process.execPath,
241
- [ROBOTA_BIN, 'open', link(current), link(current)],
270
+ [AGENT_BIN, 'open', link(current), link(current)],
242
271
  {
243
272
  cwd: current.elsewhere,
244
273
  env: { ...current.env, TERM: 'dumb' },
@@ -251,7 +280,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
251
280
  // Nothing was started and nothing was configured: no session record appeared, and the
252
281
  // settings the `/mode bypassPermissions` link tried to reach are byte-identical.
253
282
  expect(sessionRecords(current.home)).toEqual(recordsBefore);
254
- expect(readFileSync(join(current.home, '.robota', 'settings.json'), 'utf8')).toBe(
283
+ expect(readFileSync(join(current.userStateRoot, 'settings.json'), 'utf8')).toBe(
255
284
  settingsBefore,
256
285
  );
257
286
  },
@@ -266,19 +295,19 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
266
295
  const digestBefore = trustStoreDigest(current.home);
267
296
  const untrusted = openHeadless(
268
297
  current,
269
- `robota://open?v=1&prompt=hi&cwd=${current.untrusted}`,
298
+ `${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=${current.untrusted}`,
270
299
  );
271
300
  expect(untrusted.status).toBe(1);
272
- expect(`${untrusted.stderr}`).toContain('robota trust --yes');
301
+ expect(`${untrusted.stderr}`).toContain(`${CLI_NAME} trust --yes`);
273
302
 
274
- const slug = openHeadless(current, 'robota://open?v=1&prompt=hi&repo=nobody/not-a-clone');
303
+ const slug = openHeadless(current, `${PROTOCOL_SCHEME}://open?v=1&prompt=hi&repo=nobody/not-a-clone`);
275
304
  expect(slug.status).toBe(1);
276
305
  expect(`${slug.stderr}`).toContain('nobody/not-a-clone');
277
306
  expect(`${slug.stderr}`).not.toContain('cloning');
278
307
 
279
308
  // A refused link grants nothing and records nothing: the untrusted repository is still
280
309
  // untrusted and the store is byte-identical to what it was before the two invocations.
281
- const status = spawnSync(process.execPath, [ROBOTA_BIN, 'trust', 'status'], {
310
+ const status = spawnSync(process.execPath, [AGENT_BIN, 'trust', 'status'], {
282
311
  cwd: current.untrusted,
283
312
  env: current.env,
284
313
  encoding: 'utf8',
@@ -3,31 +3,46 @@
3
3
  *
4
4
  * Every other cell reads the cursor out of the byte stream our own VT interpreter replays. tmux is
5
5
  * a full, independent terminal emulator that can be driven headlessly AND queried: it runs the
6
- * built robota binary in a real pane, receives the Korean keystrokes through its own input path,
6
+ * built CLI binary in a real pane, receives the Korean keystrokes through its own input path,
7
7
  * and then reports where ITS state machine put the hardware cursor
8
8
  * (`#{cursor_x}` / `#{cursor_y}` / `#{cursor_flag}`). That makes this the strongest available
9
9
  * confirmation short of macOS hardware: a second implementation agrees the cursor sits on the
10
10
  * input row at the composition column — which is the cell an OS IME anchors its window to.
11
11
  *
12
- * Skipped when tmux is not installed; `ROBOTA_TMUX_BIN` overrides the binary. GitHub's ubuntu
12
+ * Skipped when tmux is not installed; `PRODUCT_TMUX_BIN` overrides the binary. GitHub's ubuntu
13
13
  * runner images ship tmux, so this runs in CI rather than quietly evaporating there.
14
14
  */
15
15
 
16
16
  import { execFileSync, spawnSync } from 'node:child_process';
17
- import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, realpathSync } from 'node:fs';
17
+ import {
18
+ chmodSync,
19
+ existsSync,
20
+ mkdirSync,
21
+ mkdtempSync,
22
+ rmSync,
23
+ writeFileSync,
24
+ realpathSync,
25
+ } from 'node:fs';
18
26
  import { tmpdir } from 'node:os';
19
27
  import { join, resolve } from 'node:path';
20
28
 
21
29
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
22
30
 
31
+ import { fixtureProductEnvironment } from './isolated-home.js';
32
+
23
33
  const REPO_ROOT = resolve(__dirname, '../../../../..');
24
- const ROBOTA_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/robota.cjs');
34
+ const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
35
+ const GIT_BIN =
36
+ (process.env['PATH'] ?? '/usr/bin:/bin')
37
+ .split(':')
38
+ .map((directory) => join(directory, 'git'))
39
+ .find((candidate) => existsSync(candidate)) ?? '/usr/bin/git';
25
40
  const COLS = 80;
26
41
  const COMPOSITION = '안녕';
27
42
  const COMPOSITION_WIDTH = 4;
28
43
 
29
44
  function resolveTmux(): string | undefined {
30
- const override = process.env['ROBOTA_TMUX_BIN'];
45
+ const override = process.env['PRODUCT_TMUX_BIN'];
31
46
  if (override !== undefined && override !== '') return override;
32
47
  const which = spawnSync('sh', ['-c', 'command -v tmux'], { encoding: 'utf8' });
33
48
  const found = which.stdout.trim();
@@ -49,14 +64,16 @@ interface ITmuxRun {
49
64
  socket: string;
50
65
  }
51
66
 
52
- /** Run robota inside a real tmux pane and ask tmux where the cursor ended up. */
67
+ /** Run the CLI inside a real tmux pane and ask tmux where the cursor ended up. */
53
68
  function runInTmux(run: ITmuxRun, rows: number, imeCursor: string | undefined): ITmuxProbe {
54
69
  const tmux = TMUX!;
55
70
  const projectDir = join(run.root, 'proj');
56
71
  const homeDir = join(run.root, 'home');
57
- mkdirSync(join(homeDir, '.robota'), { recursive: true });
72
+ mkdirSync(projectDir, { recursive: true });
73
+ mkdirSync(join(homeDir, 'state'), { recursive: true });
74
+ writeFileSync(join(homeDir, 'state/onboarded'), new Date().toISOString(), 'utf8');
58
75
  writeFileSync(
59
- join(homeDir, '.robota/settings.json'),
76
+ join(homeDir, 'state/settings.json'),
60
77
  JSON.stringify({
61
78
  currentProvider: 'anthropic',
62
79
  providers: {
@@ -69,14 +86,42 @@ function runInTmux(run: ITmuxRun, rows: number, imeCursor: string | undefined):
69
86
  // A wrapper keeps the pane's env explicit (never inheriting real provider keys) while letting
70
87
  // tmux itself set TERM/TERM_PROGRAM for the pane — the handshake under test.
71
88
  const wrapper = join(run.root, 'wrap.sh');
72
- const imeAssignment = imeCursor === undefined ? '' : `ROBOTA_IME_CURSOR=${imeCursor} `;
89
+ const imeAssignment = imeCursor === undefined ? '' : `PRODUCT_IME_CURSOR=${imeCursor} `;
90
+ const productAssignments = Object.entries(fixtureProductEnvironment(homeDir))
91
+ .map(([key, value]) => `${key}='${value.replaceAll("'", "'\\''")}'`)
92
+ .join(' ');
93
+ const fixtureEnv = {
94
+ PATH: process.env['PATH'] || '/usr/bin:/bin:/opt/homebrew/bin',
95
+ HOME: homeDir,
96
+ ...fixtureProductEnvironment(homeDir),
97
+ };
98
+ execFileSync(GIT_BIN, ['init', '-q', '-b', 'main'], { cwd: projectDir });
99
+ execFileSync(
100
+ GIT_BIN,
101
+ [
102
+ '-c',
103
+ 'user.name=PTY Fixture',
104
+ '-c',
105
+ 'user.email=pty@example.invalid',
106
+ 'commit',
107
+ '--allow-empty',
108
+ '-q',
109
+ '-m',
110
+ 'fixture',
111
+ ],
112
+ { cwd: projectDir, env: fixtureEnv },
113
+ );
114
+ execFileSync(process.execPath, [AGENT_BIN, 'trust', '--yes'], {
115
+ cwd: projectDir,
116
+ env: fixtureEnv,
117
+ });
73
118
  writeFileSync(
74
119
  wrapper,
75
120
  [
76
121
  '#!/bin/sh',
77
122
  `cd "${projectDir}"`,
78
- `exec env -i PATH="$PATH" HOME="${homeDir}" TERM="$TERM" TERM_PROGRAM="$TERM_PROGRAM" ${imeAssignment}` +
79
- `"${process.execPath}" "${ROBOTA_BIN}" --name tmux-fixture`,
123
+ `exec env -i PATH="$PATH" HOME="${homeDir}" TERM="$TERM" TERM_PROGRAM="$TERM_PROGRAM" ${productAssignments} ${imeAssignment}` +
124
+ `"${process.execPath}" "${AGENT_BIN}" --name tmux-fixture`,
80
125
  '',
81
126
  ].join('\n'),
82
127
  'utf8',
@@ -95,7 +140,7 @@ function runInTmux(run: ITmuxRun, rows: number, imeCursor: string | undefined):
95
140
  if (pane.includes('Type a message')) break;
96
141
  sleepSync(250);
97
142
  }
98
- expect(pane, 'robota never reached the prompt inside tmux').toContain('Type a message');
143
+ expect(pane, 'the CLI never reached the prompt inside tmux').toContain('Type a message');
99
144
  sleepSync(400);
100
145
 
101
146
  tmuxCall('send-keys', '-l', COMPOSITION);
@@ -127,7 +172,7 @@ describe.skipIf(TMUX === undefined)(
127
172
  let run: ITmuxRun;
128
173
 
129
174
  beforeEach(() => {
130
- const root = realpathSync(mkdtempSync(join(tmpdir(), 'robota-tmux-ime-')));
175
+ const root = realpathSync(mkdtempSync(join(tmpdir(), 'agent-tmux-ime-')));
131
176
  run = { root, socket: join(root, 'sock') };
132
177
  });
133
178
 
@@ -154,7 +199,7 @@ describe.skipIf(TMUX === undefined)(
154
199
  expect(probe.cursorVisible).toBe(false);
155
200
  }, 90_000);
156
201
 
157
- it('ROBOTA_IME_CURSOR=0 kill switch: tmux reports the cursor hidden even at 24 rows', () => {
202
+ it('PRODUCT_IME_CURSOR=0 kill switch: tmux reports the cursor hidden even at 24 rows', () => {
158
203
  const probe = runInTmux(run, 24, '0');
159
204
  const inputRow = probe.pane.findIndex((line) => line.includes(`> ${COMPOSITION}`));
160
205
  expect(inputRow).toBeGreaterThanOrEqual(0);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI-062 — PTY regression for real-terminal-cursor positioning (the OS-IME evidence).
3
3
  *
4
- * Runs the BUILT robota binary in a real pseudo-terminal and interprets the raw ANSI stream the
4
+ * Runs the BUILT CLI binary in a real pseudo-terminal and interprets the raw ANSI stream the
5
5
  * way a terminal emulator (and the OS IME) does. Two geometries from the implementation contract
6
6
  * (.design/investigations/2026-07-25-cli-062-ime-cursor-design.md):
7
7
  *
@@ -105,7 +105,7 @@ describe('CLI-062 — IME hardware-cursor positioning through a real PTY', () =>
105
105
  let session: IPtySession | undefined;
106
106
 
107
107
  beforeEach(() => {
108
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-pty-ime-')));
108
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-ime-')));
109
109
  writeTuiProviderSettings(projectDir);
110
110
  });
111
111
 
@@ -132,7 +132,7 @@ describe('CLI-062 — IME hardware-cursor positioning through a real PTY', () =>
132
132
  * PROVES, per cell: the built binary's observable cursor contract under exactly the environment
133
133
  * that terminal exports — positioned on the input row when the capability gate says yes, and no
134
134
  * cursor shown at all when it says no (Terminal.app's default-off I5 branch, and the
135
- * `ROBOTA_IME_CURSOR=0` kill switch).
135
+ * `PRODUCT_IME_CURSOR=0` kill switch).
136
136
  *
137
137
  * DOES NOT PROVE, for a `documented` row: anything about that emulator's own rendering, its
138
138
  * pre-edit display, or its OS IME. Terminal.app's historical Korean-IME SIGSEGV in particular is
@@ -145,7 +145,7 @@ describe('CLI-062 terminal matrix — observable cursor contract per terminal',
145
145
  let session: IPtySession | undefined;
146
146
 
147
147
  beforeEach(() => {
148
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-pty-ime-matrix-')));
148
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-ime-matrix-')));
149
149
  writeTuiProviderSettings(projectDir);
150
150
  });
151
151
 
@@ -2,7 +2,7 @@
2
2
  * Isolated HOME for PTY children (HARNESS-025).
3
3
  *
4
4
  * A PTY test that forwards the developer's real `HOME` runs its child against whatever happens to
5
- * be in that home directory — a `~/.robota` settings file, a shell rc, an `$EDITOR` preference.
5
+ * be in that home directory — a `~/state` settings file, a shell rc, an `$EDITOR` preference.
6
6
  * The suite then passes or fails depending on the machine it runs on: green on CI (empty HOME),
7
7
  * red on a developer box whose real config contradicts the fixture. That is the conditional flake
8
8
  * this module removes.
@@ -29,11 +29,11 @@ function installExitHook(): void {
29
29
  /**
30
30
  * Create a throwaway HOME directory for a PTY child and return its absolute path.
31
31
  *
32
- * The directory is empty: the child sees no `~/.robota`, no shell rc, no user preferences. Callers
32
+ * The directory is empty: the child sees no `~/state`, no shell rc, no user preferences. Callers
33
33
  * that need seeded content write it themselves after creating the directory.
34
34
  */
35
35
  export function createIsolatedHome(): string {
36
- const dir = mkdtempSync(join(tmpdir(), 'robota-pty-home-'));
36
+ const dir = mkdtempSync(join(tmpdir(), 'agent-pty-home-'));
37
37
  createdHomes.push(dir);
38
38
  installExitHook();
39
39
  return dir;
@@ -55,10 +55,43 @@ export function disposeIsolatedHomes(): void {
55
55
  * visible at the call site. Pass `overrides` for anything else the fixture needs (e.g. `EDITOR`).
56
56
  */
57
57
  export function createPtyEnv(overrides: Readonly<NodeJS.ProcessEnv> = {}): NodeJS.ProcessEnv {
58
+ const home = typeof overrides.HOME === 'string' ? overrides.HOME : createIsolatedHome();
58
59
  return {
59
60
  PATH: process.env['PATH'] ?? '',
60
- HOME: createIsolatedHome(),
61
+ HOME: home,
61
62
  TERM: 'xterm-256color',
63
+ ...fixtureProductEnvironment(home),
62
64
  ...overrides,
63
65
  };
64
66
  }
67
+
68
+ /** Product identity and state roots for the compiled CLI's isolated PTY fixtures. */
69
+ export function fixtureProductEnvironment(home: string): Record<string, string> {
70
+ const id = 'fixture-agent';
71
+ const stateRoot = join(home, 'state');
72
+ return {
73
+ PRODUCT_ID: id,
74
+ PRODUCT_DISPLAY_NAME: 'Fixture Agent',
75
+ PRODUCT_CLI_NAME: id,
76
+ PRODUCT_ENV_PREFIX: 'FIXTURE_AGENT_',
77
+ PRODUCT_PACKAGE_SCOPE: '@fixture-agent',
78
+ PRODUCT_APP_ID: 'org.example.fixture-agent',
79
+ PRODUCT_PROTOCOL_SCHEME: id,
80
+ PRODUCT_TELEMETRY_SERVICE_NAME: `${id}.agent`,
81
+ PRODUCT_DAEMON_NAMESPACE: `${id}.daemon`,
82
+ PRODUCT_DESKTOP_EXECUTABLE: `${id}-runtime`,
83
+ PRODUCT_MCP_CLIENT_NAME: `${id}-client`,
84
+ PRODUCT_MODEL_TOOL_PREFIX: 'fixture_agent_command_',
85
+ PRODUCT_PROMPT_TAG: 'fixture_agent_references',
86
+ PRODUCT_EDITOR_TEMP_PREFIX: `${id}-editor-`,
87
+ PRODUCT_USER_STATE_DIR: stateRoot,
88
+ PRODUCT_PROJECT_STATE_DIR: '.fixture-agent',
89
+ PRODUCT_CACHE_DIR: join(stateRoot, 'cache'),
90
+ PRODUCT_LOG_DIR: join(stateRoot, 'logs'),
91
+ PRODUCT_BROWSER_NAMESPACE: `${id}.browser`,
92
+ PRODUCT_BROWSER_CREDENTIAL_DATABASE: `${id}-credentials`,
93
+ PRODUCT_CREDENTIAL_SERVICE: `org.example.${id}.credentials`,
94
+ PRODUCT_CRYPTO_NAMESPACE: `${id}.crypto`,
95
+ SECURITY_MASTER_KEY_DERIVATION_PATH: '[123,0]',
96
+ };
97
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Real-PTY organization-policy engineering regression scenario (CLI-083).
3
3
  *
4
- * Drives the built Robota CLI with an isolated HOME. Nothing injects a policy object into the
4
+ * Drives the built Fixture Agent CLI with an isolated HOME. Nothing injects a policy object into the
5
5
  * command module: the refusal can only come from the shipped `loadOrgPolicy()` startup path. The
6
6
  * update check is disabled so this isolated scenario performs no startup network request.
7
7
  */
@@ -17,7 +17,7 @@ import { spawnTui } from './pty-driver.js';
17
17
  import type { IPtySession } from './pty-driver.js';
18
18
 
19
19
  function writeOrgPolicyFixture(homeDir: string): void {
20
- const settingsDir = join(homeDir, '.robota');
20
+ const settingsDir = join(homeDir, 'state');
21
21
  mkdirSync(settingsDir, { recursive: true });
22
22
  writeFileSync(
23
23
  join(settingsDir, 'settings.json'),
@@ -43,7 +43,7 @@ describe('organization policy through the built CLI (CLI-083)', () => {
43
43
  let session: IPtySession | undefined;
44
44
 
45
45
  beforeEach(() => {
46
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-pty-org-policy-')));
46
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-org-policy-')));
47
47
  homeDir = join(projectDir, 'home');
48
48
  writeOrgPolicyFixture(homeDir);
49
49
  });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Real-PTY provider-setup ask suite (CMD-004 TC-09 done-gate).
3
3
  *
4
- * Drives the BUILT robota binary through `/provider add` so the unified ask seam renders end-to-end on
4
+ * Drives the BUILT CLI binary through `/provider add` so the unified ask seam renders end-to-end on
5
5
  * a real terminal: the provider command self-asks (via the injected `askHandler` → `TuiInteractionChannel.askUser`)
6
6
  * and the TUI renders the request through `PendingActionPrompt`. Proves the masked secret field hides
7
7
  * the typed API key on the real CLI — the User Execution evidence the done-gate requires (no human
@@ -26,7 +26,7 @@ describe('provider setup ask through a real PTY (CMD-004 TC-09)', () => {
26
26
  let session: IPtySession | undefined;
27
27
 
28
28
  beforeEach(() => {
29
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-pty-provider-')));
29
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-provider-')));
30
30
  writeTuiProviderSettings(projectDir);
31
31
  });
32
32
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * PTY TUI driver (CLI-074 TC-07/08).
3
3
  *
4
- * Built-CLI convenience over the shared `spawnPty` harness (TEST-007): spawns the built robota CLI in
4
+ * Built-CLI convenience over the shared `spawnPty` harness (TEST-007): spawns the built CLI in
5
5
  * a real pseudo-terminal so Ink renders exactly as in a user terminal, with per-key paced input
6
6
  * (expect(1)-style burst input gets bundled as a bracketed paste — the failure mode this driver
7
7
  * exists to avoid). Test-only; lives in a dedicated vitest project (*.ptytest.ts).
@@ -11,17 +11,18 @@ import { mkdirSync, writeFileSync } from 'node:fs';
11
11
  import { join, resolve } from 'node:path';
12
12
 
13
13
  import { spawnPty } from './spawn-pty.js';
14
+ import { fixtureProductEnvironment } from './isolated-home.js';
14
15
 
15
16
  import type { IPtyRunSession } from './spawn-pty.js';
16
17
 
17
18
  const REPO_ROOT = resolve(__dirname, '../../../../..');
18
- const ROBOTA_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/robota.cjs');
19
+ const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
19
20
 
20
21
  /**
21
22
  * Signal the PTY child and await its ACTUAL exit before the caller removes the project dir.
22
23
  * `dispose()` sends SIGTERM (then SIGKILL after the grace window) but returns synchronously;
23
24
  * `expectExit()` resolves once the child has exited and **throws** if it has not within the timeout.
24
- * Awaiting this closes the INFRA-026 race where a dying child writes `<projectDir>/.robota` between
25
+ * Awaiting this closes the INFRA-026 race where a dying child writes under the selected state root between
25
26
  * `rmSync`'s readdir and rmdir. A child that refuses to die fails the test loudly — the exit is awaited
26
27
  * at the source, never masked by a removal fallback.
27
28
  * Extracted so the compose is unit-testable with a fake session.
@@ -69,7 +70,7 @@ export interface IPtySession {
69
70
  * Teardown that awaits ACTUAL child exit: signals the PTY child (`kill()`/`dispose()`) then awaits
70
71
  * `expectExit`, which resolves once the child has exited and **throws** if it has not within the
71
72
  * timeout (default ≥ the SIGTERM→SIGKILL grace). Await this before removing the project dir so the
72
- * dying child cannot write `<projectDir>/.robota` between `rmSync`'s readdir and rmdir (INFRA-026).
73
+ * dying child cannot write under the selected state root between `rmSync`'s readdir and rmdir (INFRA-026).
73
74
  */
74
75
  disposeAsync(timeoutMs?: number): Promise<void>;
75
76
  }
@@ -95,7 +96,7 @@ export function writeTuiProviderSettings(projectDir: string): void {
95
96
  // ARCH-042: a capabilityless built CLI is Restricted and must not read project settings. PTY
96
97
  // fixtures therefore configure the host-owned user layer under the same isolated HOME that
97
98
  // `spawnTui` derives from the fixture root.
98
- const settingsDir = join(projectDir, 'home', '.robota');
99
+ const settingsDir = join(projectDir, 'home', 'state');
99
100
  mkdirSync(settingsDir, { recursive: true });
100
101
  writeFileSync(
101
102
  join(settingsDir, 'settings.json'),
@@ -121,12 +122,13 @@ export function spawnTui(options: ISpawnTuiOptions): IPtySession {
121
122
 
122
123
  const session: IPtyRunSession = spawnPty({
123
124
  command: process.execPath,
124
- args: [ROBOTA_BIN, ...defaultedArgs],
125
+ args: [AGENT_BIN, ...defaultedArgs],
125
126
  cwd: options.projectDir,
126
127
  env: {
127
128
  PATH: process.env['PATH'] ?? '',
128
129
  HOME: options.homeDir,
129
130
  TERM: 'xterm-256color',
131
+ ...fixtureProductEnvironment(options.homeDir),
130
132
  // Never inherit real provider keys into PTY runs.
131
133
  ...(options.env ?? {}),
132
134
  },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * INFRA-018: deterministic conversation through the real binary via `--session-log` (replay).
3
3
  *
4
- * Runs in the PTY vitest project against the BUILT robota CLI. Boots with
4
+ * Runs in the PTY vitest project against the BUILT CLI. Boots with
5
5
  * `--session-log <fixture>` so the INFRA-017 ReplayProvider answers from a recorded log (no model
6
6
  * key, no network), sends a user message, and asserts the recorded assistant response renders and
7
7
  * commits to `<Static>` scrollback while the input stays pinned — closing SCREEN-010 TC-02/03
@@ -30,7 +30,7 @@ describe('replay conversation through the real binary (INFRA-018)', () => {
30
30
  let session: IPtySession | undefined;
31
31
 
32
32
  beforeEach(() => {
33
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-replay-pty-')));
33
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-replay-pty-')));
34
34
  writeTuiProviderSettings(projectDir); // dummy provider profile so the CLI boots; key never used
35
35
  });
36
36
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SCREEN-005 User Execution Test Scenarios — prompt footers and key-hint affordance on the BUILT
3
- * robota binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
3
+ * CLI binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
4
4
  *
5
5
  * Scenario 1: `/` opens the autocomplete → unified footer + `> ` indicator on the selected row.
6
6
  * Scenario 2: a replayed model turn drives a CMD-004 ask in each shape (single-select,
@@ -41,7 +41,7 @@ describe('SCREEN-005 prompt footers through the real binary', () => {
41
41
  let session: IPtySession | undefined;
42
42
 
43
43
  beforeEach(() => {
44
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-screen005-pty-')));
44
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen005-pty-')));
45
45
  writeTuiProviderSettings(projectDir);
46
46
  });
47
47
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SCREEN-006 User Execution Test Scenarios — color/motion degradation and palette-token colors on
3
- * the BUILT robota binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
3
+ * the BUILT CLI binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
4
4
  *
5
5
  * Scenario 1 (NO_COLOR legibility): `NO_COLOR=1` through a replayed conversation → the raw pty
6
6
  * transcript contains ZERO SGR color sequences (30-38/40-48/90-97/100-107 params, incl. 38;5 /
@@ -60,7 +60,7 @@ describe('SCREEN-006 color/motion through the real binary', () => {
60
60
  let session: IPtySession | undefined;
61
61
 
62
62
  beforeEach(() => {
63
- projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-screen006-pty-')));
63
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen006-pty-')));
64
64
  writeTuiProviderSettings(projectDir);
65
65
  });
66
66
 
@@ -126,9 +126,9 @@ describe('SCREEN-006 color/motion through the real binary', () => {
126
126
  // eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
127
127
  expect(raw).toMatch(/\x1b\[32m[^\x1b]*✓ Shell/);
128
128
 
129
- // Assistant label renders the accent token (cyan → ESC[36m ... "Robota:").
129
+ // Assistant label renders the accent token (cyan → ESC[36m ... "Fixture Agent:").
130
130
  // eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
131
- expect(raw).toMatch(/\x1b\[36m(?:\x1b\[[0-9;]*m)*Robota:/);
131
+ expect(raw).toMatch(/\x1b\[36m(?:\x1b\[[0-9;]*m)*Fixture Agent:/);
132
132
 
133
133
  // Markdown diff block carries the resolved theme's SGR pair (added line: light green on a dark
134
134
  // green background) — SCREEN-2002 moved the values into the theme, the bytes are unchanged.