Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ Minimal operating guide for AI coding agents in this repo.
- Use `inferFillText` and `uniqueStrings` from `src/daemon/action-utils.ts`. Do not duplicate.
- Use `evaluateIsPredicate` from `src/daemon/is-predicates.ts` for assertion logic. Do not inline.

## Diagnostics & Errors

- Use `src/utils/diagnostics.ts` as the diagnostics source of truth:
- `withDiagnosticsScope`
- `emitDiagnostic`
- `withDiagnosticTimer`
- `flushDiagnosticsToSessionFile`
- Do not add ad-hoc stderr/file logging in handlers/platform modules when diagnostics helpers can be used.
- Normalize user-facing failures through `src/utils/errors.ts` (`normalizeError`).
- Failure payload contract should include: `code`, `message`, `hint`, `diagnosticId`, `logPath`, `details`.
- When wrapping/rethrowing daemon errors (batch/replay/handler wrappers), preserve `hint`, `diagnosticId`, and `logPath` from inner errors.
- `--debug` is canonical; `--verbose` remains backward-compatible alias.
- Keep redaction centralized in `src/utils/diagnostics.ts`; do not duplicate redaction logic in handlers/CLI.

## Key Files
- CLI parse + formatting: `src/bin.ts`, `src/cli.ts`, `src/utils/args.ts`
- Daemon client transport: `src/daemon-client.ts`
Expand All @@ -56,7 +70,7 @@ Minimal operating guide for AI coding agents in this repo.
- `is` predicate evaluation: `src/daemon/is-predicates.ts`
- Shared action helpers: `src/daemon/action-utils.ts`
- Snapshot shaping + labels: `src/daemon/snapshot-processing.ts`
- Handler context helpers: `src/daemon/context.ts`, `src/daemon/device-ready.ts`, `src/daemon/app-state.ts`
- Handler context helpers: `src/daemon/context.ts`, `src/daemon/device-ready.ts`
- Dispatcher and capability source of truth: `src/core/dispatch.ts`, `src/core/capabilities.ts`
- Platform backends: `src/platforms/ios/*`, `ios-runner/*`, `src/platforms/android/*`

Expand Down Expand Up @@ -93,16 +107,12 @@ Run integration tests when behavior crosses platform boundaries:
- `pnpm test:integration`

## Measurement
- Use `docs/daemon-refactor-impact.md`.
- Track files touched per fix, cycle time, and iOS/Android regressions.

## Local Commands

- Run CLI: `pnpm ad <command>`
- Typecheck: `pnpm typecheck`
- Unit tests: `pnpm test:unit`
- Smoke tests: `pnpm test:smoke`
- Integration tests: `pnpm test:integration`
- For verification commands, use the **Testing** section above.

## Pull Requests
- Before opening PR: ensure no conflict markers and no unmerged paths.
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Flags:
- `--double-tap` use a double-tap gesture per `press`/`click` iteration (cannot be combined with `--hold-ms` or `--jitter-px`)
- `--pause-ms <ms>` delay between `swipe` iterations
- `--pattern one-way|ping-pong` repeat pattern for `swipe`
- `--verbose` for daemon and runner logs
- `--debug` (alias: `--verbose`) for debug diagnostics + daemon/runner logs
- `--json` for structured output
- `--steps <json>` batch: JSON array of steps
- `--steps-file <path>` batch: read step JSON from file
Expand Down Expand Up @@ -290,7 +290,13 @@ Boot diagnostics:
- Reason codes: `IOS_BOOT_TIMEOUT`, `IOS_RUNNER_CONNECT_TIMEOUT`, `ANDROID_BOOT_TIMEOUT`, `ADB_TRANSPORT_UNAVAILABLE`, `CI_RESOURCE_STARVATION_SUSPECTED`, `BOOT_COMMAND_FAILED`, `UNKNOWN`.
- Android boot waits fail fast for permission/tooling issues and do not always collapse into timeout errors.
- Use `agent-device boot --platform ios|android` when starting a new session only if `open` cannot find/connect to an available target.
- Set `AGENT_DEVICE_RETRY_LOGS=1` to print structured retry telemetry (attempt, phase, delay, elapsed/remaining deadline, reason).
- `--debug` captures retry telemetry in diagnostics logs.
- Set `AGENT_DEVICE_RETRY_LOGS=1` to also print retry telemetry directly to stderr (ad-hoc troubleshooting).

Diagnostics files:
- Failed commands persist diagnostics in `~/.agent-device/logs/<session>/<date>/<timestamp>-<diagnosticId>.ndjson`.
- `--debug` persists diagnostics for successful commands too and streams live diagnostic events.
- JSON failures include `error.hint`, `error.diagnosticId`, and `error.logPath`.

## App resolution
- Bundle/package identifiers are accepted directly (e.g., `com.apple.Preferences`).
Expand Down
119 changes: 119 additions & 0 deletions src/__tests__/cli-diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { runCli } from '../cli.ts';
import type { DaemonRequest, DaemonResponse } from '../daemon-client.ts';

class ExitSignal extends Error {
public readonly code: number;

constructor(code: number) {
super(`EXIT_${code}`);
this.code = code;
}
}

type RunResult = {
code: number | null;
stdout: string;
stderr: string;
calls: Omit<DaemonRequest, 'token'>[];
};

async function runCliCapture(
argv: string[],
responder: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse>,
): Promise<RunResult> {
let stdout = '';
let stderr = '';
let code: number | null = null;
const calls: Array<Omit<DaemonRequest, 'token'>> = [];

const originalExit = process.exit;
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const originalStderrWrite = process.stderr.write.bind(process.stderr);

(process as any).exit = ((nextCode?: number) => {
throw new ExitSignal(nextCode ?? 0);
}) as typeof process.exit;
(process.stdout as any).write = ((chunk: unknown) => {
stdout += String(chunk);
return true;
}) as typeof process.stdout.write;
(process.stderr as any).write = ((chunk: unknown) => {
stderr += String(chunk);
return true;
}) as typeof process.stderr.write;

const sendToDaemon = async (req: Omit<DaemonRequest, 'token'>): Promise<DaemonResponse> => {
calls.push(req);
return await responder(req);
};

try {
await runCli(argv, { sendToDaemon });
} catch (error) {
if (error instanceof ExitSignal) code = error.code;
else throw error;
} finally {
process.exit = originalExit;
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
}

return { code, stdout, stderr, calls };
}

test('cli forwards --debug as verbose/debug metadata', async () => {
const result = await runCliCapture(['open', 'settings', '--debug', '--json'], async () => ({
ok: true,
data: { app: 'settings' },
}));
assert.equal(result.code, null);
assert.equal(result.calls.length, 1);
assert.equal(result.calls[0]?.flags?.verbose, true);
assert.equal(result.calls[0]?.meta?.debug, true);
assert.equal(typeof result.calls[0]?.meta?.requestId, 'string');
});

test('cli returns normalized JSON failures with diagnostics fields', async () => {
const result = await runCliCapture(['open', 'settings', '--json'], async () => ({
ok: false,
error: {
code: 'COMMAND_FAILED',
message: 'boom',
hint: 'retry later',
diagnosticId: 'diag-123',
logPath: '/tmp/diag.ndjson',
details: { token: 'secret', safe: 'ok' },
},
}));
assert.equal(result.code, 1);
const payload = JSON.parse(result.stdout);
assert.equal(payload.success, false);
assert.equal(payload.error.code, 'COMMAND_FAILED');
assert.equal(payload.error.hint, 'retry later');
assert.equal(payload.error.diagnosticId, 'diag-123');
assert.equal(payload.error.logPath, '/tmp/diag.ndjson');
assert.equal(payload.error.details.token, '[REDACTED]');
assert.equal(payload.error.details.safe, 'ok');
});

test('cli parse failures include diagnostic references in JSON mode', async () => {
const previousHome = process.env.HOME;
process.env.HOME = '/tmp';
try {
const result = await runCliCapture(['open', '--unknown-flag', '--json'], async () => ({
ok: true,
data: {},
}));
assert.equal(result.code, 1);
assert.equal(result.calls.length, 0);
const payload = JSON.parse(result.stdout);
assert.equal(payload.success, false);
assert.equal(payload.error.code, 'INVALID_ARGS');
assert.equal(typeof payload.error.diagnosticId, 'string');
assert.equal(typeof payload.error.logPath, 'string');
} finally {
process.env.HOME = previousHome;
}
});
Loading
Loading