@apexstack/agent
v0.5.1
Published
PJT Agent — runs PJT workspace tasks on your own computer (Claude Code primary, OpenAI Codex failover).
Maintainers
Readme
PJT Agent
Run your PJT workspace tasks on your own machine.
Claude Code first, OpenAI Codex as automatic failover — with human-in-the-loop decisions and secret redaction built in.
What it is
PJT Agent is a small CLI that connects a PJT workspace to the coding agents already installed on your computer.
You press Run with PJT Agent on a task in the web app. The agent — running on your machine, in your repository, with your credentials — picks the task up, executes it, and reports the result back to the task.
Nothing runs on our servers. The server only holds a queue. Your source code, your API keys and your model credentials never leave your machine.
PJT web app your computer
┌───────────────┐ ┌──────────────────────────┐
│ Task #128 │ long poll │ pjt-agent start │
│ [Run ▶] │◄─────────────┤ └─ claude / codex │
│ │ result │ in ~/code/my-repo │
└───────────────┘─────────────►└──────────────────────────┘Quick start
1 — Install
npm install -g @apexstack/agent2 — Log in with the token from the web app (Workspace → AI Integrations → PJT Agent → Install Agent; the wizard prints the full command)
pjt-agent login --token pjta_xxxxxxxx3 — Start it inside the repository the tasks should run in
cd ~/code/my-backend
pjt-agent startThat's it. Leave it running and dispatch tasks from the web app.
npx @apexstack/agent@latest startRequirements
| | |
|---|---|
| Node.js | 18 or newer |
| Primary runner | Anthropic claude CLI, installed and signed in |
| Failover runner | OpenAI codex CLI, installed and signed in (optional — see below) |
Without codex, a Claude failure ends the task with an explicit failover error instead of silently retrying.
Commands
| Command | What it does |
|---|---|
| pjt-agent login --token <t> | Save token + runner policy to ~/.pjt/agent.json |
| pjt-agent start | Long-poll the queue and execute tasks |
| pjt-agent status | Verify the token and print the active configuration |
| pjt-agent mcp | Run a stdio MCP server exposing workspace context (legacy) |
| pjt-agent logout | Delete the local configuration |
Common options
pjt-agent login --token pjta_xxx --api https://api.pjt.ai \
--runner claude --fallback-runner codex
pjt-agent start --cwd ~/code/my-backend \
--runner claude --fallback-runner none--runner accepts claude or codex; --fallback-runner also accepts none.
How it works
1 pjt-agent start
2 GET /api/v1/agent/tasks/next?wait=20 ← long poll
3 task received → run `claude -p "<prompt>"` in the task's working directory
4 on failure, if eligible → `codex exec --sandbox workspace-write`
5 POST /api/v1/agent/tasks/{id}/complete → status, summary, changed files
6 back to 2Failover rules
Failover exists to survive a broken CLI, not to re-run work. A second runner starts only when all of these hold:
- the primary runner failed, and
- the failure was not a policy refusal or a user cancellation, and
- the git fingerprint of the working tree is byte-for-byte unchanged since the run began.
If Claude modified files or created a commit and then failed, the failure is reported as-is. Nothing is executed twice.
Human-in-the-loop decisions
Some choices are not the agent's to make — a schema migration, a dependency swap, a destructive command. Instead of guessing, the agent can stop and ask.
When the runner emits a fenced pjt-decision block, the agent posts the question to the task, shuts the runner down, and returns to polling. The task moves to WAITING_USER:
```pjt-decision
{
"type": "SINGLE_CHOICE",
"severity": "HIGH",
"question": "The users table needs a new unique index. Apply it now?",
"options": ["Apply in this task", "Defer to a migration PR"]
}
```| Field | Values |
|---|---|
| type | SINGLE_CHOICE (default) · TEXT · CONFIRMATION |
| severity | HIGH (default) · CRITICAL |
| question | required, non-empty |
You answer in the web app. The agent picks the task back up and resumes with the previous context plus your answer, so completed work is not repeated.
Free-form text is never interpreted as a question. Only the exact fenced block counts — otherwise ordinary output would halt runs.
Secret redaction
Agent output routinely contains credentials — an echoed header, a printed env var, a stack trace. Everything sent to the server is masked first, by key name and by value shape:
| Detected by key | Detected by value |
|---|---|
| authorization, api_key, client_secret, password, private_key, cookie, access_token, refresh_token, … | Bearer …, JWTs, pjta_…, glpat-…, ghp_…, sk-…, sk_live_…, xoxb-…, AKIA…, PEM private key blocks |
Values are masked together with their scheme, so Authorization: Bearer eyJ… is redacted whole rather than leaving the token behind.
MCP server
Use the remote HTTP MCP instead. The tool cards under AI Integrations give you a
https://mcp.pjt.aiURL and anAuthorization: Bearerheader — no install, no local process. The stdio server below is kept for offline and restricted environments.
# after `pjt-agent login`
claude mcp add pjt-workspace -- pjt-agent-mcp
# or inject the token directly, without logging in
claude mcp add pjt-workspace \
--env PJT_AGENT_TOKEN=pjta_xxx \
--env PJT_API_URL=https://api.pjt.ai \
-- pjt-agent-mcpSeven read-only tools are exposed:
| Tool | Input |
|---|---|
| list_workspaces | — |
| get_project | projectId (numeric or p_xxx) |
| list_project_tasks | projectId, status? |
| get_task | taskId |
| get_document | documentId |
| list_workspace_documents | workspaceId, limit? (1–50) |
| get_workspace_snapshot | workspaceId |
Authorization is enforced server-side. stdout carries JSON-RPC only — all logging goes to stderr.
Configuration
Resolution order is environment variable → ~/.pjt/agent.json → default.
| Variable | Default |
|---|---|
| PJT_AGENT_TOKEN | from config file |
| PJT_API_URL | https://api.pjt.ai |
| PJT_AGENT_RUNNER | claude |
| PJT_AGENT_FALLBACK_RUNNER | codex |
Security
- The token is stored at
~/.pjt/agent.jsonwith mode0600. - The server keeps only a SHA-256 hash — a lost token cannot be recovered, only reissued.
- If a task's
workingDirdoes not exist locally, the agent falls back to--cwdor the current directory rather than creating paths. - The agent holds no model credentials of its own; it invokes the CLIs you have already authenticated.
Troubleshooting
| Symptom | Cause |
|---|---|
| No output at all, exit code 0 | Version 0.3.0 only — upgrade: npm install -g @apexstack/agent@latest |
| ✗ Not authenticated | Token revoked or expired — reissue under AI Integrations and login again |
| Tasks stay queued | The agent is not running, or it is polling a different account's token — check pjt-agent status |
| Failover never triggers | Expected when the working tree changed before the failure, or on a policy refusal |
Development
git clone [email protected]:pjt-ai/pjt-agent.git
cd pjt-agent
npm ci
npm run build
npm link # registers the global `pjt-agent` command
node --test test/*.test.mjsBefore publishing, verify the tarball through a global install —
npm pack && npm install -g ./<tgz> && pjt-agent --version. Runningnode dist/index.jsdirectly does not exercise the bin symlink.
pjt.ai · © 2026 ApexStack · Apache-2.0
