Commit Graph

14 Commits

Author SHA1 Message Date
filip131311 4975ff490c fix(debugger): cut debugger tool fail rate — structured not-connected results, classified CDP failures, narrow recovery (#610)
## Why

PostHog (30d) shows the debugger tool family is argent's least reliable
surface:

| tool | fail rate | dominant causes |
|---|---|---|
| `debugger-status` | **37%** | METRO_NO_TARGETS /
SERVICE_INITIALIZATION_FAILED / zod |
| `debugger-connect` | **24%** | SERVICE_INITIALIZATION_FAILED (~7k, >1k
users), METRO_NO_TARGETS |
| `debugger-log-registry` | 41.7% raw / 4.2% excl. storms | one user
retry-looped it **12,742×** on NO_TARGETS |

Three design issues drive this: (1) "app/Metro not ready yet" — an
expected precondition — is reported as a tool *error*, which both
inflates fail rates and invites agent retry storms; (2) every CDP
init/lifecycle fault lands in telemetry as the catch-all
`REGISTRY_SERVICE_INITIALIZATION_FAILED` with `error_kind=unknown`; (3)
~600 users/30d hit zod validation failures with zero visibility into
which parameter was wrong.

## What

**1. Classified failures** (`d71efa43`) — six former plain-`Error` sites
in `cdp-client.ts` / `target-selection.ts` now carry dedicated
`DEBUGGER_CDP_*` / `DEBUGGER_TARGET_DEVICE_MISMATCH` signals (messages
byte-identical). The registry's two causeless "terminating window"
`ServiceInitializationError`s get their own
`REGISTRY_SERVICE_TERMINATING` code.

**2. Narrow recovery** (`ff9c1955`) — `recoverable()` on both debugger
blueprints, scoped to windows where retry is provably safe: Metro
recovers only `DEBUGGER_CDP_NOT_CONNECTED` (request never left the host;
a detected socket death already self-heals for the *next* call via the
terminated cascade — recovering `CONNECTION_CLOSED` there would be dead
code, see the blueprint comment). Chromium also recovers
`CONNECTION_CLOSED` for the tab-switch reconnect window.
`REQUEST_TIMEOUT` stays unrecoverable (double-execution risk). A
reachability test proves the retry actually fires for a RUNNING node.

**3. Structured not-connected results** (`48b91ffd`) — `debugger-status`
and `debugger-log-registry` become factories that return `{ status:
"not_connected", reason, detail, guidance }` for classified
preconditions (`metro_not_running | no_app_connected | device_mismatch |
cdp_unreachable | stale_connection | reconnecting`) and still throw on
unexpected faults. `detail` preserves the original error text for
string-matching agents. `debugger-status` gains a socket-state gate
(Metro: dispose stale node; Chromium: report transient reconnect, no
dispose — the client belongs to the ChromiumCdp dependency).
log-registry keeps **no** gate on purpose: captured logs are readable
over a dead socket, and disposal would destroy the post-crash log file.
Flows/run-sequence map a not_connected result to a **failed step**
(mirrors `isUnmetUiWaitResult`) so recorded connectivity gates don't
silently pass. `debugger-evaluate` re-codes agent-expression throws as
`DEBUGGER_EVALUATE_EXPRESSION_THREW`.

**4. Telemetry + guidance + docs** (`7602a70d`) —
`tool:fail.invalid_params` (schema-declared names only; unknown keys →
literal `"unrecognized_keys"`; capped emit-side at 16), new
`debugger:tool_outcome` event (coded enum; joins
`tool:invoke`/`complete` via `tool_invocation_id` —
`ai_client`/`duration` live on the joined row), anti-retry-loop guidance
appended to the Metro discovery errors (first sentences verbatim —
skills match on them), six skills updated, Telemetry.md bumped to v1.03
with the new coded diagnostics disclosed.

## Dashboard migration note

`debugger-status` / `debugger-log-registry` precondition failures stop
emitting `tool:fail` — track them via `debugger:tool_outcome` (`outcome
!= 'connected'`). Per-AI-client breakdowns of those outcomes need a join
on `tool_invocation_id` against `tool:complete`.

## Testing

- Full suites green: tool-server 3,142, telemetry 290, registry 87. New
coverage: recoverable truth tables (incl. non-Error inputs), status
not-connected matrix
(Metro/Chromium/terminating-window/stale/dispose-side-effect/fall-through-rethrow),
log-registry rows, zod `invalid_params` derivation + emission-level
guard, flow gate, evaluate rewrap, sanitize-level enum checks.
- Live-tested against a booted iOS simulator + RN app and an Electron
target (details in PR comments).

Plan hardened through 3 adversarial review rounds (46 confirmed findings
addressed) before implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-10 11:51:45 +02:00
filip131311 7f85f996aa fix(tool-server): defer to an existing server on EADDRINUSE instead of crash-looping (#533)
## Summary

Fixes **bucket B** of the 0.16.0 crash telemetry — the `toolserver_bind`
/ `EADDRINUSE` restart loop. It's the largest crash **volume** (~817
events, one machine alone = 785) even though it's a narrow user set
(10). Companion to #531 (which fixed the widespread multi-user bucket
A).

## Root cause (reproduced live)

The main HTTP listener's `server.on("error")` handler routed **every**
bind error through `crashShutdown` (exit 1). When a second tool-server
starts on an already-owned port, that exit just feeds a supervisor
respawn → re-bind → crash, forever.

The telemetry signature was puzzling: all these crashes are
`crash_phase=serving` with ~1s uptime and **0 tool calls** — i.e. the
listener *bound successfully* and then died. I reproduced it: on macOS,
a **second tool-server process's** `listen()` momentarily **succeeds**
(the `listening` callback runs → `listening=true` → phase `serving`),
and **then** the server emits an asynchronous `EADDRINUSE`. So the
redundant instance binds, immediately errors, crashes, and the
supervisor loops it (~1s each, 785× on one machine).

Live A/B on the compiled `dist` (isolated port, telemetry disabled),
server A up then a second server B on the same port:

```
buggy (current 0.16.0):  B: "Tools server listening ..."  → "Failed to bind ... EADDRINUSE"  → exit 1  (→ respawn loop)
fixed:                   B: "Tools server listening ..."  → "deferring to it (redundant instance)" → exit 0
```

A survives in both.

## Fix

Handle `EADDRINUSE` specifically instead of crash-looping:

- Probe the port for a healthy argent tool-server (`GET /tools` → 200,
or 401 when a token is configured).
- **If a healthy peer answers**, this instance is redundant → shut down
**cleanly** (`shutdown(0)`) and defer to it. This removes it from the
`reason:"crash"` restart-loop population entirely.
- **If nothing healthy owns the port** (a foreign holder, or a wedged
peer that doesn't answer within the probe timeout) → still crash, so a
genuinely stuck port stays visible.
- `EACCES` and all other server errors crash exactly as before.

It also corrects the now-false comment claiming these crashes are phased
`startup` ("the listen callback never ran"). Telemetry and the live
repro prove the bind succeeds first, so `listening=true` and the phase
is genuinely `serving`.

## Testing

- **New unit tests** for `probeArgentToolServer`: peer answering 200 or
401 → `true`; foreign 500, connection-refused, and wedged/timeout →
`false`.
- Full tool-server suite **2731 green**; `tsc` + `eslint` clean.
- **Live A/B** above, on the built dist.

## Note on scope

This is a robustness fix that breaks the loop for the redundant-instance
case (the dominant one). The precise reason macOS lets a second
process's bind succeed-then-error asynchronously (vs. failing
immediately, as same-process contention does) is a kernel/loopback
timing detail; the fix does not depend on it — any `EADDRINUSE`,
immediate or deferred, now defers-or-crashes deterministically rather
than hot-looping.
2026-07-22 16:56:54 +02:00
filip131311 f1331cf80d fix(installer): unbreak pnpm local installs + stop self-fulfilling editor detection (#501)
## Problem

Field report (pnpm setup): `npx @swmansion/argent init --local`

- ✅ "updated argent 14→15", ✅ created `.mcp.json` + `skills-lock.json`
- 🔴 did **not** add argent to `package.json`
- 🔴 re-running the same command → `Local install failed.` + advice to
run `pnpm add -D ...` manually
- 🔴 init "persistently detects" Cursor and Codex on a fresh machine that
never had them

## Root causes (each reproduced; confirmed against installer telemetry)

1. **Stale-binary hijack** — bare `npx @swmansion/argent` prefers an
existing **global install** over the registry. A global 0.14 predates
`--local` and `parseInitArgs` silently ignored unknown flags, so run 1
was a plain global-mode 0.14 init: it offered the 14→15 update, wrote
mcp/skills files, and never touched package.json. That's symptoms 1–3 in
one.
2. **pm detection missed pnpm's modern markers** — `pnpm init` (pnpm
10/11) writes only `devEngines.packageManager`, and no lockfile exists
before the first install. Detection fell back to npm, whose own
devEngines gate instantly rejects the install (`EBADDEVENGINES`, ~250
ms). This is the 221–537 ms `INSTALL_LOCAL_PACKAGE_FAILED` cluster in
PostHog (8 events / 7 users last week).
3. **Missing pm binary** (cloned pnpm repo, machine has only npm)
surfaced as raw `spawn pnpm ENOENT` plus manual advice that fails
identically in the user's shell.
4. **Self-fulfilling detection** — Cursor/Codex `detect()` keyed on bare
`~/.cursor` / `~/.codex` dirs, which argent itself creates (an `init -y`
with nothing detected configures *all* editors; allowlist/skills writes
too).

Telemetry also shows a ~19 s failure shape where an identical manual
retry succeeds (transient registry / pnpm first-run state errors).

## Fixes

- **Init flag parsing stays lenient**: unknown flags remain silently
ignored — aborting on them could break existing scripts/setups that pass
flags an installed version doesn't know. The parser now also accepts the
`--from=path` spelling, ignores a dangling `--from`, and keeps
first-`--from`-wins semantics. The stale-binary hijack is addressed on
the docs side (`@latest` in the README) rather than by a hard abort.
- **`detectProjectPackageManager`** honors `devEngines.packageManager`
(object + array forms) and `pnpm-workspace.yaml` — the latter ranked
*below* real lockfiles so a stray file left by a pnpm→yarn migration
can't outrank `yarn.lock`.
- **Missing-binary failures** name the real problem ("This project uses
pnpm, but the pnpm command was not found on PATH" + corepack hint).
Windows detected via cmd.exe's "is not recognized" text, since `shell:
true` never ENOENTs there. Other pm failures **retry once**.
- **Editor detection requires real evidence**: a `.cursor`/`.codex` dir
counts only if it holds something beyond argent's own artifacts (argent
mcp.json entry, `permissions.json` allowlist, managed
`developer_instructions`, bundled rules/agents/skills). Corrupt,
unparseable, or empty content conservatively counts as user evidence.
The stale-config sweep now scans **all** adapters so dead entries in
no-longer-detected dirs still get pruned.
- **README**: recommend `npx @swmansion/argent@latest init` and a `pnpm
dlx` alternative (npm's devEngines gate blocks the *outer* npx in fresh
pnpm-init projects).

## Testing

- Live-tested a packed tgz under the exact repro conditions: fresh
pnpm-init project A/B (published 0.15.0 fails EBADDEVENGINES in ~250 ms;
this branch installs the devDependency correctly), missing-pnpm message,
flag-parsing edges (`--from=` spelling, dangling `--from`),
retry-on-transient-failure (flaky pm shim), detection flips (argent-only
dirs incl. permissions.json → not detected;
`argv.json`/`sessions/`/corrupt configs → detected), plus regressions:
npm project local init, global-mode `init -y`, recovery of the broken
post-0.14 state.
- High-effort multi-agent code review of the diff surfaced 10 confirmed
defects in the first iteration (permissions.json not whitelisted,
corrupt configs misclassified as argent-only, pnpm-workspace.yaml
outranking yarn.lock, `--from=` rejected, Windows ENOENT never firing,
stale-sweep interaction, …) — all fixed and re-verified.
- 493 argent-installer + 87 registry unit tests green (new suites for
flag parsing, devEngines/workspace detection, detection evidence).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 15:37:54 +02:00
filip131311 2196cc18f0 feat(telemetry): anonymous crash diagnostics on toolserver:stop (#494)
## Why

PostHog shows **~9,785 `toolserver:stop` crashes over 90 days**, but
every one collapses into a single bucket — `error_kind=crash`,
`error_code=TOOLSERVER_UNCAUGHT_EXCEPTION`. We know *that* the tool
server died on a top-level throw, never *which* throw. The population is
also two very different things we can't tell apart today:

- **~99%** crash at **<5s uptime with 0 tool calls** — startup faults,
and heavily concentrated per-user (top 10 users ≈ 31% of all crashes;
one user logged 1,027), i.e. **boot → crash → relaunch loops**.
- **~1,000** crash after **>10 min** of real serving — genuine runtime
bugs.

Without the actual exception, all 9,785 rows are unrankable and
unfixable.

## What

Four **crash-only, provably-anonymous** fields on `toolserver:stop`,
enforced by the existing coded-allowlist sanitizer so **no free text can
ever reach PostHog**:

| field | value | why |
|---|---|---|
| `error_name` | error class name (`TypeError`, …) | "what kind" |
| `error_syscall` | Node's `err.code` (`EADDRINUSE`, `ECONNREFUSED`, …)
| the key discriminator for the startup-loop population (port/socket
conflicts) |
| `crash_fingerprint` | 16-hex hash of the **de-identified** top stack
frames | clusters the same crash across users; input carries no
identity, output is opaque |
| `crash_phase` | `"startup"` \| `"serving"` | splits the two
populations directly |

Turns 9,785 opaque rows into a handful of rankable clusters.

## Anonymity (the hard requirement)

- **No error message, no raw stack.** Those routinely embed
`/Users/<name>/…`, URLs, argv, hostnames, interpolated values — the
highest-PII surface and impossible to reliably scrub. The fingerprint
replaces the message for clustering; `error_name` + `error_syscall` give
the "what kind" without free text.
- **Fingerprint input is de-identified before hashing**
(defense-in-depth on top of the hash): first-party frames → basename
only (drops the home-dir/project prefix), `node_modules` frames →
package-relative tail (drops the user prefix; also collapses the pnpm
`.pnpm/pkg@ver/` layout), `node:` internals kept verbatim, column
dropped. So the *same crash on two machines produces the same
fingerprint*, and no username/path survives even into the hash input.
- **Sanitizer is the final gate.** `error_name`
`^[A-Za-z][A-Za-z0-9_]{0,63}$`, `error_syscall` `^E[A-Z0-9_]{1,31}$`,
`crash_fingerprint` `^[0-9a-f]{16}$`, `crash_phase` a 2-value enum. Any
path/message/lowercase/oversize value is dropped before it can leave the
machine.

## Scope & safety (no leaking crashes out of scope)

- Fields are populated **only** in `crashShutdown` (which forces
`reason="crash"`), so clean idle/signal stops carry none. Verified by
the existing startup-telemetry test (the pre-listener watcher-rejection
crash path stays the bare `{reason,uptime_ms,total_tool_calls}` shape).
- The four keys exist only on `ToolserverStopProps` and only in the
`toolserver:stop` allowlist entry — they can't ride any other event.
- `describeCrash` is **total**: every field extraction is isolated, so a
hostile throwing getter on `.name`/`.code`/`.stack` yields the phase
(and whatever else is readable) instead of turning crash reporting into
a second crash. The crash/exit guarantee — re-entrancy guard, force-exit
timer, `finalExitCode` escalation — is untouched.

## Tests

- New `crash-diagnostics.test.ts`: class-name/constructor-name/primitive
extraction, syscall capture, cross-user determinism (incl. **Windows**
drive-letter paths and **pnpm** layout), node_modules tail,
line-sensitivity, no-stack omission, hostile-getter resilience, and
"never emits a message/stack" confinement.
- `sanitize.test.ts`: crash fields pass when well-formed; dropped for
message-as-name, path/lowercase syscall, wrong-length/uppercase
fingerprint, unknown phase; `ERR_*` accepted.
- Full telemetry suite (278) + tool-server startup test green;
lint/prettier clean.

## Reviewed

Ran an adversarial review pass (anonymity leak-hunt, determinism,
crash-path safety, scope). Ship-ready; all findings were Low/Nit and the
actionable ones (hostile-getter hardening, Windows/pnpm test coverage,
`error_syscall` doc clarity) are folded in.

### Deliberately deferred (documented, not blockers)
- Sanitizer doesn't *cross-gate* the crash fields on `reason==="crash"`
— the per-field validator design has no sibling access; coupling is
enforced in emit code. The fields are non-identifying regardless, so a
hypothetical stray `crash_phase` on an idle stop is a data nit, not a
leak.
- First-party frames reduce to basename; if `dist` ever ships unbundled
with duplicate basenames, two sites at the same line could collide.
Non-issue for the bundled tool-server.
2026-07-13 22:16:33 +02:00
Hubert Gancarczyk f002fc9540 feat(telemetry): track Argent Lens design-review funnel (#478)
## What

Adds three privacy-safe telemetry events for the Argent Lens
design-review flow, capturing the human-decision funnel that the generic
`tool:*` path can't see (submission is an HTTP POST, and the
`await_user_selection` tool is hidden in `argent lens` CLI sessions):

- **`lens:preview_opened`** — a human loaded the preview UI (`GET
/preview/`), deduped per round.
- **`lens:round_completed`** — the human submitted their picks (once per
round, never on a resubmit).
- **`lens:round_abandoned`** — a staged round was discarded before
submit (window closed, `argent lens` exited mid-review, or the round
superseded).

## Privacy

Payloads carry only aggregate counts, booleans, durations, and the
device `platform` enum — never element names, comment text, variant
code, file paths, or raw device ids. The store emits privacy-safe
aggregate structs that are decoupled from the telemetry package;
structural assignability to the event props is enforced at the `track()`
call site (drift is a compile error).

## Event fields

- **`preview_opened`**: `round, element_count, variant_count,
is_cli_session, platform`
- **`round_completed`**: `round, element_count, variant_count,
annotation_count, element_comment_count, skipped_comment_count,
has_global_comment, is_cli_session, had_parked_await, round_duration_ms,
platform`
- **`round_abandoned`**: `round, element_count, variant_count,
had_parked_await, is_cli_session, platform`

## Testing

- Unit tests for all three events: per-round dedup, exactly-once
completion (no resubmit re-emit), abandoned vs happy-path roll, CLI
up-front open, and content-agnostic privacy assertions.
- Verified end-to-end against a live tool-server: all three events fire
with correct aggregate values and zero content leakage through the real
sanitize pipeline.

---------

Co-authored-by: Ignacy Łątka <latkaignacy@gmail.com>
2026-07-13 16:56:49 +02:00
filip131311 7bd017a796 feat(installer): committable repo-local (--local) install mode (#428)
## Summary

Adds an opt-in **committable / repo-local** install mode so a team can
version Argent _with the repo_. `argent init --local` installs
`@swmansion/argent` as a project **devDependency** and writes MCP
configs that launch the project-local copy — so after `git clone && npm
install`, every teammate has the same Argent version and MCP wiring with
**no global install and no per-developer `argent init`**.

**Global mode stays the default** and is unchanged; everything here is
additive and backward-compatible.

Motivation: the workflow some users already do by hand (#207).
Supersedes the earlier, since-closed #232 — this version is Windows-safe
(`node`, not the `.bin` shim), handles Yarn PnP, and closes the
cross-install tool-server hazards rather than leaving them.

## What changed

**`init`**
- `--local` / `--global` flags + an interactive "How should argent be
installed?" prompt. Mutually exclusive; non-interactive (`--yes`)
defaults to **global**.
- Local mode installs the devDependency with the package manager
detected from the project's **lockfile** (run with `cwd = projectRoot`).
- MCP entry points at the local copy: `node
node_modules/@swmansion/argent/dist/cli.js mcp` (the bin path is derived
from the installed `package.json` `bin` and existence-checked — forward
slashes, committable, Windows-safe). Yarn PnP → `yarn argent mcp`; an
`npx --no-install argent mcp` fallback is used only when the bin can't
be resolved.
- Records the mode in a committed `<root>/.argent/install.json`.
- Drops global-only adapters (Windsurf/Hermes) in local mode; prints
team-setup / commit guidance.

**`update`** — detects the project's mode; in local mode reads the
installed version from `node_modules` (not the global binary / npx
cache) and bumps the **devDependency + lockfile**, then re-emits the
local MCP command.

**`uninstall`** — scoped to the project's mode: a local-mode uninstall
removes the **local devDependency only and never the shared global
install**, and prunes `.argent/install.json`. Global-mode behavior is
unchanged.

**Cross-install hardening (the gate that makes per-project pinning
safe)**
- `launcher`: a tool-server is reused only when its `bundlePath` matches
the caller's — a repo-local pin never silently runs against (or kills) a
different version's server.
- `postinstall`: only stops a tool-server spawned from *this package's
own* bundle, so a devDependency install can't tear down an unrelated
editor session.

**Telemetry** — adds an `install_mode` dimension to
init/update/uninstall events.

## Testing

- New unit tests: install-mode helpers + record (`utils`), mode-aware
`getMcpEntry`/`resolveLocalCommandMode` incl. the opencode array form
(`mcp-configs`), local-only uninstall guard (`uninstall`), and
bundlePath-gated tool-server reuse (`launcher`).
- All existing suites green (installer 346, tools-client 115, telemetry
191, registry 83, argent 13, tool-server 1621). Build + lint clean.
- **Live end-to-end** in an isolated sandbox: `init --local` → real MCP
stdio handshake serves **69 tools from `node_modules`**; `update
--local` reads the local version; `uninstall --local` removes the local
install and **leaves the global install intact**; `init --global`
unchanged; flag conflict rejected.

## Notes / known limitations

- Recommended on mainstream platforms (macOS, Linux x64, Windows x64)
with prebuilt native binaries; Linux arm64 / Windows arm need a C/C++
toolchain to build `tree-sitter` on `npm install` (documented in the
README).
- Concurrent sessions of two *different* Argent versions on one machine
each spawn their own tool-server; an orphaned one idle-times-out (no
session is ever broken).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-08 12:24:19 +02:00
Hubert Gancarczyk f38f0e3876 feat(telemetry): distinguish tvOS and Android TV from iOS/Android (#477)
## Summary

Telemetry inferred the device platform purely from device-id **shape**
on the per-tool-call hot path, so a tvOS simulator (same 8-4-4-4-12 UUID
as an iPhone) and an Android TV emulator (same `emulator-NNNN` serial as
a phone) were both reported as `ios` / `android` — TV usage was
invisible in analytics.

This adds `tvos` and `android-tv` to the **telemetry** `PLATFORMS` enum
and refines the inference so a TV target is attributed correctly.

## Approach

- The runtime kind (`mobile` / `tv`) is already resolved and memoized in
two module-level caches (`runtimeKindCache` in `ios-devices.ts`,
`androidRuntimeKindCache` in `adb.ts`), warmed by the usual TV-path
tools (describe, screenshot, keyboard, launch/restart-app, the streaming
blueprint, tv-control).
- New synchronous, **cache-only** readers
(`getCachedSimulatorRuntimeKind`, `getCachedAndroidRuntimeKind`) let
`refineTvPlatform` in `http.ts` map `ios → tvos` / `android →
android-tv` with **no new `simctl`/`adb` round-trip** on the hot path.
- The registry **device** `Platform` (`@argent/registry`) is
intentionally left unchanged — a TV stays a `runtimeKind`, not its own
device platform, so capability gating and dispatch remain TV-agnostic.
The two new values are confined to the telemetry boundary (`tool:invoke`
/ `tool:complete` / `tool:fail` and child sub-tool attribution).

## Behavior notes

- The **first** tool call on a device may land before the cache is warm
and reports the coarse base platform (`ios`/`android`); subsequent
calls, once describe/screenshot/streaming have run, report the TV
variant. This is best-effort coarse telemetry by design.
- For an `emulator-NNNN` slot reclaimed by a different-form-factor AVD
within one tool-server lifetime, the synchronous reader can momentarily
report the prior occupant's kind until an async, avdName-checked path
re-probes. Documented inline; no crash and no dispatch impact.

## Verification

- **Live E2E** against real booted devices (real `simctl`/`adb` → real
cache → real http telemetry recording): Apple TV → `tvos` (cold-cache
`ios`, warm `tvos`); iPhone → `ios`; Android phone → `android`. The
`android-tv` path is symmetric and covered by unit/integration tests
with real leanback-feature probing.
- Unit + integration tests added: sanitize allowlist, http platform
inference (cold/warm/TV/child), and cache readers. Telemetry suite
192/192; tool-server affected suites green.
- `tsc` build, `typecheck:tests`, eslint (`--max-warnings 0`), and
prettier all clean.
2026-07-07 16:29:22 +02:00
Hubert Gancarczyk 3393d22edd feat(telemetry): derive distinct_id from a stable host fingerprint (#444)
## Summary

Replaces the random-UUID telemetry `distinct_id` with a stable
per-machine id derived from the host fingerprint, so PostHog's native
"Unique users" counts machines (funnels / retention / breakdowns work
with no HogQL). Every Argent entry point - installer, CLI, tool-server,
MCP - derives the id from one shared code path, so a given machine
reports a single identity across every event and across install /
uninstall / reinstall.

## What changed

- `distinct_id = uuidv5(hostFingerprint, NAMESPACE)`, where the
fingerprint comes from `simulator-server fingerprint`. Deterministic,
keeps the UUID shape, and never stores the raw hardware value in PostHog
(only the SHA-1-derived UUID leaves the process).
- The fingerprint is resolved in `buildPayload` - the single `capture()`
path shared by every entry point - so installer / CLI / tool-server /
MCP all produce the same id. Resolved lazily, once per process,
memoized.
- One-time, local-only migration: the first time a fingerprint resolves,
`~/.argent/telemetry-id` is rewritten from the legacy random UUID to the
derived id. No PostHog `alias` / `$identify` event is emitted.
- Fallback: if the fingerprint can't be resolved (unsupported platform,
binary absent), the stored or a fresh random UUID is used - identically
for every entry point on that machine, self-healing to the derived id on
the next successful resolve.
- Hardening: never throws; the fingerprint is validated (exactly 64 hex,
case-insensitive) before hashing; corrupt / empty / unreadable id files
self-heal; the binary is spawned at most once per process.

## Why the id is stable across install -> uninstall -> reinstall

Uninstall deletes `~/.argent/telemetry-id`. Because the id is
`uuidv5(fingerprint)` and the fingerprint is deterministic per machine,
a fresh install recomputes the identical id. Previously only the
tool-server resolved the fingerprint while the installer emitted under a
random UUID, so this cycle produced a different random id each time.

## Bundling note for reviewers

`@argent/telemetry` now depends on `@argent/native-devtools-ios` to
locate the binary. That package computes its bin dir from `__dirname` at
module load, which is undefined in an ES module. The installer / CLI /
MCP ship as ESM bundles, so `bundle-tools.cjs`'s ESM banner is extended
to shim `__dirname` / `__filename` (the CJS tool-server bundle already
has them natively). Without this shim those bundles would crash on load,
so the banner change is load-bearing.

## Testing

- Unit: 219 telemetry tests - RFC 4122 v5 vectors, migration,
idempotency, case/length validation, corrupt-file self-heal,
spawn-at-most-once, and all fallbacks.
- E2E on a real machine + booted iPhone 16 Pro: installer, CLI, and
tool-server runtimes all emit the same derived id; the id is stable
across a delete/recreate (uninstall/reinstall) cycle; a legacy v4 id
migrates locally with no alias/identify emitted; verified from an ESM
bundle placed in the real install's `dist/` so the binary resolves via
the bundled `__dirname` with no env override; tool calls against the
device carry the same id.
2026-07-07 16:28:29 +02:00
Ignacy Łątka a92c859d07 feat(argent-lens): argent lens — bind a claude session to the preview window (#416)
## What

Adds `argent lens` — open Argent Lens bound to a fresh `claude` session.

## Why

The Lens preview window and the agent communicate through a shared,
process-wide tool-server that carries **no per-agent identity** (only a
coarse telemetry header). So when the user requests changes in the
window, there's no reliable way to route them to the *specific* agent
that opened it.

`argent lens` sidesteps the problem by inverting ownership: the human
launches Lens, and Lens spawns (and therefore owns) exactly one `claude`
terminal. The binding is 1:1 by construction — no identity plumbing
required.

## How it works

1. `argent lens` ensures a tool-server and **begins a CLI Lens
session**, which opens the preview window up front (no
`await_user_selection` needed) and keeps it open across rounds.
2. It spawns a tracked `claude` terminal in the current directory — a
TypeScript port of the macOS applet's `osascript` spawn/track mechanism
— seeded to propose variants without blocking.
3. A watcher polls `GET /preview/outcome`; on each submitted round it
types a one-line feedback summary into that terminal, queuing it to the
agent as its next prompt. Requesting changes therefore "talks back" to
the bound agent instead of closing the window.

macOS only (drives Terminal/iTerm via `osascript`); off-darwin it exits
with a clear message.

## Changes

- **CLI** `lens-terminal.ts` — `osascript` spawn/track + a new
write-into-running-session path (the "outputting" channel; injects input
into the running TUI, which a tty write cannot do).
- **CLI** `lens.ts` — the command + feedback formatter.
- **tool-server** — a `cliSession` flag in the variant store (opens the
window, suppresses auto-close on submit); `POST /preview/cli-session`
and `GET /preview/outcome` routes.
- **UI** — relabels the submit action to "Request changes" while a CLI
session is active.

Behind the existing `argent-lens` feature flag.

## Verification

- Build + typecheck across the workspace; ESLint (`--max-warnings 0`) +
Prettier clean.
- 34 new unit tests; full `@argent/cli` suite **118 passing**,
tool-server variant/preview suites **46 passing**.
- **E2E (observed on this machine):**
- **tool-server HTTP** — `cli-session` toggles `cliSession` true/false;
submitted feedback round-trips through `/preview/outcome`.
- **terminal injection** — spawned a window running `cat`, confirmed
`writeToSession` lands text in the running program's **stdin** (both
iTerm and Terminal), with embedded newlines flattened.
- **full relay** — the real `watchAndRelay` loop against a real
tool-server typed two distinct submission rounds' feedback into a
stand-in agent terminal (dedup on `completedAt` verified).
- `argent lens --help` and the feature-flag gate (exit 1 with guidance,
no spawn).

Note: a real end-to-end with a live `claude` agent was not run (it would
drive a paid interactive session); the agent-facing channel is proven by
injecting into a stand-in process's stdin via the exact same code path.

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
2026-07-01 19:57:56 +02:00
Juliusz Wajgelt 20f5a1282c feat: initial sim-remote support (#291)
## Summary

Adds **initial support for remote iOS simulators** ("sim-remote") —
simulators that run in the cloud (radon-cloud orchestrator) and are
driven from the dev's machine over a QUIC/MoQ tunnel, alongside the
existing local iOS simulators. A remote sim is exposed as a new device
platform, `ios-remote`, and the existing
interaction/inspection/profiling tools work against it through the same
tool surface.

To get there without scattering `isRemote` branches across every tool,
the iOS code paths are refactored behind a small set of
host/transport/backend abstractions so each tool stays
transport-agnostic.

## How it works

- **Discovery** — `list-devices` enumerates remote sims via the
`sim-remote` CLI in addition to local `simctl`. They classify as
`platform: "ios-remote"`. Enumeration is best-effort: if `sim-remote`
isn't installed or the user isn't logged in, it silently returns none.
- **Transport** — remote interaction goes over a MoQ relay
(WebTransport/QUIC). `moq-client.ts` subscribes to the server-published
`simulator` broadcast (catalog/video/screenshot) and publishes a
`control` track carrying protobuf-encoded input commands (touch / key /
button / rotate / wheel / screenshot). WebTransport is provided in Node
by `@fails-components/webtransport`, polyfilled lazily on first use.
- **Native devtools over the tunnel** — unix sockets don't bridge the
QUIC tunnel, so on remote sims `ax-service` and `native-devtools` are
forced onto TCP with OS-assigned ephemeral ports, and a reverse proxy is
wired before the in-sim daemon dials back.

## Key components

New (`packages/tool-server/src/`):
- `utils/sim-remote.ts` — thin wrapper around the `sim-remote` CLI
(simctl verbs, `moq-info`, proxy), propagating CLI stderr verbatim.
- `utils/moq-client.ts` — pure-JS MoQ client for the remote
simulator-server relay.
- `utils/datachannel-proto.ts` + `proto/datachannel.proto` — vendored
protobuf schema + encoders for the control channel.
- `utils/ios-host.ts` — `IosHost` abstraction (`localIosHost` /
`remoteIosHost`, picked by `pickIosHost(device)`) capturing the
local-vs-remote differences (transport, env setup, proxying,
`requiresTcp`).
- `utils/simctl-backend.ts` — strategy for simctl verbs so one iOS
handler serves both `xcrun simctl` (local) and `sim-remote simctl`
(remote).
- `utils/ax-prefs.ts` — AX-pref helpers extracted from `ax-service`.
-
`tools/{launch-app,open-url,reinstall-app,restart-app}/platforms/ios-remote.ts`
+ shared handlers.

Refactors:
- **Strategy pattern** to limit `isRemote`/platform branching inside
tool and blueprint implementations.
- `simulator-client.ts` gains a transport abstraction
(`SimulatorServerTransport`) so call sites stay transport-agnostic;
local sims back it with WebSocket+HTTP, remote sims with the MoQ client.
- `ax-service` / `native-devtools` blueprints updated for ephemeral TCP
ports and the `ios-remote` platform.
- Dependency checks: tools that pick a platform path internally now gate
on the right CLI (`sim-remote` vs `xcrun`).

## New dependencies

`@moq/lite`, `@moq/net`, `@fails-components/webtransport`,
`@fails-components/webtransport-transport-http3-quiche`, `protobufjs`.

## Testing

- Existing unit suite passes (CI green); the native-devtools
status/describe tests now stub the platform dep-check so they exercise
their logic, not the host toolchain.
- E2E device-smoke jobs (local iOS sim, Android, Chromium, remote VVD)
pass.

---

<sub>Branch rebased onto latest `main` (linear history). `sim-remote` is
opt-in — only required when driving a remote iOS simulator.</sub>
2026-07-01 15:57:47 +02:00
filip131311 1aaa94d118 feat(telemetry): detect cloud/remote AI-agent environment and tag every event (#418)
## What

Adds a **`cloud_agent`** field to the base telemetry props — alongside
`is_ci` — naming the **cloud/remote AI coding-agent environment** the
process is running in, or `null`. Since it lives in the base props, it's
attached to **every** telemetry event automatically.

Crucially this is **cloud-only**: it fires for vendor-hosted / remote
autonomous runs, **not** for a developer running an agent CLI locally. A
laptop `cursor-agent`/`claude` session returns `null`; only a genuine
remote environment resolves to a slug. (Generic cloud/CI execution stays
`is_ci`; this names the specific agent.)

## How

- **New `packages/telemetry/src/cloud-agent-detect.ts`** —
`detectCloudAgent(env?)` returns a canonical slug or `null`. Modelled on
`ci-detect.ts`: takes `env` lazily for tests, runs nothing at import
time, and only ever emits a fixed allowlist slug — a raw env value is
never copied into telemetry.
- **`base-props.ts`** — adds `cloud_agent: CloudAgent | null` next to
`is_ci`, computed once in the memoized invariant block (no per-event
cost in the long-lived tool-server).

## Signals (only high-confidence remote markers; local CLI use → `null`)

| Slug | Cloud marker | Basis |
|---|---|---|
| `claude_code` | `CLAUDE_CODE_ENVIRONMENT_KIND ∈ {byoc,
anthropic_cloud}`, a `remote_*`/`claude-in-teams` entrypoint, or
`CLAUDE_CODE_REMOTE_SESSION_ID` | Verified against the Claude Code
binary's own internal cloud check |
| `cursor` | `CURSOR_AGENT_WORKER_ID` / `CURSOR_WORKER_POOL_NAME`
(worker/pool exec) | Verified `cursor-agent` reads it from env; the
local CLI sets `CURSOR_AGENT` + `CURSOR_AGENT_CLI_LOCAL_MODE` instead |
| `copilot` | `GITHUB_ACTIONS` + `copilot-swe-agent` actor/workflow |
Confirmed actor identity (also `is_ci`) |
| `replit` | `REPLIT_AGENT` | Agent-specific (the broad `REPL_ID` fires
in any Repl and is **not** used) |
| `devin` | `/opt/.devin` VM marker | Confirmed (`@vercel/detect-agent`)
|
| `jules` | `/opt/environment_summary.sh` VM marker | Confirmed
(official Jules docs) |

### Deliberately excluded
- **Codex cloud** — no reliable cloud-specific marker exists
(`CODEX_SANDBOX` is the *local* sandbox signal; OpenAI declined an
`AGENT=codex` flag in `openai/codex#13416`). Not detected rather than
guessed.
- **Local agent CLIs** — `CURSOR_AGENT`, `CLAUDECODE` with
`entrypoint=cli`, plain `REPL_ID` workspaces: intentionally `null`.

## Privacy
Reads only a fixed allowlist of env-var **names** plus two filesystem
markers (`/opt/.devin`, `/opt/environment_summary.sh`); emits only a
fixed slug. The filesystem checks are injectable and guarded so a
permission error can never break telemetry.

## Tests
- New `test/cloud-agent-detect.test.ts` — every vendor signal, plus the
key **negatives** (local Claude `entrypoint=cli`, local `cursor-agent`,
plain Repl `REPL_ID`, normal CI run) asserting `null`.
- Updated `test/base-props.test.ts` for the renamed key.
- Full telemetry suite **191 passing**, typecheck + build clean. Live
check confirms this local Claude Code session resolves to `null` while a
simulated `byoc`/remote env resolves to `claude_code`.

---

> Scope note: started as a broad "is an agent in the loop" flag
(`agent_env`); narrowed to cloud-only per review — a human using an
agent locally shouldn't look like a cloud run.
2026-06-26 16:33:32 +02:00
Hubert Gancarczyk 0edefebe06 fix(telemetry): orchestator sub-tools invocations was missing ai_client and platform attributes (#400)
## Summary

Fixes telemetry attribution for tools dispatched by orchestrators.
`run-sequence`,
`flow-execute`, and `flow-add-step` run their steps via
`registry.invokeTool()`
directly, which minted a fresh, unregistered invocation id per nested
call — so the
`ai_client` / `platform` the HTTP layer captured for the outer request
never reached
the nested gestures, and they were recorded as **anonymous** in
telemetry.
Remove `opt_out` event that is a leftover.

## Changes

- Add an optional `recordChildInvocation` recorder to
`InvokeToolOptions` /
`ToolContext`, bound by the HTTP layer to the outer request's
attribution.
- New `invokeSubTool` helper: registers each sub-tool under a minted id
inheriting
the request's AI client, with the platform re-derived from the
sub-tool's **own**
device arg (`udid` / `device_id` / `avdName`); forwards the recorder so
propagation
survives arbitrary nesting (e.g. `flow-execute → run-sequence →
gesture-tap`).
- Route the three orchestrators' sub-dispatch through
- Refactor platform inference in the HTTP layer into a shared
`platformFromArgs`
  helper (also hardened to not throw on a malformed device arg).
- Remove opt_out event leftover

Why re-derive platform per child: an orchestrator like `flow-execute`
has no platform
of its own and a single flow can target several devices, so the child's
device arg is
the only correct platform source; the parent's platform is the fallback.
2026-06-24 14:03:57 +02:00
Piotr Krawiec 0bed3624a0 feat(vega): Amazon Vega (Fire TV) platform support (#366)
## Summary

Adds **Vega (Amazon Fire TV)** as a third Argent platform alongside iOS
and Android. Vega apps are React Native 0.72 + Hermes running on a QEMU
**Virtual Device (VVD)**, driven by a **D-pad tv-remote rather than
touch**. This PR extends Argent's platform abstraction so the existing
tool surface gates correctly on `vega`, and adds the Vega-specific tools
needed to actually drive a Fire TV app end to end.

### Platform abstraction

- `Platform` gains `vega`; `DeviceKind` gains `vvd` (the QEMU VVD);
`ToolCapability` gains a `vega { vvd, device }` block; `ToolDependency`
gains `vega`.
- `assertSupported()` selects the `vega` capability matrix for Vega
devices; `dispatchByPlatform()` gains an optional `vega` branch, and a
Vega dispatch with no branch throws `NotImplementedOnPlatformError`
(501).
- New `vega-cli` util (modeled on `adb.ts`) resolves `vega`/`kepler`
from `PATH` or `~/vega/bin` with the same hardened exec/quoting;
`check-deps` resolves and hints the `vega` dependency.
- The `simulator-server` blueprint is never wired for `vega` (Vega uses
`adb` + the `vega`/`kepler` CLI, not the proprietary binary).

### How Vega is wired

- **Input / screenshot / describe** go over `adb` (input via
`inputd-cli`, screenshots host-side via the Android emulator console,
element tree via the on-device automation toolkit).
- **App lifecycle** goes over the `vega`/`kepler` CLI.
- v1 is **Virtual-Device-only**: every Vega capability is declared
`vvd`. Physical Fire TV is out of scope and untested — the tool-server
does not connect to or detect hardware, so `resolveDevice` classifies
every `amazon-` serial as a VVD by shape. A physical device is therefore
not classified as `device` and not rejected at the capability gate;
supporting and gating real hardware is deferred to a version where it
can be tested.
- Vega runs **React Native 0.72**, which predates the new React Native
DevTools. The CDP/Hermes debugger surface (`debugger-*`, React/native
profilers) depends on integration points the new DevTools provides, so
**debugging and profiling are out of scope for this PR** — Vega
profiling/crash analysis is handled by the
`amazon-devices-buildertools-mcp` server instead.

### Also includes

- `argent-vega` skill documenting the full Vega workflow (target
discovery, the describe → compute-path → `tv-remote` navigation loop,
text injection).
- Vega VVD end-to-end CI workflow.

## Feature support by platform

Legend: ✅ supported · ❌ not supported · ⚠️ degraded / conditional · —
not applicable

| Feature (tool) | iOS | Android | Chromium | **Vega (VVD)** | Vega
notes |
|---|:---:|:---:|:---:|:---:|---|
| **Device discovery** (`list-devices`) | ✅ | ✅ | ✅ | **✅** | Tagged
`platform:"vega"`, `kind:"vvd"`; pass `serial` as `udid` |
| **Boot / start device** | ✅ | ✅ | — | ✅ |  |
| **Launch app** (`launch-app`) | ✅ | ✅ | ✅ | **✅** | `bundleId` =
interactive-component app id from `manifest.toml` |
| **Restart app** (`restart-app`) | ✅ | ✅ | ✅ | **✅** | Terminate +
relaunch |
| **Reinstall app** (`reinstall-app`) | ✅ | ✅ | ❌ | **✅** | Uninstall +
install a `.vpkg` |
| **Screenshot** (`screenshot`) | ✅ | ✅ | ✅ | **✅** | Host-side via
emulator console (needs `adb`); rotation ignored (fixed landscape) |
| **Describe / element tree** (`describe`) | ✅ | ✅ | ✅ | **✅** |
On-device automation toolkit; surfaces `[focused]`/`[selected]` +
normalized frames |
| **Touch gestures** (`gesture-tap`/`swipe`/`scroll`/`drag`/`custom`) |
✅ | ✅ | ✅ | **❌** | Vega is tv-remote-driven; gestures error — use
`tv-remote` |
| **Pinch / rotate gesture** (`gesture-pinch`/`gesture-rotate`) | ✅ | ❌
| ⚠️ | **❌** | N/A on a TV |
| **Remote / D-pad** (`tv-remote`) | — | — | — | **✅** | Vega-only tool;
single key, repeat, or a whole path in one call |
| **Hardware buttons** (`button`) | ✅ | ✅ | — | **❌** | Remote keys live
in `remote` instead |
| **Text input** (`keyboard`) | ✅ | ✅ | ✅ | **✅** | Whole-string
injection via `inputd-cli send_text` |
| **Rotate orientation** (`rotate`) | ✅ | ✅ | — | **❌** | TV framebuffer
is fixed landscape |
| **Open URL / deep link** (`open-url`) | ✅ | ✅ | ✅ | **❌** | Not wired
on Vega |
| **JS debugger** (`debugger-*`) | ✅ | ✅ | ✅ | **❌** | RN 0.72 predates
the new RN DevTools — out of scope for this PR |
| **Native devtools / AX** (`native-describe-screen`, etc.) | ✅ | ❌ | —
| **❌** | iOS-only |
| **Native profiler** (`native-profiler-*`) | ✅ | ✅ | — | **❌** | Use
`amazon-devices-buildertools-mcp` (Perfetto) instead |
| **React profiler** (`react-profiler-*`) | ✅ | ✅ | — | **❌** | Out of
scope; RN 0.72 / new RN DevTools not supported |
| **Screenshot diff** (`screenshot-diff`) | ✅ | ✅ | ✅ | **✅** |
Host-side image comparison; platform-agnostic |
| **Flows record/replay** (`flow-*`) | ✅ | ✅ | ✅ | **✅** | Records any
tool-call sequence |

## Test plan

- [x] `list-devices` surfaces connected Vega devices (`platform:"vega"`,
`kind:"vvd"`)
- [x] `launch-app`, `restart-app`, `reinstall-app`,
`list-installed-apps` work on Vega
- [x] `tv-remote` drives focus (single key, `repeat`, and multi-key
path) and `keyboard` injects whole strings
- [x] `screenshot` returns a frame via the host-side VVD path
- [x] `describe` returns the on-screen element tree with `[focused]`
state and normalized coordinates
- [x] Vega VVD e2e CI workflow passes
- [ ] Test multiple platforms being driven at the same time
    - [x] Android + Vega
    - [ ] iOS + Vega
    - [ ] chrome + Vega

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:31:45 +02:00
Hubert Gancarczyk 8483fe23f9 feat: add telemetry (#274)
## Summary

Adds anonymous, opt-out telemetry for Argent installer, update/uninstall
flows, CLI telemetry controls, tool-server lifecycle, and tool
invocation health signals.
## What Changed
- Added a new private `@argent/telemetry` package
- Introduced typed telemetry events with runtime property
allowlisting/sanitization.
- Added lazy anonymous identity creation in `~/.argent/telemetry-id`.
- Added consent handling via: `argent telemetry enable`, `argent
telemetry disable`, `argent telemetry status`
- Added local debug logging with `ARGENT_TELEMETRY_DEBUG=1`.
- Tracks installer funnel events for init, global install/update
decisions, editor selection, allowlist decisions, skill installation,
rules/agents copy, update, and uninstall.
- Tracks tool-server start/stop and tool invocation/complete/fail
events.
- Adds per-tool invocation IDs so request metadata can be correlated
without relying on ordering.
- Records only coarse platform metadata (`ios` / `android`) for device
tools
- Ensures short-lived commands drain telemetry with bounded shutdown
before exit.
- Wires telemetry package into bundling and injects full Argent CLI
version metadata at build time.
- Added central `FAILURE_CODES` and `FailureSignal` helpers in
`@argent/registry`.
- Attached structured, sanitized failure metadata across registry
errors, tool-server tools/services, HTTP validation/dependency gates,
installer flows, and `argent run`.
- Added telemetry allowlisting for safe failure fields only: static
error codes/stages, coarse area/kind/command, bounded exit codes,
allowlisted signals/spawn codes, and coarse network failure classes.
- Split CLI-local `argent run` failures into `cli:run_fail` so they are
not double-counted as tool failures.
- Added installer telemetry for failed package actions, update/uninstall
failures, rules/agents copy results, and skill refresh results.
- Made telemetry consent writes atomic and kept telemetry status
side-effect free via `peekAnonId()`.
- Adds a coarse, anonymous signal to tool telemetry: which AI coding
tool is driving the MCP server (Claude Code, Codex, Cursor, Gemini, VS
Code, Windsurf, Zed, opencode, Copilot, or other).

## Tests Added / Updated

- Telemetry unit coverage for consent precedence, identity creation,
base props, sanitization, PostHog config, debug logging, shutdown,
opt-out, and forget behavior.
- Registry/tool-server tests for invocation IDs, platform metadata
boundaries, startup failure flushing, and tool lifecycle telemetry.
- Installer tests for final telemetry drain, update action
classification, and uninstall identity cleanup ordering.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:22:34 +02:00