The tool-server binds to 127.0.0.1, but a public attacker page that
DNS-rebinds its hostname to 127.0.0.1 still reaches us — the Host header
is the only signal that distinguishes that traffic from a legitimate
same-origin request.
Adds an early middleware (runs before CORS) that 403s any request whose
Host header is not a loopback name (127.0.0.1, localhost, ::1). IPv6
literals in brackets are normalised before the check.
Without this, an attacker can pair a DNS-rebinding flow with the
existing wildcard CORS to read /tools and POST to /tools/:name from any
public origin.
All skills are currently exposed as `argent-***`. In the markdown
though, we refer to them as just `***`. This may confuse the agent when
it tries reading `skills/***` instead of `skills/argent-***`.
This PR changes all skill references in docs to the correct `argent-***`
form.
## Why?
There's no downside to this fix. Keeping it as-is potentially introduces
some confusion especially if when the user is running a weaker model or
if we encounter namespace conflicts.
## Summary
Refactor of the iOS profiler (`xctrace`-driven) session to make start,
stop, and unexpected-exit handling proper, without the iOS hanging
infinitely.
### Startup (`ios-profiler-start`)
- **Locale ready signal**: subscribe-before-spawn to a Darwin
notification (`com.argent.ios-profiler.started.<pid>.<ts>`) via
`notifyutil`, passed through `xctrace --notify-tracing-started`. Falls
back to the existing stdout substring match (`Starting recording` /
`Ctrl-C to stop`) if `notifyutil` fails to spawn or register within 2 s.
- **Startup timeout**: new 10 s cap on reaching the ready state — on
timeout, `SIGKILL` the child so it cannot leak.
- **Bounded `simctl` calls**: `detectRunningApp` now applies a 10 s
timeout to both `xcrun simctl spawn ... launchctl list` and `xcrun
simctl listapps`, with explicit error messages telling the user to
verify the simulator is booted.
- **Cold-start retry**: if xctrace's process resolver fails with
`"Cannot find process matching name:"` (launchd has registered the
bundle but xctrace's resolver hasn't caught up), the tool retries once
after a 1.2 s backoff — up to 2 attempts total. On exhausted retries
throws a classified error naming the cold-launch race and pointing at
`app_process` as the override. Non-cold-start failures propagate
immediately without retry.
- **Exit listener installed**: every `xctrace record` child gets a
long-lived `'exit'` handler (`handleXctraceExit`) that marks the session
as `recordingExitedUnexpectedly` and stores `lastExitInfo`.
### Shutdown (`ios-profiler-stop`)
- **SIGINT → SIGTERM → SIGKILL ladder** via
`utils/ios-profiler/lifecycle.ts::shutdownChild` (30 s grace, 5 s term,
5 s kill).
- **Returns a `warning`** when the ladder had to escalate past SIGINT,
so callers know the trace bundle may be incomplete.
### Exit recovery
- **New `recordingTimedOut` / `recordingExitedUnexpectedly` flags** on
the session API. If `ios-profiler-stop` is called after either fired,
the tool now exports whatever `.trace` bundle is on disk and returns it
with a descriptive `warning` instead of throwing "no active session".
- Covers the in-process 10-min recording cap, attached-app crashes, and
simulator daemon hiccups.
### Dispose semantics
- Registry teardown (process shutdown) skips SIGINT and goes straight to
`SIGKILL` with a 1 s reap window. The explicit `ios-profiler-stop`
contract is the only path that takes the slow finalise grace.
### Other
- `utils/ios-profiler/run-with-timeout.ts`: `execSyncWithTimeout`
wrapper with a 60 s default — guards every shelled-out `xctrace export`
/ `xctrace version`.
- New reference doc `utils/ios-profiler/IOS_PROFILER_REFERENCE.md`
covering the native foundations, capture flow, and pipeline.
## Tests
New unit tests under `packages/tool-server/test/ios-instruments/`
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- Bumps all 9 published packages from `0.6.0-next.2` → `0.6.0` (stable)
in preparation for the `0.6.0` release.
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is.
- Lockfile updated via `npm install`; `npm run format` was a no-op.
## Test plan
- [ ] CI green
- [ ] Run the stable publish workflow against this commit and confirm
publish succeeds
## Summary
- Bumps all 9 published packages from `0.6.0-next.1` → `0.6.0-next.2` in
preparation for the next prerelease publish.
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is.
- Lockfile updated via `npm install`; `npm run format` was a no-op.
## Test plan
- [ ] CI green
- [ ] Run `Publish prerelease to npm (next)` workflow against this
commit and confirm publish succeeds
## Summary
- Bumps all 9 published packages from `0.6.0-next.0` → `0.6.0-next.1` in
preparation for the next prerelease publish (the `0.6.0-next.0` train
was blocked by the build-config and update-checker fixes in #172 and
#173).
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is,
matching #171.
- Lockfile updated via `npm install` — only the 9 workspace `version`
entries changed.
## Test plan
- [ ] CI green
- [ ] Run `Publish prerelease to npm (next)` workflow against this
commit (after #172 and #173 land) and confirm publish succeeds
## Summary
- The homegrown `parseSemver` in
`packages/tool-server/src/utils/update-checker.ts` only matched plain
`MAJOR.MINOR.PATCH` and returned `null` for prerelease strings. After
[#171](https://github.com/software-mansion/argent/pull/171) bumped the
local version to `0.6.0-next.0`, `isNewerVersion` started returning
`false` for **any** value of `latest`, because the *current* side failed
to parse. Two consequences:
- Users on the `next` dist-tag are never notified of new releases
(silent feature degradation, not a crash).
- The `detects an available update on startup` unit test broke because
its asserted state now reads `{ updateAvailable: false, latestVersion:
"99.0.0" }` instead of `{ true, "99.0.0" }` — see [run
25161184594](https://github.com/software-mansion/argent/actions/runs/25161184594/job/73755770427).
- Replaces the regex parser with the `semver` package (already pinned at
`^7.7.4` and present in the lockfile via `@argent/installer`). Adds it
as a dep of `@argent/tool-server` along with `@types/semver`.
- Keeps the existing UX rule: don't notify when upstream `latest` is
itself a prerelease — npm's `latest` dist-tag should be stable, and we
shouldn't push betas to stable users. `semver.gt` correctly treats a
stable version as greater than its own prereleases, so a user on
`0.6.0-next.0` will be notified when `0.6.0` ships.
## Behavior matrix
| current | npm `latest` | before (broken) | after |
|---|---|---|---|
| `0.6.0-next.0` | `99.0.0` | not flagged | **flagged** |
| `0.6.0-next.0` | `0.6.0` | not flagged | **flagged** |
| `0.6.0` | `99.0.0` | flagged | flagged |
| `0.6.0` | `1.0.0-beta.1` | not flagged | not flagged |
| anything | invalid string | not flagged | not flagged |
## Test plan
- [x] `npm test -w @argent/tool-server -- test/update-checker.test.ts` —
all 10 tests pass (was 9/10).
- [x] `npm test -w @argent/tool-server` — full suite 444/444 passes.
- [x] `npx tsc --build` clean from the repo root.
- [ ] CI green on this branch.
## Summary
- Bump all published packages to `0.6.0-next.0` in preparation for the
next prerelease train.
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`@argent/native-devtools-ios` was at `0.5.1`; aligned with the rest.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is.
## Test plan
- [ ] CI green
- [ ] `npm next` prerelease publish workflow picks up the new version
## What this PR does
This refactor:
- replaces the DFS fiber-walking hook inside the app's JS runtime with
the **React DevTools backend** as the source of profiling data
- adds a session-ownership system so two tool-server instances can't
silently clobber each other
- gives agent a new status tool for recovery after interruptions
- improves what the profiling analysis report shows the agent
I realise this a big pull request, but changes began to affect each
other when it comes to registered scripts on the react-native side.
This, all-in-all, should however cover our needs when it comes to the
react profiler and if it proves sufficient - we will only have to
iterate on small fixes afterwards.
If I were you, I would skip over the implementation of scripts on the
native-side and focus on what the tool-server does when it comes to
profiling right now.
---
## Profiling workflow
The overall flow has not changed from the agent's perspective - agent
calls `react-profiler-start`, do something in the app, call
`react-profiler-stop`.
What changed is how the profiling data is collected.
**Before:** `react-profiler-start` injected a custom `onCommitFiberRoot`
hook that walked the live fiber tree at every React commit in DFS-like
way and appended records to a global `__ARGENT_DEVTOOLS_COMMITS__` array
in the app's JS heap. On stop, the array was fetched in chunks over CDP.
**Now:** `react-profiler-start` calls `ri.startProfiling(true)` on the
React DevTools backend's `rendererInterface`. React's own profiler
accumulates commit data internally. On stop, a single
`STOP_AND_READ_SCRIPT` call stops the backend and reads
`ri.getProfilingData()` in one round-trip. The DevTools backend data is
richer, more accurate, and doesn't require maintaining a bespoke
fiber-walking implementation.
---
## Session ownership
The profiler runs inside the app's JS heap. There's only one "slot": it
used to be that when two tool-server instances (or the same instance
after a process restart) both try to start profiling, the second call
silently overwrites the first and all data from the first session is
gone.
Right now, there is an approach which blocks silent overtake of the
profiling session.
When `react-profiler-start` runs, it:
1. Reads the current state from the runtime using `READ_STATE_SCRIPT` (a
pure, side-effect-free read).
2. If a session is already running, it reads the
`__ARGENT_PROFILER_OWNER__` object written into `globalThis`, which
contains `sessionId`, `startedAtEpochMs`, and `lastHeartbeatEpochMs`.
3. Classifies the existing session as **fresh** or **stale** using
`classifyStaleness()` (stale = no heartbeat for > 5 minutes).
If the session is fresh and you don't pass `{ force: true }`, start
refuses and returns an `already_running` payload instead of overwriting.
4. If the session is stale or `force: true`, it calls
`STOP_FOR_TAKEOVER_SCRIPT` to cleanly stop the prior session, then
starts a new one.
### Heartbeats
Each `sessionId` UUID is written to `__ARGENT_PROFILER_OWNER__` in the
runtime at start. While profiling, any side query tool
(`react-profiler-renders`, `react-profiler-fiber-tree`) that this same
tool-server process calls will bump `lastHeartbeatEpochMs` via
`HEARTBEAT_SCRIPT`. This keeps the session from being classified as
stale as long as the owning process is still making calls.
### What the agent sees when something goes wrong
Previously, if you called `react-profiler-start` into an already-running
session you'd get a silent clobber or a confusing Hermes error. Now:
- **Running session detected, still fresh:** `{ already_running: true,
owner: { sessionId, startedAtEpochMs, lastHeartbeatEpochMs },
age_seconds, how_to_reclaim }` — the agent can read the owner timestamp
and decide whether to tell the user to wait or force-reclaim.
- **Running session detected, stale:** automatic takeover with a log
note.
- **No owner record (e.g. another DevTools client started the
session):** also automatic takeover.
### New `react-profiler-status` tool
A read-only tool that reports the current state without starting or
stopping anything. Returns `session_status` as one of `"active"` /
`"taken_over"` / `"stopped"` / `"no_react_runtime"`, plus the full owner
record. Use this after a debugger disconnect, an unexpected error, or
when the agent restarts mid-session to decide whether to continue,
reclaim, or give up.
The agent has instructions to use the tool whenever it is interrupted
during the profiling by the user or otherwise. Will have to look into
how much this is actually used.
---
## What changed in the data we collect
The old hook read fiber fields directly (`fiber.type.displayName`,
`fiber.memoizedProps`, `fiber.selfBaseDuration`, etc.) at commit time.
This meant:
- Names for **transient components** (modals, tooltips, navigation
screens) that unmounted before stop were already lost - the fiber was
gone.
- Change-detection logic (which props changed, which hooks fired) was
reimplemented manually against the fiber internals.
The new approach reads `ri.getProfilingData()` which gives us the
DevTools backend's own commit log. Each commit entry has
`fiberActualDurations`, `fiberSelfDurations`, `changeDescriptions`,
`timestamp`, and `duration` - the same data React DevTools Profiler
shows in its browser panel.
### Fiber name cache for transient components
To address the lost-name problem, `FIBER_ROOT_TRACKER_SCRIPT` now hooks
`onCommitFiberRoot` and, immediately after the DevTools backend finishes
processing each commit (synchronously, while the fiber is still in
memory), populates `globalThis.__argent_fiberNames__` with `fiberID →
displayName` entries. Because fiber IDs are monotonically increasing and
never reused, this cache never goes stale. `STOP_AND_READ_SCRIPT` uses
this cache as a fallback for any fiber whose name
`getDisplayNameForElementID` can no longer resolve.
### Unattributed fiber tracking
When a transient fiber renders during the session but still can't be
named at stop time (not in the name cache, not in the DevTools instance
map), its work is now **surfaced rather than silently dropped**. The
stop tool computes `unattributedByCommit`: a per-commit tuple of
`[commitIndex, droppedFiberCount, droppedSelfMs]`. This flows through
to:
- The session metadata file on disk (`unattributedByCommit` array).
- The pipeline (`buildHotCommitSummaries` receives and indexes it).
- The `HotCommitSummary` type gains `unattributedMs` and
`unattributedFiberCount`.
- The generated report shows a warning line per commit with unattributed
work, e.g. `⚠️ 12ms unattributed — 3 fibers unmounted before stop`.
- `react-profiler-stop` returns top-level `unattributed_ms`,
`unattributed_fiber_count`, `unattributed_commit_count` in its response
so the agent immediately knows if there's a gap.
### Actual vs. self duration now both surfaced
The old report showed only `selfDuration` (exclusive render time). Both
are now captured in `HotCommitComponentEntry` and rendered in the report
as:
```
- `ComponentName` — 4.2ms self, 18.7ms w/children
```
A legend at the top of the report explains when to sum self and when not
to sum inclusive time. The summary table also shows both columns.
### Component name unwrapping
Display names from the DevTools backend can be wrapped:
`Forget(Memo(MyButton))`, `ForwardRef(Icon)`, etc. The pipeline's render
stage now strips these wrappers using `annotateComponentName()` and
appends a human-readable tag, so the report shows `\`MyButton\`
[React.memo + React Compiler]` instead of the raw internal string. The
raw DevTools name is still used in all tool-call suggestions (since
every pipeline stage keys on the original string).
---
## What scripts do what
All injected JS scripts now live in a single file —
`packages/tool-server/src/utils/react-profiler/scripts.ts` — grouped by
responsibility:
| Script | When injected | What it does |
|---|---|---|
| `REACT_NATIVE_PROFILER_SETUP_SCRIPT` | Every `react-profiler-start`
(idempotent) | Wraps `ri.startProfiling` / `ri.stopProfiling` to track
`isProfiling` state and `startedAtEpochMs` without clock skew; installs
the `__argent_profilerHeartbeat` helper |
| `FIBER_ROOT_TRACKER_SCRIPT` | On CDP connect (idempotent) | Intercepts
`onCommitFiberRoot` to populate the `__argent_fiberNames__` commit-time
name cache |
| `READ_STATE_SCRIPT` | Every `react-profiler-start`,
`react-profiler-status` | Pure read: returns `{ hookExists,
rendererInterfaceFound, isRunning, owner, nowEpochMs }` |
| `buildStartScript(ownerJson)` | `react-profiler-start` on the happy
path | Calls `ri.startProfiling`, writes owner record, returns
post-start verification flags |
| `STOP_FOR_TAKEOVER_SCRIPT` | `react-profiler-start` on the takeover
path | Stops the prior session cleanly before starting a new one |
| `HEARTBEAT_SCRIPT` | `react-profiler-renders`,
`react-profiler-fiber-tree` | Bumps `lastHeartbeatEpochMs` while the
owning process is still active |
| `STOP_AND_READ_SCRIPT` | `react-profiler-stop` | Stops the backend
profiler and reads `getProfilingData()` + name cache in one round-trip |
| `RESOLVE_FIBER_META_SCRIPT` | `react-profiler-stop` (if commits > 0) |
Walks the live fiber tree to collect `hookTypes`, `parentName`, and
`isCompilerOptimized` metadata per component |
## Summary
Sets up the foundation for adding Android support without changing any
user-visible iOS behavior. iOS works exactly as before; Android handlers
are stubs that throw a clear \"not yet implemented\" message until
they're filled in.
### New primitives
- `Platform`, `DeviceKind`, `DeviceInfo`, `ToolCapability`,
`ToolDependency` (in `@argent/registry`)
- `classifyDevice(udid)` + `resolveDevice(udid)` (UUID shape → iOS, else
Android)
- `assertSupported(toolId, capability, device)` — throws
`UnsupportedOperationError`
- `ensureDep(s)` — host-binary preflight with install hints
- `dispatchByPlatform({ toolId, capability, ios, android })` — one-line
cross-platform `execute()`
### HTTP gating
`/tools/:name` now runs:
- Host-binary preflight (`requires`) → 424 with install hint
- Capability gate (`capability` + `udid`) → 400 with reason
### Per-tool directory layout
Every cross-platform tool now lives in its own directory:
\`\`\`
tools/gesture-tap/
index.ts ← ToolDefinition, capability, dispatchByPlatform
platforms/
ios.ts ← iOS impl (existing logic)
android.ts ← stub: throws \"not yet implemented\"
\`\`\`
Refactored 17 tools this way: gesture-{tap,swipe,custom,pinch,rotate},
screenshot, button, paste, keyboard, launch-app, restart-app,
reinstall-app, open-url, rotate, run-sequence, describe (with shared
contract + iOS adapters under platforms/).
### Tool call flow (iOS, today)
1. Agent calls \`gesture-tap { udid: \"<uuid>\", x, y }\`
2. HTTP zod-parses, then host-binary preflight, then capability gate
(passes — capability has \`apple\`)
3. Tool's \`execute()\` (built by \`dispatchByPlatform\`):
- \`resolveDevice(udid)\` → \`{ platform: \"ios\", kind: \"simulator\",
id }\`
- \`assertSupported\` → passes
- branches to \`tapIos(services, params, device)\`
4. iOS impl uses \`SimulatorServerApi\` exactly as before.
### Tool call flow (Android, future)
Same flow; the Android branch in \`dispatchByPlatform\` runs. Filling in
Android = three steps:
1. Implement the function in \`tools/<tool>/platforms/android.ts\`
2. Add \`android: { emulator: true, device: true, unknown: true }\` to
the tool's capability
3. Make \`services()\` platform-aware where the iOS service URN doesn't
apply (e.g. \`launch-app\` declares \`nativeDevtools\` for iOS only)
## Test plan
- [x] All 395 existing tests pass; 9 new tests for `device-info` +
`capability`
- [x] TypeScript clean across `@argent/registry` and
`@argent/tool-server`
- [ ] Manual smoke-test on a real iOS simulator (gesture-tap,
screenshot, describe, launch-app)
## Deferred to follow-ups
- **Per-tool capability declarations on iOS-only tools**
(`native-devtools/*`, `profiler/ios-profiler/*`, `network/*`,
`simulator/*`). They still work; the HTTP capability gate is a no-op
when no declaration exists. Adding them is mechanical and doesn't affect
iOS behavior.
- **`list-simulators` / `boot-simulator` rename to `list-devices` /
`boot-device`** — that's a behavior change, paired with the Android
impls.
- **Platform-aware `services()`** for tools whose iOS service has no
Android equivalent (e.g. `nativeDevtools` in `launch-app` /
`restart-app`). Needed when filling in Android impls.
## Summary
These tests were taking up too long.
| File | Before | After |
|------|-------:|------:|
| `fetch-timeout.test.ts` hanging-POST test | 6281ms | ~600ms |
| `workspace-reader.test.ts` (full file) | ~4500ms | ~140ms |
| `gather-workspace-data.test.ts > valid snapshot` | ~590ms | ~3ms |
**Package totals:** `packages/mcp` 6.44s → ~830ms,
`packages/tool-server` 5.29s → ~790ms.
### What was slow and why
- `fetchWithReconnect` is now exported from `mcp-server.ts` so the test
uses the real function. The config accepts an `expBackoffBase` and a
`fetchTimeoutMs` so the hanging-POST test shrinks the backoff base to
`5ms` and the per-attempt timeout to `100ms` (~575ms total)
- every `readWorkspaceSnapshot` call spawned 8 real subprocesses
(`node`, `npm`, `yarn`, `pnpm`, `bun`, `pod`, `eas`, `expo`) via
`runVersionCommand`. Added `vi.mock("node:child_process", ...)` that
stubs `execFile`.
* feat(mcp): support progressive tool loading via _meta hints
Add optional alwaysLoad and searchHint fields to ToolDefinition,
forward them through GET /tools, and emit them as
_meta["anthropic/alwaysLoad"] / _meta["anthropic/searchHint"] in the
MCP ListTools response. Claude Code's ToolSearch defers MCP tools by
default; tools marked alwaysLoad stay in context on every turn, and
searchHint feeds the BM25 ranker so deferred tools surface for
relevant queries.
Mark the core discovery + interaction tools as alwaysLoad so Claude
never has to round-trip through ToolSearch to tap the screen:
describe, debugger-component-tree, screenshot, gesture-tap,
gesture-swipe, launch-app, run-sequence. Everything else defers.
* feat(mcp): mark restart-app as alwaysLoad
Tools that depend on controlling app startup (relaunch for clean
state, refreshing native-devtools injection) need restart-app to be
immediately callable rather than routed through a search round-trip.
* fix: address crash recovery review issues from #135
- crashShutdown now exits with code 1 (was 0 via shared shutdown())
- reconnect() failure no longer aborts the retry loop
- health monitor timeout 3s → 10s (avoids false positives on busy server)
- ListToolsRequestSchema lets errors propagate (empty list could be cached)
- event emitter catches async listener rejections
- forceExit timer keeps event loop alive (removed .unref())
- stderr error handler no longer writes to stdout
- SIGINT/SIGTERM handlers fall back to process.exit(0) if shutdown is null
- fetchTools() checks res.ok
* fix: clarify stderr error handler comment
* fix: revert stderr handler — stdout fallback is safe
The launcher uses readline + regex matching on stdout, so extra
non-matching lines are harmlessly discarded. Restore the original
stdout fallback for stderr errors.
* fix: add fetch timeout, MCP error handlers, and idle timer on GET /tools
- fetchWithReconnect: wrap each attempt in an AbortController with a
configurable timeout (ARGENT_FETCH_TIMEOUT_MS, default 30s) so a
hanging tool-server doesn't wedge the MCP parent indefinitely
- MCP parent (mcp-server.ts): install process.stdout/stderr error
handlers and uncaughtException/unhandledRejection listeners so an
EPIPE from a closed Codex transport doesn't crash the process
- GET /tools: touch the idle timer so list_tools polling keeps the
tool-server alive between tool calls
- Add tests for fetch timeout and idle timer on GET /tools
* chore: update showcase GIF asset
* Revert "chore: update showcase GIF asset"
This reverts commit 2b9379fb05.
* fix: add safety timeout to shutdown and validate FETCH_TIMEOUT_MS
- shutdown() now has a 5s force-exit safety timeout matching
crashShutdown, preventing hangs if server.close() blocks on a
lingering connection
- ARGENT_FETCH_TIMEOUT_MS is validated: NaN/0/negative values fall
back to 30s instead of causing immediate aborts
* fix: remove dead null guard on SIGINT/SIGTERM handlers
shutdown is always assigned (line 57) before the handlers are
registered (line 98-99) in the same synchronous block. The arrow
wrapper is still needed to avoid passing the signal string as exitCode.
* cleanup unneeded bang op
* update comment
* update comment
* prettier
* fix: rewrite fetch-timeout test to not depend on pre-built dist
The test spawned `node dist/cli.js mcp` which requires running
`npm run build:mcp` first. If the dist is stale (e.g. from main),
the old code without the AbortController timeout runs and the test
hangs. Rewrite as an in-process test using a replica of the
fetchWithReconnect pattern against a hanging HTTP server.
* test: add minimal EPIPE → Transport Closed repro
Proves the mechanism: a Node child (mimicking argent mcp) without a
stdout error handler dies on EPIPE when the parent (mimicking Codex)
closes its read end. With the handler, the child survives and the
transport stays usable.
* revert: remove speculative EPIPE stdout/stderr handlers from MCP parent
The EPIPE defense assumed Codex would close its read end of the stdio
pipe while the MCP server is still alive. We can't evidence this from
the logs — the proven root cause is the fetch-timeout hang (repro #3).
Remove the handlers and the EPIPE repro test.
* revert emitter promise error handling
* use universal process timeout value
* remove dubious fixes
* remove unused const
* rev event emitter to main
* revert mcp server to main
* move const to file scope
---------
Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
* fix: add tool server crash recovery to MCP server
- MCP CLI: log uncaught exceptions/rejections but don't exit when
running as MCP server (long-lived stdio process must survive)
- Tool server: attempt graceful registry disposal before exiting on
fatal errors so child processes (simulator servers) are not orphaned
- fetchWithReconnect: retry up to 5 times with exponential backoff
(500ms→8s) giving a restarted tool server time to boot
- ListToolsRequestSchema: add try/catch, return empty list on failure
instead of propagating raw errors
- Health monitor: periodic 30s ping detects tool server crashes between
requests and preemptively triggers reconnect
- Defense-in-depth: catch errors in EventEmitter listeners, handle
rejected promises in simulator watcher and update checker, log
stdout/stderr errors instead of rethrowing
* fix: address PR review feedback
- Halve retry backoff (4 retries, 250ms base → ~3.75s total wait)
- Log initSimulator failures instead of silently swallowing them
`xcrun simctl getenv` silently truncates return values at 127 bytes.
The code was reading with `simctl getenv` but writing with
`simctl spawn ... launchctl setenv`, causing each ensureEnv() cycle to
read a truncated value, fail to recognize the truncated argent path
fragments, preserve them, and append a fresh bootstrap — corrupting
the variable and clobbering third-party dylibs like SimCam.
Fix:
- Read via `simctl spawn <udid> launchctl getenv` (same store, no limit)
- Restore fs.existsSync guard to strip truncated/stale entries while
keeping third-party dylibs that exist on disk (e.g. SimCam)
- Bump mcp version to 0.4.4
* fix: detect project root via .argent dir instead of git
Flow tools previously shelled out to 'git rev-parse --show-toplevel' to
locate the project root, which failed outside git repos and pulled flows
from the wrong place in nested worktrees. Walk up from cwd looking for
an existing .argent/ directory; fall back to cwd so the first
flow-start-recording still creates it in place.
* fix: skip $HOME when walking up for .argent/
The MCP launcher stores its own state in ~/.argent/ (logs, tool-server
pid). Without this guard, running any flow tool from anywhere under
$HOME — but outside a real project with its own .argent/ — would stop
at the home directory and treat it as the project root, mixing personal
MCP state with flow files.
* revert: restore getGitRoot in flow-utils
The walk-up .argent approach is fundamentally broken because the
tool-server is a singleton daemon — its process.cwd() is not the
calling project's cwd. Reverting both 882ed4c and 83e6ae6 so the
next commit can replace the resolution with an AsyncLocalStorage
context populated from an X-Argent-Project-Root header sent by the
argent mcp proxy.
This reverts commit 882ed4c.
This reverts commit 83e6ae6.
* feat: resolve project root via AsyncLocalStorage from mcp proxy header
The tool-server is a singleton daemon shared by every MCP client,
so its process.cwd() is never the calling project's cwd. The git-
root and walk-up heuristics both ran inside the daemon and silently
returned the wrong directory.
Shift the source of truth: the argent mcp proxy is per-client, so
it IS in the client's workspace. Stamp process.cwd() on every POST
to the tool-server as X-Argent-Project-Root, run invokeTool inside
an AsyncLocalStorage scope, and expose requireProjectRoot() to any
tool that needs it. Missing/malformed headers surface a clear error
from the getter instead of silently defaulting to the daemon's cwd.
- packages/mcp/src/mcp-server.ts: callTool sends the header
- packages/tool-server/src/request-context.ts: ALS module
- packages/tool-server/src/http.ts: extract header, wrap invokeTool
- packages/tool-server/src/tools/flows/flow-utils.ts: requireProjectRoot
- packages/tool-server/test/http-project-root.test.ts: integration tests
covering present/missing/empty/malformed header, unicode roundtrip,
non-flow tools bypassing the getter, and concurrent-request isolation
* refactor: profiler tools resolve project root via requireProjectRoot
Drop the project_root input parameter from react-profiler-analyze
and react-profiler-component-source. Both now pull the root from
the AsyncLocalStorage context stamped by the argent mcp proxy,
matching flow-utils.
Second caller for requireProjectRoot so the mechanism is exercised
beyond flows. If the ALS approach turns out flaky, these two tools
break in the same way as the flow tools and fail loudly with the
same clear error message.
Update the profiler skill doc to drop the project_root arg since
agents no longer pass it.
* test: stress-test AsyncLocalStorage projectRoot mechanism
Four comprehensive test files generated by a parallel swarm audit of
the new request-context.ts / requireProjectRoot() approach, covering
every realistic way it could break.
- test/als-propagation.test.ts — 15 tests documenting what Node's
AsyncLocalStorage actually does under every async boundary the
tool-server uses. Split into SAFE baselines (Promise chains,
microtasks, timers, streams, synchronous EventEmitter emits,
concurrent frames) and LATENT TRAPS (listeners fired outside
an active frame, worker_threads, child_process). Serves as the
authoritative reference for "can I read projectRoot here?".
- test/als-nested-invocation.test.ts — 10 tests proving propagation
through registry.invokeTool nesting: two-level, deep (A→B→C→D→E),
mixed sync/async, parallel fan-out, cross-request isolation,
flow-execute style loops, error mid-chain, long chains, explicit
runWithContext re-entry overrides.
- test/als-http-edge-cases.test.ts — HTTP boundary stress tests:
malformed percent-encoding, null-byte injection, header case
variants, duplicate headers, unicode round-trip, abort mid-flight,
relative/Windows/URL-shaped paths.
- test/als-state-contamination.test.ts — adversarial audit that
surfaced two PRE-EXISTING P0 bugs the ALS fix does NOT solve:
* activeFlowName module-level singleton in flow-utils.ts
leaks between concurrent projects on a shared daemon
* profilerPathsCache in react-profiler-session.ts is keyed
only by Metro port, not projectRoot — two projects sharing
port 8081 see each other's cached profiler sessions
Both are documented via it.fails() so vitest passes today; the
day a fix lands the it.fails flips to an alert and the test
becomes a regression guard.
449/449 tool-server tests pass.
* revert: drop AsyncLocalStorage project-root approach
Reverts the ALS-based project root resolution because the approach
isn't working well in practice. Restores the previous getGitRoot
implementation in flow-utils, the project_root input parameter on
the profiler tools, and the skill doc that describes it.
Reverts:
- 3932eb5 test: stress-test AsyncLocalStorage projectRoot mechanism
- 5398cbc refactor: profiler tools resolve project root via requireProjectRoot
- b7ae895 feat: resolve project root via AsyncLocalStorage from mcp proxy header
A follow-up commit adds clearer error handling around the git call
so the agent gets a useful message when the tool-server's cwd isn't
inside a git repo.
* fix: clearer error when getGitRoot fails outside a git repo
Wrap the `git rev-parse --show-toplevel` call in getGitRoot so that
the failure surfaces a useful message to the agent instead of the
raw child_process stderr. The tool-server daemon's cwd is not the
calling project's cwd, so if the daemon happens to be launched
outside any git repository the git command fails with a terse
"fatal: not a git repository" that tells the agent nothing about
what to do next.
The new error reports the daemon's cwd, explains that flows are
anchored to a git root, and points the user at `git init`.
* refactor: require project_root as a flow tool parameter
The tool-server is a singleton daemon — its process.cwd() is never
the calling project's cwd, so flow-utils cannot infer the project
root on its own. Instead of guessing via git or walking the
filesystem, ask the calling agent directly: every flow tool now
takes project_root as a required parameter and the agent supplies
the absolute path it is working in.
This matches the existing pattern used by react-profiler-analyze,
react-profiler-component-source, and gather-workspace-data, all of
which already accept an agent-provided project/workspace path.
Changes:
- flow-utils.ts: drop getGitRoot/execFile; getFlowsDir and
getFlowPath become pure functions that take projectRoot; throw
on non-absolute paths at the boundary.
- flow-start-recording, flow-add-step, flow-add-echo,
flow-finish-recording, flow-read-prerequisite, flow-execute:
add project_root to each zod schema and plumb it through.
- argent-create-flow SKILL.md: document project_root on every
example tool call; note that it must be passed every time.
- flow-tools.test.ts: remove the flow-utils vi.mock (no longer
needed) and pass project_root: tmpDir on every call; add a
regression test rejecting relative project_root.
* prettier
* cleanup skill
* refactor: store project_root on flow-start-recording, not on every tool
The tool-server cannot infer the calling project's working directory
on its own. The agent now provides it once — as project_root on
flow-start-recording, which stores it alongside activeFlowName in
module state. All other flow tools (flow-add-step, flow-add-echo,
flow-finish-recording, flow-execute, flow-read-prerequisite) read
the stored value and require no project_root parameter.
This matches the existing "active flow" pattern: just as flow name
is tracked automatically after flow-start-recording, so is the
project root.
* feat: require project_root on flow-execute and flow-read-prerequisite
These tools are independent entry points that may be called without a
prior recording session (e.g. after daemon restart), so they cannot
rely on module state set by flow-start-recording. Adding project_root
as an explicit parameter mirrors the pattern already used by
flow-start-recording and react-profiler-analyze.
* fix: make requireActiveProjectRoot error message tool-agnostic
The previous message pointed only at flow-start-recording, but the
check is now reached from flow-execute and flow-read-prerequisite as
well.
* refactor: store flows in .argent/flows/ instead of .argent/
Keeps the .argent/ directory clean for other argent-related artifacts
and groups flow files under a dedicated subdirectory.
* style: prettier --write
process.exit() is synchronous and does not flush Node's WriteStream
buffer when stdout is a pipe. For large outputs like the ~61 KB
tool manifest from `-t`, the tail was being truncated, causing
`argent init` to fail with "Unterminated string in JSON" when
parsing the child's output in getAvailableToolIds.
* Reapply "feat: enhance React profiler session handling with device ID support"
This reverts commit 2bf25cb950.
* Reapply "refactor: remove device-related fields from React profiler session"
This reverts commit badf127411.
* Reapply "refactor: unify device ID handling across debugger and profiler tools, make device_id required, not optional"
This reverts commit c6f1fd7578.
* format: run format
* refactor: enforce device ID requirement in JsRuntimeDebugger and ReactProfilerSession blueprints
* feat: add appName and deviceName to React profiler session and related tools
* fix: update React profiler session output to reflect accurate field names for runtime and project details
* skills: enhance skills for Metro debugger and React profiler tools to clarify device_id usage and connection diagnostics
* format: run format
* test: update NetworkInspector integration tests to include mock-device in service resolutions
* feat: enhance error handling in registry and tool server blueprints
* test: add tests for error handling in registry and tool server blueprints
* refactor: streamline exception detail formatting in CDP client
* format: run format
* add `getAllTools` + tests
* add minimap hints
* cleanup - don't use hardcoded filename
* make cli depend on tool server
* cleanup gemini function - will be used as template for further work
* add allowlist logic
* move codex config type to types section
* add package module and exports to tool server
* revert package changes
* make tool server a CLI
* make the tool server start with `start` command
* add tool printing command
* use CLI tools getter inside mcp configs
* remove artifacts
* add help to help menu
* revert artifacts
* remove redundant null getters
* add codex tests
* refactor: rewrite the `argent` main rule
* fix: minor skills and tool description updates
* feat: add "use when" for CI
---------
Co-authored-by: Filip131311 <Filip.kaminski@swmansion.com>
* feat: first implementation integration of ax-service for enhanced accessibility support
* feat: enhance ax-service with automation enablement and daemon management
* feat: export ensureAutomationEnabled function for improved accessibility automation in simulator server
* feat: add new build scripts for native development and update binary files
* feat: enhance describe tool to support native-devtools fallback and introduce DescribeResult interface
* chore: bump version to 0.4.1
* chore: bump argent-private to main; list ax-service in LICENSE
- Point packages/argent-private at current main and set submodule branch.
- Declare packages/native-devtools-ios/bin/ax-service as proprietary alongside simulator-server and dylibs.
Made-with: Cursor
* docs: document ax-service as proprietary across LICENSE and README
- Extend root README and CONTRIBUTING requirements (mcp README syncs from root at build).
- List bin/ax-service in @swmansion/argent and native-devtools-ios LICENSE.
- Assert ax-service is present and executable in publish workflow.
Made-with: Cursor
---------
Co-authored-by: Filip131311 <Filip.kaminski@swmansion.com>
* chore: Update subproject commit for argent-private and refresh native devtools binaries with fixes for ios26
* chore: Update subproject commit for argent-private and refresh native devtools binaries
* fix: prime native devtools during simulator boot (#89)
Wait for full simulator boot and resolve the NativeDevtools service before boot-simulator returns so immediate launches cannot beat the watcher. Add a focused regression test for both fresh boots and already-booted simulators.
Made-with: Cursor
---------
Co-authored-by: filip131311 <159789821+filip131311@users.noreply.github.com>
* switch to NPM private registry for @swmansion/argent publishing
* switch publish workflow, constants, and test to npmjs.org registry
* revert publish.yml — deprecated workflow
* feat: add update notification and update-argent tool
Injects an update note into HTTP responses when a newer version of Argent
is available, forwards the note through the MCP server so agents see it
prepended to tool results, and adds an update-argent tool that agents can
call (with explicit user consent) to apply the pending update.
* test: add edge-case tests for update note injection and update-argent tool
* fix: guard update-argent against double-spawn and no-op calls, use argent binary
* feat: auto-suppress update note for 30min after delivery, add dismiss-update tool
* fix: correct npm package name to @software-mansion/argent
* fix: use semver ordering for version comparison, guard double-start, handle response stream errors
* feat: add update notification and update-argent tool
Injects an update note into HTTP responses when a newer version of Argent
is available, forwards the note through the MCP server so agents see it
prepended to tool results, and adds an update-argent tool that agents can
call (with explicit user consent) to apply the pending update.
* test: add edge-case tests for update note injection and update-argent tool
* fix: guard update-argent against double-spawn and no-op calls, use argent binary
* feat: auto-suppress update note for 30min after delivery, add dismiss-update tool
* fix: correct npm package name to @software-mansion/argent
* fix: use semver ordering for version comparison, guard double-start, handle response stream errors
* fix unresolved conflicts
* add yarn to gitignore
* cleanup typecasting
* isolate update note builder
* remove unused import
* fix package name
* change update rate to once per day
* add note against using permanent storage mediums
* update tool descriptions
* fix tests
* fix: move describe to native devtools
Replace the AX describe path with an app-scoped native hierarchy adapter so coordinate discovery stays on the supported injection flow.
Also add safer targeting and restart guidance to reduce ambiguous Home and cold-start recovery paths.
Made-with: Cursor
* init
* update sim server
* Delete NATIVE_DEVTOOLS_DESCRIBE_MIGRATION_NOTE.md
* feat: add update notification and update-argent tool
Injects an update note into HTTP responses when a newer version of Argent
is available, forwards the note through the MCP server so agents see it
prepended to tool results, and adds an update-argent tool that agents can
call (with explicit user consent) to apply the pending update.
* test: add edge-case tests for update note injection and update-argent tool
* fix: guard update-argent against double-spawn and no-op calls, use argent binary
* feat: auto-suppress update note for 30min after delivery, add dismiss-update tool
* fix: correct npm package name to @software-mansion/argent
* fix: use semver ordering for version comparison, guard double-start, handle response stream errors