Skip to content

feat(cli): add headless CLI for one-shot agent runs - #76470

Closed
charlesvien wants to merge 5 commits into
masterfrom
posthog-code/headless-cli-mode
Closed

feat(cli): add headless CLI for one-shot agent runs#76470
charlesvien wants to merge 5 commits into
masterfrom
posthog-code/headless-cli-mode

Conversation

@charlesvien

Copy link
Copy Markdown
Member

Ports PostHog/code#3798 into the monorepo (PostHog/code main is frozen after the desktop import; open PRs there are remade here). Commits preserve the original authorship. Opened as a draft to match the source PR's state.

Problem

There's no way to run the agent from a script. The desktop app needs a GUI, and cloud tasks need a sandbox; anyone who wants "prompt in, answer on stdout" (CI checks, shell pipelines, cron jobs) has to shell out to a different tool entirely.

Changes

Adds packages/cli (@posthog/code-cli, bin posthog-code-cli), a headless CLI host that drives the same in-process ACP connection the desktop app uses via Agent from @posthog/agent:

posthog-code-cli "Fix the failing test" --cwd ~/src/myrepo --permission-mode auto
  • Prompt as a positional arg or piped on stdin; assistant text streams to stdout, diagnostics go to stderr, exit code reflects the stop reason (0 end_turn, 2 refusal, 3 token/turn limits, 130 SIGINT).
  • --permission-mode auto (default) or bypassPermissions. Interactive modes are rejected since there's no UI to answer prompts. Tool permissions that still reach the client are auto-approved (preferring allow_once so one-shot runs don't persist allow rules); AskUserQuestion calls are parked with the same no-user-available contract cloud background runs use, so the model restates the question as text and ends its turn.
  • --output json buffers and emits one { text, stopReason, usage, sessionId } document for machine consumption.
  • Auth comes from ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL env vars, falling back to a stored claude login credential.

ClaudeAcpAgent previously hardcoded a debug console logger, ignoring the logger injected through createAcpConnection; it now accepts the injected logger (keeping the console fallback), which lets the CLI route adapter diagnostics off stdout behind --debug. Desktop and cloud behavior is unchanged since they don't pass one.

The new package lands in packages/ rather than the apps/cli slot listed in AGENTS.md because repo settings deny writes there; the AGENTS.md hosts table now points at packages/cli.

How did you test this code?

  • 39 unit tests across arg parsing, the unattended permission policy, and the output sink (pnpm --filter @posthog/code-cli test).
  • Full @posthog/agent suite after the logger change (1625 passed; one pre-existing flake in settings.test.ts that passes in isolation).
  • Live smoke runs against a scratch repo: read-only question, auto-mode file edit applied without prompting, --output json shape, clean stdout (zero stderr bytes without --debug, 15 diagnostic lines with it), and exit codes for bad flags/missing prompt.
  • Opt-in e2e spec (pnpm --filter @posthog/code-cli test:e2e) using the same gateway env contract as packages/agent/e2e.
  • After the port: from products/desktop/, pnpm install --frozen-lockfile against the regenerated lockfile, pnpm typecheck (25/25 clean including the new package), @posthog/code-cli tests (53) and the full @posthog/agent suite (1781/1782, the one failure being the same pre-existing settings.test.ts flake, which passes in isolation).

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

N/A

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

This PR is a port of PostHog/code#3798 (author: haacked) into products/desktop/, done with Claude Code following the repo's /porting-code-prs skill: git am -3 --directory=products/desktop/ over the source patch series, preserving the original commits and authorship. The source lockfile hunk was excluded per the migration contract (the monorepo lockfile carries local security override pins) and regenerated from products/desktop/ with pnpm install --lockfile-only; the regenerated diff is scoped to the new packages/cli importer and lands as a separate chore commit.


Created with PostHog Code

haacked and others added 5 commits August 2, 2026 17:12
Adds packages/cli (@posthog/code-cli, bin posthog-code-cli): run one
agent turn against a local repo from the shell — prompt in, assistant
output on stdout, exit code out. Supports --permission-mode auto
(default) and bypassPermissions, --output text|json, --model,
--system-prompt, and stdin prompts. Auth comes from ANTHROPIC_* env
vars; question tool calls are parked with the same no-user-available
contract as cloud background runs.

ClaudeAcpAgent now accepts an injected logger (falling back to its
console logger), letting hosts route adapter diagnostics off stdout.

Generated-By: PostHog Code
Task-Id: 97a6265f-b609-4775-9949-887aca895991
Review follow-ups: adapter error-level diagnostics now reach stderr
without --debug (except expected teardown noise); SIGINT cancels the
turn and exits 130 deterministically instead of racing the main path;
stdout is flushed before the force-exit timer so piped JSON can't
truncate; commander choices()/argParser replace hand-rolled validation
(and the double error print); session updates and prompt results use
the ACP SDK types instead of unknown casts; reject-only permission
requests resolve to reject_once; bypassPermissions as non-sandboxed
root fails fast with a clear message.

Generated-By: PostHog Code
Task-Id: 97a6265f-b609-4775-9949-887aca895991
The force-exit backstop now arms unconditionally so a stalled stdout
consumer cannot hang the process, and a SIGINT that lands during
session setup exits 130 instead of being reset when the turn activates.

Generated-By: PostHog Code
Task-Id: 97a6265f-b609-4775-9949-887aca895991
Review follow-ups on the headless CLI, plus two fixes in @posthog/agent
that it depends on.

Adapter diagnostics are no longer forwarded to a host logger by default.
Forwarding them routed lines that carry whole payloads (an expanded
slash-command's output, raw tool inputs, subprocess stderr) into desktop's
electron-log file and OTLP transports, and into cloud's task feed on a
session re-init. `Logger.emitLog` calls `onLog` for every level before the
debug gate, so `debug: false` did not hold them back. A new
`forwardAdapterLogs` opt-in carries them, and only the CLI sets it, where
the sink is the operator's own stderr.

A plan approval no longer downgrades the session. The unattended policy
ranks options by kind, but a plan approval's options are session modes and
its only `allow_once` is "manually approve edits", so approving a plan
switched an unattended run into an interactive mode and persisted that into
the target repo's settings. Plan approvals now take array order, which is
the mode the adapter puts first, matching the cloud client.

Also:

- SIGTERM is handled alongside SIGINT (exit 143), so a supervisor no longer
  skips teardown and orphans the claude subprocess.
- EPIPE on stdout is swallowed, so `… | head -1` stops quietly instead of
  crashing with a stack trace mid-teardown.
- A whitespace-only positional prompt is rejected like a piped one.
- The root/bypass guard reads the effective uid, matching `IS_ROOT`.
- Reading a prompt from a stdin that never closes is bounded.
- The `usage` key stays in the JSON document as null when a turn settles
  without token counts, and `stopReason`/`usage` use the SDK's types.
- An unavailable `--model` id now warns instead of being swapped silently.
- `--debug` keeps the stack on an unexpected throw.

The unattended permission policy moves to
`@posthog/agent/unattended-permission-policy`. It is the third copy in the
repo and the only one that ranks by kind; giving it a home is a step toward
cloud and desktop adopting it. `runTurn` is split behind a structural
connection interface so the signal and exit-code paths are testable, and
`ParseResult` is a tagged union so `--help` stops travelling as an error.

Adds a README, drops the stale `apps/cli` knip entry, and separates hosts
from executable packages in AGENTS.md.

Claude-Session: https://claude.ai/code/session_013Gyfq8R69DNpPSV2DBicK5
@charlesvien charlesvien added the feature/desktop Feature Tag: Desktop label Aug 3, 2026
@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature/desktop Feature Tag: Desktop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants