evals 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,9 +12,11 @@ You'll see a picker of the coding agents installed on your machine (Claude Code,
12
12
 
13
13
  No coding agent installed? The picker shows install links instead.
14
14
 
15
+ Needs **Node.js 20 or newer** (the Ink UI does). On anything older, `npx evals` says so and points you at the shell launcher below rather than failing obscurely.
16
+
15
17
  ### Without Node
16
18
 
17
- If you don't have Node, use the shell launchers (they detect an agent and fetch the prompt from this published package via jsDelivr):
19
+ If you don't have Node — or have one older than 20 — use the shell launchers (they detect an agent and fetch the prompt from this published package via jsDelivr):
18
20
 
19
21
  ```bash
20
22
  # macOS / Linux
@@ -26,7 +28,7 @@ bash <(curl -fsSL https://cdn.jsdelivr.net/npm/evals/start.sh)
26
28
  irm https://cdn.jsdelivr.net/npm/evals/start.ps1 | iex
27
29
  ```
28
30
 
29
- When Node **is** present, these hand off to `npx evals` for the richer UI.
31
+ When **Node 20+** is present, these hand off to `npx evals` for the richer UI. On an older Node they say so and carry on in the shell, which needs no Node at all.
30
32
 
31
33
  ## What it does
32
34
 
@@ -34,7 +36,7 @@ When Node **is** present, these hand off to `npx evals` for the richer UI.
34
36
  2. Launches the agent in your current directory, seeded with the bundled onboarding prompt ([`onboarding-prompt.md`](./onboarding-prompt.md)).
35
37
  3. The agent walks you through: create/sign in to Arize AX → pick what to trace → instrument it → verify your first traces.
36
38
 
37
- You can trace an app in the current folder, a starter app the agent creates, or **the coding agent itself** — so every session you run, in any project, shows up in Arize AX. Agent tracing is machine-wide and captures prompts and tool output by default, so the prompt asks for that explicitly and offers per-category opt-outs.
39
+ You can trace an existing app (the one in the current folder, or another one you point it at by path), a starter app the agent creates, or **the coding agent itself** — so every session you run, in any project, shows up in Arize AX. Agent tracing is machine-wide and captures prompts and tool output by default, so the prompt asks for that explicitly and offers per-category opt-outs.
38
40
 
39
41
  The agent runs with its **normal permission model** — `evals` never passes skip-permissions, so you approve each step, and the prompt itself gates real changes on your confirmation.
40
42
 
@@ -45,9 +47,12 @@ The agent runs with its **normal permission model** — `evals` never passes ski
45
47
  | `ARIZE_AGENT=<id>` | `start.sh` / `start.ps1` | Skip the picker and use this agent (`claude`, `codex`, `cursor-agent`, `copilot`, `gemini`). |
46
48
  | `ARIZE_SKIP_NPX=1` | `start.sh` / `start.ps1` | Force the shell path even when Node/npx is available. |
47
49
  | `ARIZE_PROMPT_URL=<url>` | `start.sh` / `start.ps1` | Fetch the onboarding prompt from a custom URL (supports `file://`). |
50
+ | `ARIZE_ONBOARDING_DIR=<dir>` | all three | Stage the prompt somewhere other than `~/.arize/onboarding`. |
48
51
 
49
52
  The shell launchers also accept `--agent <id>`.
50
53
 
54
+ The prompt is staged in `~/.arize/onboarding` and handed to your agent by path, since it's too large to pass as a command-line argument. That directory is cleared at the start of each launch and holds nothing but the prompt and the offline tracing bundle. If your home directory isn't writable, the launcher says so and falls back to a temp directory — instrumenting an app still works, but tracing the coding agent itself needs a writable home.
55
+
51
56
  ## About Arize
52
57
 
53
58
  [Arize AX](https://arize.com/docs/ax) is the AI engineering platform for tracing, evaluating, and observing LLM and agent applications. Learn more at [arize.com](https://arize.com).
package/bin.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // Node version gate for `npx evals`.
4
+ //
5
+ // This is a separate file from cli.js on purpose. ESM `import` statements are
6
+ // hoisted and evaluated before any of the importing module's own code runs, so a
7
+ // check at the top of cli.js would execute *after* Ink's dependency graph loads —
8
+ // too late. On Node 18 that graph dies while parsing string-width with
9
+ // "SyntaxError: Invalid regular expression flags" (the regex `v` flag is Node 20+),
10
+ // which tells the user nothing about Node versions. So: check here, where nothing
11
+ // but Node builtins have loaded, then reach cli.js through a dynamic import.
12
+ //
13
+ // Keep this file dependency-free and conservative in syntax — it has to parse and
14
+ // run on the very versions it exists to reject.
15
+ //
16
+ // MIN_MAJOR tracks ink's own `engines.node`, and package.json's `engines` field
17
+ // must say the same thing; test/cli.test.js asserts they agree.
18
+
19
+ const MIN_MAJOR = 20;
20
+
21
+ const major = Number.parseInt(process.versions.node.split('.')[0], 10);
22
+
23
+ if (!Number.isInteger(major) || major < MIN_MAJOR) {
24
+ const shell = process.platform === 'win32'
25
+ ? 'irm https://cdn.jsdelivr.net/npm/evals/start.ps1 | iex'
26
+ : 'bash <(curl -fsSL https://cdn.jsdelivr.net/npm/evals/start.sh)';
27
+
28
+ console.error(`evals needs Node.js ${MIN_MAJOR} or newer — this is ${process.version}.`);
29
+ console.error('');
30
+ console.error('Either upgrade Node (https://nodejs.org), or use the launcher that');
31
+ console.error('needs no Node at all — it runs the same onboarding flow:');
32
+ console.error('');
33
+ console.error(` ${shell}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ // cli.js only auto-runs when it is the entry point, so call main() explicitly.
38
+ import('./cli.js')
39
+ .then((cli) => cli.main())
40
+ .catch((err) => {
41
+ console.error(`Could not start evals: ${err && err.message ? err.message : err}`);
42
+ process.exit(1);
43
+ });
package/cli.js CHANGED
@@ -9,24 +9,27 @@ import {
9
9
  readFileSync,
10
10
  writeFileSync,
11
11
  mkdtempSync,
12
+ mkdirSync,
12
13
  realpathSync,
13
14
  readdirSync,
14
15
  copyFileSync,
15
16
  chmodSync,
16
17
  renameSync,
17
18
  rmSync,
19
+ rmdirSync,
20
+ unlinkSync,
18
21
  } from 'fs';
19
- import { join } from 'path';
20
- import { tmpdir } from 'os';
22
+ import { join, isAbsolute, resolve } from 'path';
23
+ import { tmpdir, homedir } from 'os';
21
24
  import { fileURLToPath } from 'url';
22
25
 
23
26
  const e = React.createElement;
24
27
 
25
- // The onboarding prompt is bundled with this package (onboarding-prompt.md, a
26
- // copy of the docs landing-page prompt). We read it, write it to a temp file,
27
- // and tell the agent to read that file a ~27 KB prompt is too large to pass
28
- // reliably as a command-line argument.
29
- // TODO: sync this copy with the docs source (arize.com/docs) later.
28
+ // The onboarding prompt is bundled with this package (onboarding-prompt.md).
29
+ // This repo is its only home there is no docs original to sync against, so
30
+ // edit it here. We read it, write it to a temp file, and tell the agent to read
31
+ // that file — a ~35 KB prompt is too large to pass reliably as a command-line
32
+ // argument.
30
33
  const BUNDLED_PROMPT_PATH = fileURLToPath(new URL('./onboarding-prompt.md', import.meta.url));
31
34
 
32
35
  // Wheels for the coding-agent tracing harness, built by scripts/build-harness-wheel.mjs.
@@ -41,6 +44,106 @@ const OFFLINE_DIR_NAME = 'arize-offline';
41
44
  // would send the agent down the offline path with no wheel to install.
42
45
  const OFFLINE_FILES = ['harness-install.sh', 'harness-install.bat', 'LICENSE-coding-harness-tracing'];
43
46
 
47
+ const PROMPT_FILE_NAME = 'onboarding-prompt.md';
48
+
49
+ // Step 4A writes the harness credentials here and deletes them itself. We name it
50
+ // only so a stray one — left by a run that died between writing and deleting —
51
+ // gets cleared at the next launch instead of sitting around holding a live key.
52
+ const ENV_FILE_NAME = 'harness.env';
53
+
54
+ // MANIFEST only lands in the shell launchers' bundles, never in ours. Naming it
55
+ // anyway keeps one allowlist correct for every implementation, so a directory
56
+ // staged by start.sh can be cleared by `npx evals` and vice versa.
57
+ const BUNDLE_EXTRA_FILES = ['MANIFEST'];
58
+
59
+ // Where the prompt and the offline bundle get staged: `~/.arize/onboarding`.
60
+ // Deliberately stable rather than a fresh temp directory. The agent is asked to
61
+ // read a file outside its workspace, and that is an approval a human can grant
62
+ // with confidence for `~/.arize/onboarding/onboarding-prompt.md`, where a
63
+ // `/var/folders/_s/3_t5nrxs…/T/arize-onboarding-c5uxb8/` path just looks alarming.
64
+ //
65
+ // This sits beside Step 4A's own `~/.arize/harness` install, which the launcher
66
+ // must never touch — clearing our staging directory cannot uninstall a working
67
+ // harness, and that boundary is why we only ever name files under `onboarding/`.
68
+ export function resolveOnboardingDir({
69
+ home = homedir(),
70
+ override = process.env.ARIZE_ONBOARDING_DIR,
71
+ } = {}) {
72
+ if (override) return resolve(override);
73
+ return join(home, '.arize', 'onboarding');
74
+ }
75
+
76
+ // Clear a staged bundle directory by naming every entry we could have put in it,
77
+ // then rmdir. Bails out — deleting nothing further — the moment it meets an entry
78
+ // it doesn't recognise. Wheel names carry a version we can't know ahead of time,
79
+ // so `.whl` is matched by extension, non-recursively, inside this one directory.
80
+ function clearBundleDir(dir) {
81
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
82
+ const known =
83
+ entry.name.endsWith('.whl') ||
84
+ OFFLINE_FILES.includes(entry.name) ||
85
+ BUNDLE_EXTRA_FILES.includes(entry.name);
86
+ if (entry.isDirectory() || !known) return false;
87
+ unlinkSync(join(dir, entry.name));
88
+ }
89
+ rmdirSync(dir);
90
+ return true;
91
+ }
92
+
93
+ // Empty the staging directory by deleting the exact files we create — never with
94
+ // a recursive delete. Two things fall out of that: a bad path can at worst try to
95
+ // unlink a handful of names that won't exist, and `rmdir` refuses a non-empty
96
+ // directory, so anything unexpected in there stops us instead of being destroyed.
97
+ // Unlinking a symlink removes the link and never the target.
98
+ //
99
+ // Returns true when the directory is gone or empty and safe to stage into.
100
+ export function clearOnboardingDir(dir) {
101
+ if (!isAbsolute(dir)) return false;
102
+ if (!existsSync(dir)) return true;
103
+
104
+ try {
105
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
106
+ const path = join(dir, entry.name);
107
+
108
+ if (entry.isDirectory()) {
109
+ // arize-offline/ is ours; .staging-* is the leftover of a crashed run.
110
+ const ours = entry.name === OFFLINE_DIR_NAME || entry.name.startsWith('.staging-');
111
+ if (!ours || !clearBundleDir(path)) return false;
112
+ continue;
113
+ }
114
+
115
+ if (entry.name !== PROMPT_FILE_NAME && entry.name !== ENV_FILE_NAME) return false;
116
+ unlinkSync(path);
117
+ }
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ // Clear and create the staging directory, degrading to a temp directory when the
125
+ // home path can't be used: a read-only home, a directory owned by another user
126
+ // after a past `sudo npx evals`, a full disk, an offline redirected profile on
127
+ // Windows, or an unrecognised file we refuse to delete. We attempt the work and
128
+ // degrade rather than probing for writability first — a probe lies on NFS and
129
+ // ACL filesystems, races, and can't see ENOSPC at all.
130
+ //
131
+ // Mode 0700 because Step 4A writes credentials into this directory.
132
+ export function prepareOnboardingDir(dir = resolveOnboardingDir()) {
133
+ if (clearOnboardingDir(dir)) {
134
+ try {
135
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
136
+ // mkdir's mode is umask-filtered, so set it outright. Windows has no POSIX
137
+ // mode; there the file inherits the profile's own ACL, as it does today.
138
+ if (process.platform !== 'win32') chmodSync(dir, 0o700);
139
+ return { dir, fellBack: false };
140
+ } catch {
141
+ // Fall through to the temp directory.
142
+ }
143
+ }
144
+ return { dir: mkdtempSync(join(tmpdir(), 'arize-onboarding-')), fellBack: true };
145
+ }
146
+
44
147
  // Copy the bundled wheels next to the prompt. Returns the staged directory, or
45
148
  // null when this package has no usable vendor/ (a git checkout that hasn't run
46
149
  // the build). The shell launchers stage the same bundle themselves, fetching it
@@ -77,15 +180,22 @@ export function stageOfflineHarness(dir, vendorDir = VENDOR_DIR) {
77
180
  }
78
181
  }
79
182
 
80
- // Write the bundled prompt to a temp file and return a short seed instruction
81
- // that points the agent at it.
183
+ // Stage the bundled prompt and return the seed instruction that points the agent
184
+ // at it, plus whether we had to fall back off the home directory.
82
185
  function prepareSeedPrompt() {
83
186
  const promptText = readFileSync(BUNDLED_PROMPT_PATH, 'utf8');
84
- const dir = mkdtempSync(join(tmpdir(), 'arize-onboarding-'));
85
- const promptFile = join(dir, 'onboarding-prompt.md');
187
+ const { dir, fellBack } = prepareOnboardingDir();
188
+ const promptFile = join(dir, PROMPT_FILE_NAME);
86
189
  writeFileSync(promptFile, promptText, 'utf8');
87
190
  stageOfflineHarness(dir);
88
- return `Read the file ${promptFile} and follow it to set up Arize AX tracing in this project, walking me through each step and asking me questions as needed.`;
191
+ // Single quotes, not double: a home path is likelier to contain a space than a
192
+ // temp path was, but Windows spawns through the shell and embedded double
193
+ // quotes don't survive that reliably. cmd.exe leaves single quotes alone.
194
+ // No "in this project": the launcher may well be run from a home directory, and
195
+ // Step 4 is what scopes the work — an app here, an app at another path, a starter
196
+ // app, or the coding agent itself. Keep this wording in step with start.{sh,ps1}.
197
+ const seed = `Read the file '${promptFile}' and follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed.`;
198
+ return { seed, dir, fellBack };
89
199
  }
90
200
 
91
201
  // Coding agents we can launch interactively, seeded with the prompt.
@@ -351,17 +461,26 @@ function App({ onDone }) {
351
461
  function launchAgent(agent) {
352
462
  const isWin = process.platform === 'win32';
353
463
 
354
- let seed;
464
+ let prepared;
355
465
  try {
356
- seed = prepareSeedPrompt();
466
+ prepared = prepareSeedPrompt();
357
467
  } catch (err) {
358
- console.error(`Could not read the onboarding prompt: ${err.message}`);
468
+ console.error(`Could not prepare the onboarding prompt: ${err.message}`);
359
469
  process.exit(1);
360
470
  }
361
471
 
472
+ // Say so when we couldn't use the home directory. Otherwise the seed path
473
+ // silently reverts to the /var/folders/… form this change exists to avoid, and
474
+ // Step 4A is going to fail later anyway — it writes the harness and its
475
+ // credentials under ~/.arize regardless of where the prompt was staged.
476
+ if (prepared.fellBack) {
477
+ console.log(`\nCouldn't use ${resolveOnboardingDir()} — staged in ${prepared.dir} instead.`);
478
+ console.log('Instrumenting an app still works; tracing this coding agent (Step 4A) needs a writable home directory.');
479
+ }
480
+
362
481
  console.log(`\nLaunching ${agent.label}…\n`);
363
482
 
364
- const child = spawn(agent.bin, agent.args(seed), {
483
+ const child = spawn(agent.bin, agent.args(prepared.seed), {
365
484
  stdio: 'inherit',
366
485
  shell: isWin // .cmd/.bat shims on Windows need the shell to resolve
367
486
  });
@@ -6,15 +6,14 @@ Work through the flow below in order. Installing the AX CLI and Arize skills, au
6
6
 
7
7
  ## Step 0: Welcome and confirm
8
8
 
9
- Greet the user and show the plan, then ask before doing anything:
9
+ Greet the user and show the plan, then ask before doing anything. Send this block as written and nothing else: no added steps, no parentheticals on the ones here (not "browser OAuth", not a folder path), and no preamble about the network check or the approval gate. Both come later in the flow, and explaining them before the user has agreed to anything is noise.
10
10
 
11
11
  ```text
12
12
  Welcome to Arize AX. I'll get you set up with tracing. Here's what I'll do:
13
13
 
14
14
  1. Install the AX CLI and Arize skills
15
15
  2. Create a free Arize AX account or sign you in
16
- 3. Add tracing to your app, a new starter app, or this coding agent itself
17
- 4. Capture your first traces
16
+ 3. Add tracing to an existing app, a new starter app, or this coding agent itself
18
17
 
19
18
  Shall I proceed?
20
19
  ```
@@ -23,6 +22,26 @@ Do not proceed until the user approves.
23
22
 
24
23
  ## Prerequisites
25
24
 
25
+ **Check network access first.** Every step below needs it: the CLI comes from PyPI, and auth and traces go to Arize. Agent sandboxes often block network by default, and it fails as a hang rather than an error:
26
+
27
+ ```bash
28
+ for h in pypi.org registry.npmjs.org api.arize.com app.arize.com; do
29
+ code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "https://$h")
30
+ [ "$code" != "000" ] && echo "$h ok" || echo "$h BLOCKED"
31
+ done
32
+ ```
33
+
34
+ `000` is a DNS or connect failure — your sandbox, not the user's connection.
35
+
36
+ If anything is blocked, ask your harness to escalate: request network or elevated permissions for the commands that need them, rather than retrying silently or waiting. If escalation is refused or your harness doesn't offer it, stop — with no network this flow cannot complete, so don't retry and don't look for a workaround:
37
+
38
+ ```text
39
+ My sandbox is blocking network access, so I can't install the CLI or reach
40
+ Arize. Restart me with network access enabled and I'll pick this up.
41
+ ```
42
+
43
+ Later in the flow, treat a connection or DNS error from any command the same way: it means network, not authentication, so stop and report rather than re-authenticating or recreating a profile.
44
+
26
45
  The AX CLI must be **arize-ax-cli `0.29.0` or newer** (Step 1 installs the latest) and needs **Python 3.11+** — a hard requirement. If Python is missing, stop and have the user install it from https://www.python.org/downloads/ and re-run. **Node.js 18+ with npx** is optional but installs the Arize skills; without it, don't stop — continue and use the docs paths in Step 6 (the Vercel AI SDK v7 starter needs Node.js 22+).
27
46
 
28
47
  ## Step 1: Install or update the AX CLI and Arize skills
@@ -35,7 +54,7 @@ pipx install arize-ax-cli # already installed: pipx upgrade arize-ax-cli
35
54
  python3 -m pip install --upgrade arize-ax-cli
36
55
  ```
37
56
 
38
- If npx is available, install the Arize agent skills; otherwise skip and continue (Step 6 falls back to the docs paths):
57
+ If npx is available, install the Arize agent skills; otherwise skip and continue (Step 6 falls back to the docs paths). If the install errors or doesn't finish promptly, skip it and continue rather than waiting — the same fallback applies:
39
58
 
40
59
  ```bash
41
60
  npx skills add Arize-ai/arize-skills --skill '*' --yes
@@ -72,7 +91,7 @@ If a `default` profile exists but the probe failed, it's signed out or expired,
72
91
  ax profiles create default --auth-method api-key --api-key "$ARIZE_API_KEY"
73
92
  ```
74
93
 
75
- Reuse this key throughout — do **not** create a new one in Step 6.
94
+ Reuse this key throughout — do **not** create a new one in Step 6. Only if the probe passes, though: a stale or wrong-org `ARIZE_API_KEY` still looks present. If it fails, `ax profiles delete default`, take the OAuth path below, and let Step 6 create a key as normal.
76
95
 
77
96
  - **No key anywhere** — sign up or sign in with browser OAuth:
78
97
 
@@ -80,7 +99,7 @@ If a `default` profile exists but the probe failed, it's signed out or expired,
80
99
  ax profiles create default --auth-method oauth --utm-params "utm_source=npmevals&utm_medium=cli&utm_campaign=prompt-first-onboarding"
81
100
  ```
82
101
 
83
- The `--utm-params` flag tags a new sign-up with onboarding attribution. It's already included on the three browser OAuth commands that take it — the two `ax profiles create --auth-method oauth` calls and the `ax auth login` fallback. Pass those verbatim; don't drop it, change the values, or add the flag to the api-key `profiles create` or any other `ax` command.
102
+ Pass `--utm-params` verbatim on the two browser OAuth commands that carry it (`profiles create --auth-method oauth` and the `ax auth login` fallback) and add it to no other `ax` command it tags a new sign-up with onboarding attribution.
84
103
 
85
104
  Always pass the positional profile name `default`. Without it, the CLI prompts `profile name [default]:`, receives EOF from an agent-run command, and exits with `Goodbye!` without creating a profile.
86
105
 
@@ -88,39 +107,23 @@ The rest of this step applies only to the **browser OAuth** branch.
88
107
 
89
108
  Creating an OAuth profile **is** the browser login flow. Treat it as an interactive browser handoff: it opens a browser, starts a localhost callback server such as `127.0.0.1:<port>/callback`, and waits for the browser redirect. The command must stay alive until the redirect lands and the CLI exits on its own.
90
109
 
91
- While the OAuth command is waiting, do not close its stdin, send Ctrl-C, `pkill` the AX process, start a second auth command, or run an auth probe. Any of these aborts the in-progress browser flow. There is one deliberate exception — the new email/password signup case described below, where the callback never lands and the command must be restarted.
110
+ While the OAuth command is waiting, do not close its stdin, send Ctrl-C, `pkill` the AX process, start a second auth command, or run an auth probe. Any of these aborts the in-progress browser flow. There is no exception: every sign-in path existing account, Google/SSO, existing or brand-new email/password completes through the same callback, so the command always gets there on its own.
92
111
 
93
112
  After launching the OAuth command, tell the user:
94
113
 
95
114
  ```text
96
115
  A browser window is opening for Arize AX sign-in.
97
116
 
98
- - Already have an account, or signing in with Google/SSO or an existing
99
- email/password? Just finish in the browser and I'll continue automatically.
100
- - Creating a BRAND-NEW account with email and password? Arize emails you a
101
- validation link. That link does NOT complete the CLI login, so this command
102
- will hang. Click the link to finish creating your account, then tell me.
117
+ Sign in or create a new account in the browser. I'll continue
118
+ automatically once it completes.
103
119
 
104
- Did you just sign up for a new account with email and password?
120
+ Creating a BRAND-NEW account with email and password? Arize emails you a
121
+ validation link. Click it and finish in the browser; I'll wait.
105
122
  ```
106
123
 
107
- **If the user says yes (new email/password signup):** the localhost callback will never fire and the command will wait forever, so this is the one time you break the "keep it alive" rule. Once they confirm they've clicked the validation link and their account exists, terminate the waiting OAuth command (send Ctrl-C / kill that process), then re-run it:
124
+ Then wait for the command to exit on its own. Treat exit code `0`, or CLI success output such as `Configuration saved to profile 'default'` or `Active profile set`, as the primary completion signal. A new email/password sign-up can sit waiting for a while the user has to find the validation email so a long wait is not a hang; leave it alone.
108
125
 
109
- ```bash
110
- ax profiles create default --auth-method oauth --utm-params "utm_source=npmevals&utm_medium=cli&utm_campaign=prompt-first-onboarding"
111
- ```
112
-
113
- The second run is now a plain sign-in for the validated account. Its callback lands normally, so handle it with all the standard rules below — keep it alive and wait for it to exit on its own.
114
-
115
- **Otherwise (existing account, SSO, or existing email/password):** wait for the command to exit on its own. Treat exit code `0`, or CLI success output such as `Configuration saved to profile 'default'` or `Active profile set`, as the primary completion signal.
116
-
117
- Only after the OAuth command completes, verify authentication with a non-secret probe:
118
-
119
- ```bash
120
- ax spaces list --limit 1 --output json
121
- ```
122
-
123
- If the probe succeeds, continue. If the default OAuth profile already existed and the probe returns an authentication error, the profile is signed out or expired — run the fallback:
126
+ Only after the OAuth command completes, re-run the probe from the top of this step. If it succeeds, continue. If the default OAuth profile already existed and the probe returns an authentication error, the profile is signed out or expired — run the fallback:
124
127
 
125
128
  ```bash
126
129
  ax auth login --utm-params "utm_source=npmevals&utm_medium=cli&utm_campaign=prompt-first-onboarding"
@@ -151,16 +154,20 @@ Ask what the user wants to trace before inspecting anything — the third option
151
154
  ```text
152
155
  What would you like to trace?
153
156
 
154
- 1. An app in this folder
157
+ 1. An existing app in this folder or at another path
155
158
  2. A new starter app I create for you
156
159
  3. This coding agent itself — every session you run, in any project
157
160
 
158
161
  Which one?
159
162
  ```
160
163
 
161
- If they pick **3**, go to [Step 4A](#step-4a-trace-this-coding-agent) and skip the folder inspection entirely. For **1** or **2**, continue below.
164
+ If they pick **3**, go to [Step 4A](#step-4a-trace-this-coding-agent) and skip the folder inspection entirely. If they pick **2**, go straight to "Create a starter app" below — don't inspect anything. For **1**, continue.
165
+
166
+ For options **1** and **2**, one directory ends up being **the app folder**: the app you instrument, which is not always the folder you were launched in. Establish it in this step and use it for every path in Steps 5 to 7.
162
167
 
163
- Inspect the current folder to decide whether an app already exists. Do not change files during inspection. Look for:
168
+ ### Detecting an app
169
+
170
+ Inspect a candidate folder to decide whether an app already exists. Do not change files during inspection. Look for:
164
171
 
165
172
  - Python: `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile`, imports.
166
173
  - TypeScript/JavaScript: `package.json`, lockfiles, `src`, `app`, `pages`, provider imports.
@@ -168,24 +175,48 @@ Inspect the current folder to decide whether an app already exists. Do not chang
168
175
  - Existing observability: `opentelemetry`, `TracerProvider`, `ARIZE_*`, `OTEL_*`, `OTLP_*`, Datadog, Honeycomb, Sentry, or other tracing.
169
176
  - Agent framework: identify it by its import/package — e.g. `langchain` / `langgraph`, `llama_index`, `crewai`, `autogen`, `semantic_kernel`, `pydantic_ai`, `google.adk`, `dspy`, `agent_framework`, and others. **Route on the framework, not the provider client it wraps** — an `openai` or `anthropic` import inside a framework app is not the thing to instrument; the framework almost certainly has its own integration (see Step 6).
170
177
 
171
- In a monorepo, check the git root to get oriented, but only instrument apps in or below the current working directory. If the project spans more than one language, instrument each one (route each through its own integration page in Step 6).
178
+ In a monorepo, check the git root to get oriented, but only instrument apps in or below the folder you are scanning. If the project spans more than one language, instrument each one (route each through its own integration page in Step 6).
172
179
 
173
- Then branch on what you found a folder with no app means option 2:
180
+ Run this detection on the current folder first, then branch on what you found.
174
181
 
175
182
  ### If an app exists in the current folder
176
183
 
177
- Do not offer the starter-app path. Summarize the detected stack and offer to instrument it:
184
+ Summarize the detected stack and offer both routes — declining the local app must not be a dead end, because the user may well have run this from a folder above the app they care about:
178
185
 
179
186
  ```text
180
- I found a <language>/<framework> app in this folder. Want me to add Arize AX
181
- tracing to it?
187
+ I found a <language>/<framework> app in this folder. I can trace that, or an
188
+ app somewhere else.
189
+
190
+ 1. Trace the app in this folder
191
+ 2. Trace an app at a different path — tell me where
192
+
193
+ Which one?
182
194
  ```
183
195
 
184
- If the user declines, stop here rather than falling back to a starter app.
196
+ On **1**, the app folder is the current folder; continue to Step 5. On **2**, follow "If the user gives a path" below. Don't offer the starter-app path here — they already have an app.
185
197
 
186
198
  ### If no app exists in the current folder
187
199
 
188
- Go straight to the starter-app choice — do not ask whether to instrument the empty folder. Ask which folder to create it in, then offer these choices:
200
+ Do not ask whether to instrument the empty folder, and do not assume a starter app is what they want. Ask for a path first:
201
+
202
+ ```text
203
+ I don't see an app in this folder. Give me the path to the app you want to
204
+ trace, and I'll instrument it — or say "starter" and I'll create a new app
205
+ for you instead.
206
+ ```
207
+
208
+ On a path, follow the next section. On "starter", go to "Create a starter app".
209
+
210
+ ### If the user gives a path
211
+
212
+ - Expand `~` and resolve a relative path against the current folder, then confirm the resolved absolute path back to the user before you scan it. A typo caught here beats an API key written into the wrong repo.
213
+ - Run the detection checklist on that folder. If it holds an app, that folder is the app folder from now on — summarize the stack you found there and continue to Step 5.
214
+ - If the path doesn't exist, or exists but has no app in it, say what you actually found and ask for another path. Don't quietly fall through to a starter app.
215
+ - If your own permission or sandbox layer won't let you read and write outside the folder you were launched in, say so plainly instead of half-instrumenting the app. Give the two ways out: the user grants access to that path, or they re-run `npx evals` from inside the app folder.
216
+
217
+ ### Create a starter app
218
+
219
+ For option 2, or when the user asks for a starter app after the questions above. Ask which folder to create it in — that folder becomes the app folder — then offer these choices:
189
220
 
190
221
  - OpenAI — Python or TypeScript (with a tool call)
191
222
  - Anthropic — Python (with a tool call; official AX auto-instrumentation)
@@ -202,6 +233,16 @@ Installer harness names: `claude`, `codex`, `cursor`, `copilot`, `gemini`, `kiro
202
233
 
203
234
  Per-agent setup, including the Claude Code and Cursor marketplace-plugin routes, is at `https://arize.com/docs/ax/integrations/platforms/<agent>/<agent>-tracing` (e.g. `.../claude-code/claude-code-tracing`) — the source of truth if anything below fails.
204
235
 
236
+ ### Check the home directory is writable first
237
+
238
+ Everything below writes under `~/.arize` — the credentials file, then the harness itself. Confirm that works before asking for approval, so a read-only or restricted home fails here rather than halfway through an install the user already said yes to:
239
+
240
+ ```bash
241
+ mkdir -p ~/.arize/onboarding && touch ~/.arize/onboarding/.probe && rm -f ~/.arize/onboarding/.probe
242
+ ```
243
+
244
+ If that fails, say so and stop this path — agent tracing needs a writable home and there is no workaround from inside the session. Instrumenting an app (options 1 and 2) does not, so offer that instead. If the launcher already told the user it couldn't use `~/.arize/onboarding`, this is the same cause; don't re-diagnose it.
245
+
205
246
  ### Get approval — this needs its own explicit yes
206
247
 
207
248
  Wider scope than an app install, so disclose it and wait:
@@ -227,7 +268,7 @@ Do not proceed without a yes. Note which categories they accepted — you enable
227
268
  The installer reads credentials from a dotenv file, keeping the API key out of the command line, shell history, and this chat. Write it outside the project so it cannot be committed:
228
269
 
229
270
  ```bash
230
- mkdir -p ~/.arize && : > ~/.arize/onboarding.env && chmod 600 ~/.arize/onboarding.env
271
+ : > ~/.arize/onboarding/harness.env && chmod 600 ~/.arize/onboarding/harness.env
231
272
  ```
232
273
 
233
274
  Add the Step 3 space ID as file contents, plus one `true` line per category the user **accepted**. Unattended installs capture nothing unless asked to, so a category you omit is off — omit the line for anything they declined:
@@ -242,7 +283,7 @@ ARIZE_LOG_TOOL_CONTENT=true
242
283
  Do **not** set `ARIZE_PROJECT_NAME` — each harness defaults to its own project (`claude-code`, `codex`, …), which keeps two traced agents apart, and you read the real name back after installing. Then create the key into the same file, written atomically and never printed:
243
284
 
244
285
  ```bash
245
- ax api-keys create --name "Coding agent tracing" --env-file ~/.arize/onboarding.env
286
+ ax api-keys create --name "Coding agent tracing" --env-file ~/.arize/onboarding/harness.env
246
287
  ```
247
288
 
248
289
  **Skip that if `ARIZE_API_KEY` already existed in Step 2** — copy the existing value in without echoing it. At most one key, ever.
@@ -254,7 +295,7 @@ If a directory named `arize-offline/` sits beside this prompt file, install from
254
295
  ```bash
255
296
  # keep `< /dev/null`: an installer too old for --non-interactive then fails
256
297
  # fast instead of hanging on a prompt you cannot answer
257
- ARIZE_ENV_FILE=~/.arize/onboarding.env \
298
+ ARIZE_ENV_FILE=~/.arize/onboarding/harness.env \
258
299
  bash <prompt-dir>/arize-offline/harness-install.sh \
259
300
  <harness> --wheel-dir <prompt-dir>/arize-offline --non-interactive < /dev/null
260
301
  ```
@@ -262,7 +303,7 @@ ARIZE_ENV_FILE=~/.arize/onboarding.env \
262
303
  Otherwise fetch it (the harness name must come first, before any flag):
263
304
 
264
305
  ```bash
265
- ARIZE_ENV_FILE=~/.arize/onboarding.env \
306
+ ARIZE_ENV_FILE=~/.arize/onboarding/harness.env \
266
307
  bash <(curl -fsSL https://raw.githubusercontent.com/Arize-ai/coding-harness-tracing/main/install.sh) \
267
308
  <harness> --non-interactive < /dev/null
268
309
  ```
@@ -270,14 +311,14 @@ ARIZE_ENV_FILE=~/.arize/onboarding.env \
270
311
  Delete the env file only once the install has actually run — a retry needs it, and so does the user if they end up running the command:
271
312
 
272
313
  ```bash
273
- rm -f ~/.arize/onboarding.env
314
+ rm -f ~/.arize/onboarding/harness.env
274
315
  ```
275
316
 
276
317
  On Windows, use `harness-install.bat` from the same directory — `cmd` cannot run the `.sh`:
277
318
 
278
319
  ```powershell
279
- $env:ARIZE_ENV_FILE = "$HOME\.arize\onboarding.env"
280
- & <prompt-dir>\arize-offline\harness-install.bat <harness> --wheel-dir <prompt-dir>\arize-offline --non-interactive
320
+ $env:ARIZE_ENV_FILE = "$HOME\.arize\onboarding\harness.env"
321
+ & "<prompt-dir>\arize-offline\harness-install.bat" <harness> --wheel-dir "<prompt-dir>\arize-offline" --non-interactive
281
322
  ```
282
323
 
283
324
  Without `arize-offline/`, use the install command from the agent's docs page with `$env:ARIZE_ENV_FILE` set first.
@@ -320,19 +361,21 @@ hooks existed. So:
320
361
  3. Tell me when you have, and I'll check for traces.
321
362
  ```
322
363
 
323
- Then poll with the Step 7 command and counter, passing that project name and space ID, every ~15 seconds for up to ~3 minutes. On a non-zero count go to Step 8. On timeout say so plainly and give the likely causes: the new session started before the install finished, the agent was never asked to do anything, `ARIZE_TRACE_ENABLED` is `false` in the agent's settings, or the wrong space. Never fabricate trace results.
364
+ Then poll with the Step 7 command and counter, passing that project name and space ID, every ~15 seconds for up to ~3 minutes. On a non-zero count go to Step 8. On timeout say so plainly and give the likely causes: the new session started before the install finished, the agent was never asked to do anything, `ARIZE_TRACE_ENABLED` is `false` in the agent's settings, or the wrong space.
324
365
 
325
366
  ## Step 5: Present the plan and get approval
326
367
 
327
- Steps 5 to 7 cover the app paths (options 1 and 2). If you took Step 4A, its own approval gate and verification replace them — go straight to Step 8.
368
+ Steps 5 to 7 are for the app paths only; if you took Step 4A, go straight to Step 8.
328
369
 
329
- Before creating any remote resource, writing files, or installing dependencies, present one consolidated plan and wait for approval. This is the gate the intro refers to — nothing so far has modified the app or created AX resources.
370
+ Before creating any remote resource, writing files, or installing dependencies, present one consolidated plan and wait for approval.
330
371
 
331
372
  For an existing app, cover: detected language and framework, package manager, LLM provider or agent framework, any existing tracing to preserve, the env file that will be updated, the instrumentation packages and files that will change, whether a new AX user API key will be created or the existing `ARIZE_API_KEY` reused, and the project name that will be used.
332
373
 
333
374
  For a starter app, cover: the chosen provider and language, the target folder, the packages that will be installed, and the project name.
334
375
 
335
- Choose a default project name from the current folder or app name: lowercase it, replace spaces and unsupported punctuation with hyphens, and append `-arize-tracing` if it is too generic. Do not create an AX project explicitly it is created on first trace ingestion.
376
+ **State the app folder's absolute path in the plan whenever it isn't the folder you were launched in** every file you touch lands there, and it's the one detail the user cannot verify from context.
377
+
378
+ Choose a default project name from the app folder or app name: lowercase it, replace spaces and unsupported punctuation with hyphens, and append `-arize-tracing` if it is too generic. Do not create an AX project explicitly — it is created on first trace ingestion.
336
379
 
337
380
  ```text
338
381
  Here's my plan. Shall I proceed?
@@ -346,7 +389,7 @@ Only after approval, execute the plan in this order.
346
389
 
347
390
  ### Choose the env file
348
391
 
349
- Pick the env file to match the app and use it for every variable below:
392
+ The env file lives in the **app folder** from Step 4. Pick its name to match the app and use that one file for every variable below:
350
393
 
351
394
  - Next.js, Vite, or browser-adjacent TypeScript apps: `.env.local`
352
395
  - Python apps, Node scripts, backend services, or unknown type: `.env`
@@ -354,7 +397,7 @@ Pick the env file to match the app and use it for every variable below:
354
397
 
355
398
  Do not read existing env file contents into chat. Preserve unrelated variables and never reveal their values.
356
399
 
357
- Make sure the env file is git-ignored before writing the API key to it — if the repo has a `.gitignore`, confirm it covers the file (add `.env` / `.env.local` if not); for a starter app you create, add one. The API key must never be committed to version control.
400
+ Make sure the env file is git-ignored before writing the API key to it — check the `.gitignore` of the app folder's own repo. If it has one, confirm it covers the file (add `.env` / `.env.local` if not); for a starter app you create, add one. The API key must never be committed to version control.
358
401
 
359
402
  ### Write the non-secret variables
360
403
 
@@ -365,9 +408,11 @@ ARIZE_SPACE_ID=<space-id>
365
408
  ARIZE_PROJECT_NAME=<chosen-project-name>
366
409
  ```
367
410
 
411
+ An exported variable beats this file — both `dotenv` implementations leave an existing environment variable alone by default. Check `env | grep ARIZE_`; if anything you just wrote is exported, tell the user to `unset` it in the shell they'll run the app from, or to load with `override=True`. Your Step 7 check reads the CLI profile, not the app's environment, so it would pass regardless.
412
+
368
413
  ### Create the API key
369
414
 
370
- **Skip this entirely if `ARIZE_API_KEY` was already present in Step 2** — reuse it and leave its env value untouched. Only create a key when you authenticated with browser OAuth and the app has no key yet.
415
+ **Skip this entirely if `ARIZE_API_KEY` was already present and validated in Step 2** — reuse it and leave its env value untouched. Only create a key when you authenticated with browser OAuth and the app has no key yet.
371
416
 
372
417
  Create the key and write it into the env file in one step:
373
418
 
@@ -375,11 +420,11 @@ Create the key and write it into the env file in one step:
375
420
  ax api-keys create --name "Local Arize AX tracing" --env-file .env
376
421
  ```
377
422
 
378
- `--env-file` writes `ARIZE_API_KEY` atomically and **never prints it** — no temp file, no secret in your terminal or chat. Use `.env.local` if that's the app's convention; the file is created if missing, an existing `ARIZE_API_KEY` is replaced in place, and other variables are preserved.
423
+ `--env-file` writes `ARIZE_API_KEY` atomically and **never prints it** — no temp file, no secret in your terminal or chat. Use `.env.local` if that's the app's convention, and always pass the app folder's path (`--env-file /path/to/app/.env`) — a bare `.env` would leave a stray key file in the wrong directory. The file is created if missing, an existing `ARIZE_API_KEY` is replaced in place, and other variables are preserved.
379
424
 
380
- **Create the key exactly once** — a second `ax api-keys create` just orphans a still-active key. Git-ignore the env file (see above) *before* running this so the key is never committed.
425
+ **Create the key exactly once** — a second `ax api-keys create` just orphans a still-active key, and the env file must already be git-ignored.
381
426
 
382
- If key creation fails, have the user create one in the Arize AX UI and add it to the env file without exposing it in chat. Either way, create only one key.
427
+ If key creation fails, have the user create one in the Arize AX UI and add it to the env file without exposing it in chat.
383
428
 
384
429
  Handle the LLM provider's own key (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) in the same env file — the app can't make a call or produce traces without it:
385
430
 
@@ -405,7 +450,7 @@ Common shortcuts:
405
450
 
406
451
  1. Use a framework-specific OpenInference instrumentor if one exists (`openinference-instrumentation-<name>` / `@arizeai/openinference-instrumentation-<name>`), wired up with `arize-otel` per the manual-instrumentation guide; install it unpinned.
407
452
  1. Instrument the underlying provider (OpenAI, Anthropic, Bedrock, …) with its instrumentor **only if the framework calls the provider SDK directly**. Many agent frameworks instead drive the model through their own client layer and emit their own OpenTelemetry spans — a provider instrumentor captures **no traces** for those. Never reach for the provider instrumentor as a blind fallback just because you recognize an `openai`/`anthropic` client.
408
- 1. Otherwise instrument manually via `arize-otel` (see https://arize.com/docs/ax/instrument/manual-instrumentation), or stop and ask the user if you still can't determine a setup.
453
+ 1. Otherwise instrument manually by code follow https://arize.com/docs/ax/instrument/manual-instrumentation#by-code or stop and ask the user if you still can't determine a setup. `arize-otel` gives you a tracer, not LLM spans: **set the OpenInference span kind on every span you create** (`LLM` for a model call, `TOOL` for a tool, `AGENT` for the loop, `CHAIN` for a step), **and set the span status** — `ERROR` with the exception recorded on failure, `OK` otherwise. Without the kind, spans arrive as generic spans that AX cannot read as LLM calls: Step 7's span count comes back non-zero and looks like success while the trace shows no input, output, model, or token counts. Without the status, failed calls look like successful ones and errors never surface in AX.
409
454
 
410
455
  For existing apps:
411
456
 
@@ -425,7 +470,7 @@ For starter apps:
425
470
 
426
471
  ### Package guidance
427
472
 
428
- Install into the app's existing environment (its virtualenv if it has one) exactly the packages the detected framework's integration page lists — instrumentor names and peer dependencies differ per framework, so follow that page rather than copying from another stack or guessing versions.
473
+ Install into the app's existing environment — the one in the app folder, using its virtualenv if it has one exactly the packages the detected framework's integration page lists — instrumentor names and peer dependencies differ per framework, so follow that page rather than copying from another stack or guessing versions.
429
474
 
430
475
  ## Step 7: Run the app and poll for the first trace
431
476
 
@@ -437,11 +482,12 @@ If you created a starter app in this flow, offer to run it for the user:
437
482
  Your starter app is ready. Want me to run it for you?
438
483
  ```
439
484
 
440
- If they say yes, run it yourself with the run command, then poll. If they say no — or if you instrumented their existing app rather than creating a starter — tell them the exact run command and ask them to run it:
485
+ If they say yes, run it yourself with the run command, then poll. If they say no — or if you instrumented their existing app rather than creating a starter — tell them the exact run command and ask them to run it. When the app folder isn't the folder they're sitting in, lead with the `cd` so the command works as pasted:
441
486
 
442
487
  ```text
443
488
  Run your app with:
444
489
 
490
+ cd <app folder>
445
491
  <run command>
446
492
 
447
493
  It should make at least one LLM call. I'll poll Arize AX and let you know as
@@ -462,20 +508,22 @@ A non-zero count confirms traces are arriving. If you need to inspect a span to
462
508
  This uses the CLI profile from Step 2 (OAuth or api-key) — it works the same either way. If the export errors with an authentication failure, the profile isn't valid; re-run the Step 2 probe and re-authenticate, or fall back to having the user open the project in the Arize AX UI to confirm traces.
463
509
 
464
510
  - When spans come back, stop polling and continue to Step 8.
465
- - On timeout, do not fail silently. Tell the user no traces arrived yet, and give likely causes: app didn't make an LLM call, tracing initialized after the client was created, a short-lived script exited before flushing spans, or the wrong space/project/env file. Offer to re-check once they've run it again.
511
+ - On timeout, do not fail silently. Tell the user no traces arrived yet, and give likely causes: app didn't make an LLM call, tracing initialized after the client was created, a short-lived script exited before flushing spans, the wrong space/project/env file, or **an exported `ARIZE_*` variable overriding the env file** (`env | grep ARIZE_`). Offer to re-check once they've run it again.
466
512
 
467
513
  Do not fabricate trace results. Only report traces the export command actually returned.
468
514
 
469
515
  ## Step 8: Report the first traces with a link
470
516
 
471
- Once spans arrive, report the span count and give the user a link into Arize AX for the project. Point them at the UI to explore the trace contents rather than printing span bodies into chat.
517
+ Once spans arrive, report the span count and give the user a deep link to the project in Arize AX. Point them at the UI to explore the trace contents rather than printing span bodies into chat.
518
+
519
+ Build that link with the **Arize link skill**: if you installed the skills in Step 1, load it now by reading `arize-link/SKILL.md` from your agent's skills directory (same paths as Step 6) and follow it. It owns the URL format and the `ax` commands that discover the organization and project IDs; you already have the project name and the space ID from Step 3.
472
520
 
473
521
  ```text
474
522
  Your first traces are in Arize AX. Open project `<ARIZE_PROJECT_NAME>` here:
475
- https://app.arize.com/
523
+ <project link>
476
524
  ```
477
525
 
478
- If a space- or project-specific URL is known from the CLI or docs, prefer that deep link over the app root. Do not invent a URL structure you are not sure of; fall back to `https://app.arize.com/` plus instructions to select the project.
526
+ If that skill file doesn't exist (e.g. npx was missing in Step 1), don't invent a URL structure: fall back to `https://app.arize.com/` plus instructions to select the project.
479
527
 
480
528
  ## Step 9: Point at docs
481
529
 
@@ -501,11 +549,13 @@ If you took Step 4A, add the controls that matter for agent tracing:
501
549
  ## Critical rules
502
550
 
503
551
  - Get the user's approval (Step 5) before creating AX resources, editing files, or installing dependencies.
504
- - Authenticate the CLI before any other `ax` call — every command, including the final trace check, uses the active profile; env vars alone don't authenticate it. On the browser OAuth branch, keep the command alive until its localhost callback completes (never close stdin, Ctrl-C, `pkill`, or probe while it waits) sole exception: a brand-new email/password signup, whose link never calls back, so ask the user, then kill and re-run as a sign-in.
552
+ - Authenticate the CLI (Step 2) before any other `ax` call; env vars alone don't. Never interrupt a waiting OAuth command no stdin close, Ctrl-C, `pkill`, or probe. A slow wait is not a hang.
505
553
  - Never print, log, or summarize secrets in chat — API keys, env-file contents/values, or span bodies (prompts, completions, tool args, user data) — and never read env files into chat. Only report traces a command actually returned.
506
- - Create at most one AX API key: skip if `ARIZE_API_KEY` already exists (reuse it), otherwise a **single** `ax api-keys create --env-file <file>`. Git-ignore that file before creating the key; never create a second key.
554
+ - One AX API key, ever: reuse a **validated** `ARIZE_API_KEY`, else a **single** `ax api-keys create --env-file <file>` into an already-git-ignored file.
555
+ - Ambient `ARIZE_*` variables are claims to verify, not facts to adopt: probe a found API key before reusing it, and remember that an exported variable overrides the env file you write.
556
+ - Everything you write goes in the **app folder** (Step 4), which may not be the folder you were launched in. Name its absolute path in the plan; pass it explicitly to `--env-file` and the run command.
507
557
  - Write the space **ID** (not its name) to `ARIZE_SPACE_ID`, or traces won't route; never create an AX project explicitly (it's made on first ingestion).
508
- - Initialize tracing before LLM clients are created, and flush/shut down the tracer before short-lived scripts exit. Vercel AI SDK v7 also needs Node.js 22+, `@ai-sdk/otel` registered, and `experimental_telemetry: { isEnabled: true }` per call.
509
- - Tracing the coding agent (Step 4A) is machine-wide and captures prompts and tool output by default, so it needs its own explicit yes never fold it into another approval. Run its installer with `--non-interactive` and `< /dev/null` so it can never sit waiting on a prompt, verify with `status --json` before claiming success, and never restart or kill the session you are running in.
558
+ - Initialize tracing before LLM clients exist; flush before short-lived scripts exit. Vercel AI SDK v7 also needs Node.js 22+, `@ai-sdk/otel` registered, and per-call `experimental_telemetry`.
559
+ - Step 4A needs its own explicit yes, never folded into another approvalmachine-wide, captures prompts and tool output. Run its installer with `--non-interactive` and `< /dev/null`, verify with `status --json`, and never restart the session you're in.
510
560
 
511
561
  Docs: https://arize.com/docs/llms.txt
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "evals",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Arize AX onboarding — instrument your app with tracing via your coding agent",
5
5
  "type": "module",
6
6
  "main": "cli.js",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
7
10
  "scripts": {
8
11
  "postinstall": "node -e \"console.log('Arize evals installed — run: npx evals')\"",
9
12
  "start": "node cli.js",
@@ -17,9 +20,10 @@
17
20
  "react": "^19.0.0"
18
21
  },
19
22
  "bin": {
20
- "evals": "./cli.js"
23
+ "evals": "./bin.js"
21
24
  },
22
25
  "files": [
26
+ "bin.js",
23
27
  "cli.js",
24
28
  "onboarding-prompt.md",
25
29
  "start.sh",
package/start.ps1 CHANGED
@@ -32,9 +32,24 @@ $VendorUrl = if ($env:ARIZE_VENDOR_URL) { $env:ARIZE_VENDOR_URL } else { 'https:
32
32
  # Prefer the richer, bundled-prompt `npx evals` experience when Node is present.
33
33
  # This script is the no-npm fallback; if npx exists, hand off to it.
34
34
  # Set $env:ARIZE_SKIP_NPX=1 to force this shell path even when npx is available.
35
+ #
36
+ # Only hand off on Node 20+, which is what the Ink UI needs (see bin.js). Handing
37
+ # an older Node to npx would swap a working flow for a hard failure — this script
38
+ # needs no Node at all, so stay here instead. Keep the floor in step with bin.js.
35
39
  if (-not $env:ARIZE_SKIP_NPX -and (Get-Command npx -ErrorAction SilentlyContinue)) {
36
- npx --yes evals
37
- exit $LASTEXITCODE
40
+ $nodeMajor = 0
41
+ $nodeVersion = $null
42
+ if (Get-Command node -ErrorAction SilentlyContinue) {
43
+ $nodeVersion = (& node --version 2>$null)
44
+ if ($nodeVersion -match '^v?(\d+)') { $nodeMajor = [int]$Matches[1] }
45
+ }
46
+ if ($nodeMajor -ge 20) {
47
+ npx --yes evals
48
+ exit $LASTEXITCODE
49
+ }
50
+ $shown = if ($nodeVersion) { $nodeVersion } else { 'not found' }
51
+ Write-Host "Node $shown is too old for the npx UI — continuing without it."
52
+ Write-Host ""
38
53
  }
39
54
 
40
55
  # Supported agents: id -> label, install URL, and whether the REPL is seeded with `-i`.
@@ -98,69 +113,142 @@ if (-not $chosen) { exit 1 }
98
113
 
99
114
  # A directory, not a bare temp file: Step 4A looks for the offline bundle *beside*
100
115
  # the prompt, so the prompt needs a directory of its own.
101
- $promptDir = Join-Path ([System.IO.Path]::GetTempPath()) ("arize-onboarding-" + [System.Guid]::NewGuid().ToString('N').Substring(0, 8))
102
- New-Item -ItemType Directory -Path $promptDir -Force | Out-Null
103
- $promptFile = Join-Path $promptDir 'onboarding-prompt.md'
104
- try {
105
- Write-Host "Fetching the onboarding prompt..."
116
+ #
117
+ # ~/.arize/onboarding, not a temp path: the agent is asked to read a file outside
118
+ # its workspace, and a named path is an approval a human can grant with confidence.
119
+ # Sits beside Step 4A's own ~/.arize/harness install, which we must never touch.
120
+ #
121
+ # GetFolderPath('UserProfile') rather than $HOME: on Windows PowerShell 5.1 $HOME
122
+ # comes from HOMEDRIVE+HOMEPATH, which for domain users can point at a redirected
123
+ # network share that is slow or offline. This returns USERPROFILE on Windows and
124
+ # $HOME under PowerShell Core on macOS/Linux — one expression for both.
125
+ function Clear-BundleDir($dir) {
126
+ # Delete only what we stage, by name. Wheel filenames carry a version we can't
127
+ # know, so those go by extension, non-recursively, in this directory only.
128
+ Get-ChildItem -LiteralPath $dir -File -Filter '*.whl' -ErrorAction SilentlyContinue |
129
+ Remove-Item -Force -ErrorAction SilentlyContinue
130
+ foreach ($name in @('harness-install.sh', 'harness-install.bat', 'LICENSE-coding-harness-tracing', 'MANIFEST')) {
131
+ Remove-Item -LiteralPath (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue
132
+ }
133
+ # No -Recurse anywhere: a non-empty directory fails here rather than taking
134
+ # something we didn't put there with it, and Remove-Item's habit of recursing
135
+ # through a directory junction can't bite us.
136
+ Remove-Item -LiteralPath $dir -Force -ErrorAction SilentlyContinue
137
+ return -not (Test-Path -LiteralPath $dir)
138
+ }
139
+
140
+ # -LiteralPath throughout: a home path containing [ or ] is a real wildcard
141
+ # footgun for the non-literal forms. Keep this list in step with cli.js
142
+ # OFFLINE_FILES and start.sh.
143
+ function Clear-PromptDir($dir) {
144
+ if (-not (Test-Path -LiteralPath $dir)) { return $true }
145
+ foreach ($sub in Get-ChildItem -LiteralPath $dir -Directory -Force -ErrorAction SilentlyContinue) {
146
+ $ours = ($sub.Name -eq 'arize-offline') -or $sub.Name.StartsWith('.staging')
147
+ if (-not $ours) { return $false }
148
+ if (-not (Clear-BundleDir $sub.FullName)) { return $false }
149
+ }
150
+ foreach ($name in @('onboarding-prompt.md', 'harness.env')) {
151
+ Remove-Item -LiteralPath (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue
152
+ }
153
+ # Anything left is not ours.
154
+ return -not (Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)
155
+ }
156
+
157
+ $candidate = if ($env:ARIZE_ONBOARDING_DIR) {
158
+ $env:ARIZE_ONBOARDING_DIR
159
+ } else {
160
+ $profileDir = [Environment]::GetFolderPath('UserProfile')
161
+ if (-not $profileDir) { $profileDir = $HOME }
162
+ if ($profileDir) { Join-Path (Join-Path $profileDir '.arize') 'onboarding' } else { $null }
163
+ }
164
+
165
+ # Try the work and degrade, rather than probing for writability — a probe races
166
+ # and can't see a full disk. Covers a read-only home, a directory owned by another
167
+ # user, an offline redirected profile, and a file in there we refuse to delete.
168
+ $promptDir = $null
169
+ if ($candidate -and (Clear-PromptDir $candidate)) {
106
170
  try {
107
- Invoke-RestMethod -Uri $PromptUrl -OutFile $promptFile
171
+ New-Item -ItemType Directory -Path $candidate -Force -ErrorAction Stop | Out-Null
172
+ $promptDir = $candidate
108
173
  } catch {
109
- Write-Host "Failed to download the onboarding prompt from $PromptUrl" -ForegroundColor Red
110
- Write-Host "Check your connection and try again."
111
- exit 1
174
+ $promptDir = $null
112
175
  }
176
+ }
177
+ if (-not $promptDir) {
178
+ $promptDir = Join-Path ([System.IO.Path]::GetTempPath()) ("arize-onboarding-" + [System.Guid]::NewGuid().ToString('N').Substring(0, 8))
179
+ New-Item -ItemType Directory -Path $promptDir -Force | Out-Null
180
+ if ($candidate) {
181
+ Write-Host "Couldn't use $candidate — staging in $promptDir instead."
182
+ Write-Host "Instrumenting an app still works; tracing this coding agent (Step 4A) needs a writable home directory."
183
+ }
184
+ }
185
+ $promptFile = Join-Path $promptDir 'onboarding-prompt.md'
113
186
 
114
- # Fetch the bundled tracing harness so Step 4A can install it without piping a
115
- # downloaded script into a shell — the thing auto-approval permission
116
- # classifiers refuse. `npx evals` ships these files; this path pulls them from
117
- # the CDN, using vendor/MANIFEST because a launcher cannot glob a CDN and must
118
- # not hardcode versioned wheel names.
119
- #
120
- # Best-effort throughout. Any failure leaves no arize-offline directory and the
121
- # prompt takes its documented network path instead. Staged under a temp name and
122
- # renamed, so the directory Step 4A keys off only ever appears complete.
123
- Write-Host "Fetching the tracing harness..."
124
- $staging = Join-Path $promptDir '.staging'
125
- try {
126
- New-Item -ItemType Directory -Path $staging -Force | Out-Null
127
- $manifestPath = Join-Path $staging 'MANIFEST'
128
- Invoke-RestMethod -Uri "$VendorUrl/MANIFEST" -OutFile $manifestPath
129
-
130
- foreach ($line in Get-Content $manifestPath) {
131
- if (-not $line.Trim()) { continue }
132
- # MANIFEST is `shasum -a 256` format: "<sha256> <filename>".
133
- $parts = $line -split '\s+', 2
134
- $expected = $parts[0]
135
- $name = $parts[1].Trim()
136
- $dest = Join-Path $staging $name
137
- Invoke-RestMethod -Uri "$VendorUrl/$name" -OutFile $dest
138
- $actual = (Get-FileHash -Path $dest -Algorithm SHA256).Hash.ToLower()
139
- if ($actual -ne $expected.ToLower()) {
140
- throw "checksum mismatch for $name"
141
- }
142
- }
187
+ Write-Host "Fetching the onboarding prompt..."
188
+ try {
189
+ Invoke-RestMethod -Uri $PromptUrl -OutFile $promptFile
190
+ } catch {
191
+ Write-Host "Failed to download the onboarding prompt from $PromptUrl" -ForegroundColor Red
192
+ Write-Host "Check your connection and try again."
193
+ exit 1
194
+ }
143
195
 
144
- Move-Item -Path $staging -Destination (Join-Path $promptDir 'arize-offline')
145
- } catch {
146
- Remove-Item $staging -Recurse -Force -ErrorAction SilentlyContinue
147
- Write-Host " (not available Step 4A will download the installer instead)"
196
+ # Fetch the bundled tracing harness so Step 4A can install it without piping a
197
+ # downloaded script into a shell — the thing auto-approval permission
198
+ # classifiers refuse. `npx evals` ships these files; this path pulls them from
199
+ # the CDN, using vendor/MANIFEST because a launcher cannot glob a CDN and must
200
+ # not hardcode versioned wheel names.
201
+ #
202
+ # Best-effort throughout. Any failure leaves no arize-offline directory and the
203
+ # prompt takes its documented network path instead. Staged under a temp name and
204
+ # renamed, so the directory Step 4A keys off only ever appears complete.
205
+ Write-Host "Fetching the tracing harness..."
206
+ $staging = Join-Path $promptDir '.staging'
207
+ try {
208
+ New-Item -ItemType Directory -Path $staging -Force | Out-Null
209
+ $manifestPath = Join-Path $staging 'MANIFEST'
210
+ Invoke-RestMethod -Uri "$VendorUrl/MANIFEST" -OutFile $manifestPath
211
+
212
+ foreach ($line in Get-Content $manifestPath) {
213
+ if (-not $line.Trim()) { continue }
214
+ # MANIFEST is `shasum -a 256` format: "<sha256> <filename>".
215
+ $parts = $line -split '\s+', 2
216
+ $expected = $parts[0]
217
+ $name = $parts[1].Trim()
218
+ $dest = Join-Path $staging $name
219
+ Invoke-RestMethod -Uri "$VendorUrl/$name" -OutFile $dest
220
+ $actual = (Get-FileHash -Path $dest -Algorithm SHA256).Hash.ToLower()
221
+ if ($actual -ne $expected.ToLower()) {
222
+ throw "checksum mismatch for $name"
223
+ }
148
224
  }
149
225
 
150
- $seed = "Read the file $promptFile and follow it to set up Arize AX tracing in this project, walking me through each step and asking me questions as needed."
226
+ Move-Item -LiteralPath $staging -Destination (Join-Path $promptDir 'arize-offline')
227
+ } catch {
228
+ Clear-BundleDir $staging | Out-Null
229
+ Write-Host " (not available — Step 4A will download the installer instead)"
230
+ }
151
231
 
152
- Write-Host ("Launching {0}..." -f $Agents[$chosen].Label)
153
- Write-Host ""
232
+ # Single quotes around the path: the home directory is likelier to contain a space
233
+ # than the old temp path was, and single quotes survive the console handoff below
234
+ # where embedded double quotes would not.
235
+ # No "in this project": this may well be run from a home directory, and Step 4 is
236
+ # what scopes the work. Keep this wording in step with cli.js and start.sh.
237
+ $seed = "Read the file '$promptFile' and follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed."
154
238
 
155
- # Launch interactive + seeded. No skip-permissions — the agent's approval
156
- # model and the prompt's own approval gate must stay intact. External commands
157
- # attach to the console, so the agent gets a real terminal.
158
- $flag = $Agents[$chosen].Flag
159
- if ($flag) {
160
- & $chosen $flag $seed
161
- } else {
162
- & $chosen $seed
163
- }
164
- } finally {
165
- Remove-Item $promptDir -Recurse -Force -ErrorAction SilentlyContinue
239
+ Write-Host ("Launching {0}..." -f $Agents[$chosen].Label)
240
+ Write-Host ""
241
+
242
+ # Launch interactive + seeded. No skip-permissions — the agent's approval
243
+ # model and the prompt's own approval gate must stay intact. External commands
244
+ # attach to the console, so the agent gets a real terminal.
245
+ #
246
+ # No try/finally around this any more: the staging directory persists so the agent
247
+ # can still reach the prompt in a session that outlives this script. It is cleared
248
+ # at the start of the next launch instead.
249
+ $flag = $Agents[$chosen].Flag
250
+ if ($flag) {
251
+ & $chosen $flag $seed
252
+ } else {
253
+ & $chosen $seed
166
254
  }
package/start.sh CHANGED
@@ -28,8 +28,23 @@ VENDOR_URL="${ARIZE_VENDOR_URL:-https://cdn.jsdelivr.net/npm/evals/vendor}"
28
28
  # Prefer the richer, bundled-prompt `npx evals` experience when Node is present.
29
29
  # This shell script is the no-npm fallback; if npx exists, hand off to it.
30
30
  # Set ARIZE_SKIP_NPX=1 to force this shell path even when npx is available.
31
+ #
32
+ # Only hand off on Node 20+, which is what the Ink UI needs (see bin.js). Handing
33
+ # an older Node to npx would swap a working flow for a hard failure — this script
34
+ # needs no Node at all, so stay here instead. Keep the floor in step with bin.js.
35
+ node_major() {
36
+ command -v node >/dev/null 2>&1 || return 1
37
+ v="$(node --version 2>/dev/null)" || return 1
38
+ v="${v#v}"
39
+ echo "${v%%.*}"
40
+ }
31
41
  if [ -z "${ARIZE_SKIP_NPX:-}" ] && command -v npx >/dev/null 2>&1; then
32
- exec npx --yes evals
42
+ major="$(node_major || echo 0)"
43
+ if [ "${major:-0}" -ge 20 ] 2>/dev/null; then
44
+ exec npx --yes evals
45
+ fi
46
+ echo "Node $(node --version 2>/dev/null || echo 'not found') is too old for the npx UI — continuing without it."
47
+ echo
33
48
  fi
34
49
 
35
50
  # Supported agents: id | display label | argv to start the REPL seeded with a prompt.
@@ -136,12 +151,62 @@ choose_agent() {
136
151
  CHOSEN="$(choose_agent)" || exit 1
137
152
 
138
153
  # A directory, not a bare temp file: Step 4A looks for the offline bundle *beside*
139
- # the prompt, so the prompt needs a directory of its own rather than sharing
140
- # /tmp with everything else.
141
- PROMPT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/arize-onboarding-XXXXXX")"
154
+ # the prompt, so the prompt needs a directory of its own.
155
+ #
156
+ # ~/.arize/onboarding, not a temp path: the agent is asked to read a file outside
157
+ # its workspace, and that approval is one a human can grant with confidence for a
158
+ # named path but not for /tmp/arize-onboarding-8fJ2x1. Sits beside Step 4A's own
159
+ # ~/.arize/harness install, which we must never touch.
160
+ #
161
+ # Cleared at launch and left in place afterwards — no EXIT trap — so the agent can
162
+ # still reach the prompt in a session that outlives this script, and so we can
163
+ # exec rather than waiting on a child just to clean up.
164
+ clear_bundle_dir() {
165
+ # Delete only what we stage, by name. Wheel filenames carry a version we can't
166
+ # know, so those go by extension, non-recursively, in this directory only.
167
+ rm -f "$1"/*.whl
168
+ rm -f "$1/harness-install.sh" "$1/harness-install.bat" \
169
+ "$1/LICENSE-coding-harness-tracing" "$1/MANIFEST"
170
+ # rmdir, never rm -r: a non-empty directory fails here instead of taking
171
+ # something we didn't put there down with it.
172
+ rmdir "$1" 2>/dev/null
173
+ }
174
+
175
+ # Never a recursive delete. Worst case on a bad path is unlinking a few names that
176
+ # don't exist. Keep this list in step with cli.js OFFLINE_FILES and start.ps1.
177
+ clear_prompt_dir() {
178
+ [ -d "$1" ] || return 0
179
+ for sub in "$1"/arize-offline "$1"/.staging*; do
180
+ [ -d "$sub" ] && clear_bundle_dir "$sub"
181
+ done
182
+ rm -f "$1/onboarding-prompt.md" "$1/harness.env"
183
+ # Anything left is not ours: bail out and let the caller fall back.
184
+ rmdir "$1" 2>/dev/null || { [ -z "$(ls -A "$1" 2>/dev/null)" ]; return $?; }
185
+ }
186
+
187
+ # $HOME unset (containers, cron, some CI) must degrade, never abort — hence
188
+ # ${HOME:-} and not ${HOME:?}. Same for a read-only home, a directory owned by a
189
+ # past `sudo`, or a full disk: try the work, fall back to a temp dir on failure.
190
+ PROMPT_DIR=""
191
+ if [ -n "${ARIZE_ONBOARDING_DIR:-}" ]; then
192
+ CANDIDATE="$ARIZE_ONBOARDING_DIR"
193
+ elif [ -n "${HOME:-}" ]; then
194
+ CANDIDATE="$HOME/.arize/onboarding"
195
+ else
196
+ CANDIDATE=""
197
+ fi
198
+
199
+ if [ -n "$CANDIDATE" ] && clear_prompt_dir "$CANDIDATE" \
200
+ && mkdir -p "$CANDIDATE" 2>/dev/null && chmod 700 "$CANDIDATE" 2>/dev/null; then
201
+ PROMPT_DIR="$CANDIDATE"
202
+ else
203
+ PROMPT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/arize-onboarding-XXXXXX")"
204
+ if [ -n "$CANDIDATE" ]; then
205
+ echo "Couldn't use $CANDIDATE — staging in $PROMPT_DIR instead."
206
+ echo "Instrumenting an app still works; tracing this coding agent (Step 4A) needs a writable home directory."
207
+ fi
208
+ fi
142
209
  PROMPT_FILE="$PROMPT_DIR/onboarding-prompt.md"
143
- cleanup() { rm -rf "$PROMPT_DIR"; }
144
- trap cleanup EXIT
145
210
 
146
211
  if command -v curl >/dev/null 2>&1; then
147
212
  fetch() { curl -fsSL "$1" -o "$2"; }
@@ -198,18 +263,21 @@ if ! stage_offline_bundle; then
198
263
  echo " (not available — Step 4A will download the installer instead)"
199
264
  fi
200
265
 
201
- SEED="Read the file $PROMPT_FILE and follow it to set up Arize AX tracing in this project, walking me through each step and asking me questions as needed."
266
+ # No "in this project": this may well be run from a home directory, and Step 4 is
267
+ # what scopes the work. Keep this wording in step with cli.js and start.ps1.
268
+ SEED="Read the file '$PROMPT_FILE' and follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed."
202
269
 
203
270
  echo "Launching $(agent_label "$CHOSEN")…"
204
271
  echo
205
272
 
206
273
  # Launch interactive + seeded, on the real terminal. No skip-permissions —
207
274
  # the agent's approval model and the prompt's own approval gate must stay intact.
208
- # Run as a child (not exec) so the EXIT trap cleans up the temp prompt file.
275
+ # exec, now that the staging directory persists and there is no EXIT trap to run:
276
+ # the agent replaces this shell, so signals and the exit code reach it directly.
209
277
  run_agent() {
210
278
  case "$CHOSEN" in
211
- copilot|gemini) "$CHOSEN" -i "$SEED" ;;
212
- *) "$CHOSEN" "$SEED" ;;
279
+ copilot|gemini) exec "$CHOSEN" -i "$SEED" ;;
280
+ *) exec "$CHOSEN" "$SEED" ;;
213
281
  esac
214
282
  }
215
283
 
package/vendor/MANIFEST CHANGED
@@ -1,6 +1,6 @@
1
1
  a9cede65f98d3415b13e1c6a437d0fdee27286bfff0186dd72bb65d9869b7864 LICENSE-coding-harness-tracing
2
2
  62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 certifi-2026.7.22-py3-none-any.whl
3
- e17d86afa36365ddd451325b7b2ed3e35ba85801fb10e35401fded609095f550 coding_harness_tracing-0.1.0-py3-none-any.whl
3
+ c41daa85c05f38eabd63fd0c7a2c021ffc0ecb7483485f7dd1b8a637f983b778 coding_harness_tracing-0.1.0-py3-none-any.whl
4
4
  aca14e09ca99c253d2a142e2d5c5c38c8c7cf90840d3cb10b241c29929871fa4 harness-install.bat
5
5
  a7ba18a98280e236b20ab4cb9f8a4006068b454586a2af50bf23fb0dbe70583d harness-install.sh
6
6
  b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 python_dotenv-1.2.1-py3-none-any.whl
@@ -5,7 +5,7 @@
5
5
  "source": "git",
6
6
  "wheel": "coding_harness_tracing-0.1.0-py3-none-any.whl",
7
7
  "wheelVersion": "0.1.0",
8
- "wheelSha256": "e17d86afa36365ddd451325b7b2ed3e35ba85801fb10e35401fded609095f550",
8
+ "wheelSha256": "c41daa85c05f38eabd63fd0c7a2c021ffc0ecb7483485f7dd1b8a637f983b778",
9
9
  "files": [
10
10
  "LICENSE-coding-harness-tracing",
11
11
  "MANIFEST",