outsrc 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/config.example.toml +46 -0
- package/dist/adapters.d.ts +110 -0
- package/dist/adapters.js +246 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +302 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +87 -0
- package/dist/engines.d.ts +44 -0
- package/dist/engines.js +69 -0
- package/dist/fs-home.d.ts +7 -0
- package/dist/fs-home.js +28 -0
- package/dist/git.d.ts +9 -0
- package/dist/git.js +27 -0
- package/dist/init.d.ts +54 -0
- package/dist/init.js +322 -0
- package/dist/job.d.ts +19 -0
- package/dist/job.js +112 -0
- package/dist/limits.d.ts +13 -0
- package/dist/limits.js +10 -0
- package/dist/mailbox.d.ts +122 -0
- package/dist/mailbox.js +503 -0
- package/dist/migrate.d.ts +11 -0
- package/dist/migrate.js +122 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +36 -0
- package/dist/permissions.d.ts +2 -0
- package/dist/permissions.js +29 -0
- package/dist/plugin-contract.d.ts +12 -0
- package/dist/plugin-contract.js +79 -0
- package/dist/plugins.d.ts +44 -0
- package/dist/plugins.js +215 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +20 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +127 -0
- package/dist/settings.d.ts +247 -0
- package/dist/settings.js +214 -0
- package/dist/state.d.ts +79 -0
- package/dist/state.js +61 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.js +35 -0
- package/dist/workspace.d.ts +18 -0
- package/dist/workspace.js +66 -0
- package/dist/wrapper.d.ts +2 -0
- package/dist/wrapper.js +246 -0
- package/engines.lock.json +14 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Proticom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# outsrc
|
|
2
|
+
|
|
3
|
+
Good work takes a team. outsrc lets a Grok Bot hand coding tasks to Claude Code, Codex or Grok Build on your computer. Each task runs in its own git worktree, and the result comes back to an inbox.
|
|
4
|
+
|
|
5
|
+
Site: https://outsrc.ing
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add the Outsrc bot from Bot Exchange. The listing is not live yet. Once installed, the bot walks you through setup on your computer.
|
|
10
|
+
|
|
11
|
+
This repository is the open-source harness the bot runs. You only need it to build your own bot, contribute, or run outsrc by hand.
|
|
12
|
+
|
|
13
|
+
## Security
|
|
14
|
+
|
|
15
|
+
outsrc is not a sandbox. Agents run as you, on your computer, in the repositories you allow. Review their work before you ship it. Details: https://outsrc.ing/security.md
|
|
16
|
+
|
|
17
|
+
To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
18
|
+
|
|
19
|
+
### What outsrc adds over calling the CLIs directly
|
|
20
|
+
|
|
21
|
+
- **Only the repositories you chose**, each job in its own git worktree and branch. Your checkout stays untouched.
|
|
22
|
+
- **No inherited secrets.** Jobs get only `PATH`, `HOME`, `USER`, `LANG`, `TMPDIR` and `TERM` from your environment.
|
|
23
|
+
- **Read-only reviews.** Reviews never get the flags that skip approvals, and plugin reviews never get `--write`.
|
|
24
|
+
- **Reviewing a branch does not run the branch.** Hooks and MCP servers in a branch's `.claude/`, `.codex/`, `.grok/`, `.cursor/` or `.mcp.json` run commands as soon as a CLI opens the folder. For reviews, outsrc keeps those files out of the worktree (git still shows them in the diff), Claude loads only your own settings and no MCP servers, and repository `setup` commands do not run. Tested with a branch that tried each route: called directly, Claude ran its hooks and MCP server, and Codex ran its MCP server when the repository was trusted. Through outsrc, none ran.
|
|
25
|
+
- **Prompts stay text.** outsrc passes `--` before the prompt, so a message starting with `--` cannot set an option.
|
|
26
|
+
- **No chains.** Jobs run with `OUTSRC_JOB=1`, and an outsrc server started inside a job refuses `send`.
|
|
27
|
+
- **Separate callers.** Each caller sees only its own threads.
|
|
28
|
+
- **Private state.** `~/.outsrc` is created readable by you only.
|
|
29
|
+
- **Bounded jobs.** By default at most 4 jobs at once and 2 hours per run; the owner can change both (see Settings). `stop` signals a process only if it is still the job outsrc started.
|
|
30
|
+
- **Pinned vendor engines**, checked daily for upstream changes.
|
|
31
|
+
|
|
32
|
+
## Running the harness yourself
|
|
33
|
+
|
|
34
|
+
It needs Node 22.12 or later (22.x, 24.x or 26+), git, and at least one of the `claude`, `codex` or `grok` CLIs, logged in.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g outsrc
|
|
38
|
+
outsrc init
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Install it globally rather than running it through `npx`: agents launch the installed copy, and the npx cache moves. Upgrade with `npm install -g outsrc@latest`.
|
|
42
|
+
|
|
43
|
+
`outsrc init` is safe to rerun and keeps existing settings. It:
|
|
44
|
+
|
|
45
|
+
1. Checks Node and git, finds the installed CLIs and checks each is logged in.
|
|
46
|
+
2. Downloads the pinned Codex and Grok plugin engines into `~/.outsrc/engines/`. Claude Code is not needed for this. `--no-plugins` calls the CLIs directly instead.
|
|
47
|
+
3. Asks which repositories agents may work in. Repositories are added only by running `outsrc` on this computer; MCP callers cannot add them.
|
|
48
|
+
4. Asks for the limits: how many jobs may work at once, and how long a run may take (see Settings).
|
|
49
|
+
5. Writes `~/.outsrc/config.toml` and reports on each target.
|
|
50
|
+
6. Offers to register outsrc as an MCP server with each installed CLI.
|
|
51
|
+
|
|
52
|
+
For agents and scripts, `outsrc init --json --repo <path> [--repo <path>] [--local claude,codex,grok] [--max-jobs <n|unlimited>] [--max-run-minutes <n|unlimited>]` prints one JSON event per line and exits with code 3 when a person has to act, such as logging in to a CLI. Rerun it after that step. Without `--local`, nothing is registered. A `settings` event lists the limits, repositories and targets so the agent can review them with the owner.
|
|
53
|
+
|
|
54
|
+
`outsrc doctor` rechecks the configuration. `OUTSRC_HOME` moves the state directory away from `~/.outsrc`, which outsrc keeps readable by you only.
|
|
55
|
+
|
|
56
|
+
## Settings
|
|
57
|
+
|
|
58
|
+
`outsrc config` lists every setting with its current value, its default and what it means. `outsrc config set <key> <value>` changes one, and `outsrc config unset <key>` restores the default or removes an entry. Each change is checked against the whole configuration before the file is written, the previous file is kept as `config.toml.bak`, and running MCP servers pick it up on their next call. MCP callers can read settings with the `settings` tool but cannot change them: only someone who can run `outsrc` on this computer can.
|
|
59
|
+
|
|
60
|
+
| Key | Values | Default |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `limits.max_jobs` | a positive whole number, or `unlimited` | 4 |
|
|
63
|
+
| `limits.max_run_minutes` | a positive whole number, or `unlimited` | 120 |
|
|
64
|
+
| `repos.<alias>` | `{"path": "/absolute/path"}` to add; `unset` to remove | |
|
|
65
|
+
| `repos.<alias>.setup`, `.auto_commit`, `.retention_days` | see Repository options | none, `false`, none |
|
|
66
|
+
| `targets.<name>` | a JSON object, such as `{"adapter": "claude", "command": "claude"}` | |
|
|
67
|
+
| `targets.<name>.permissions` | `auto` or `ask` | `auto` |
|
|
68
|
+
| `targets.<name>.effort.default`, `.effort.allowed` | a value, and a comma list | |
|
|
69
|
+
| `targets.<name>.models.default`, `.models.allowed` | a value, and a comma list | the CLI's choice |
|
|
70
|
+
| `targets.<name>.description`, `.cost_note`, `.command`, `.args`, `.adapter` | see Targets | |
|
|
71
|
+
|
|
72
|
+
Values are read as JSON when they parse (`true`, `14`, `[["npm","ci"]]`) and as text otherwise. Aliases may contain dots: `outsrc config set repos.outsrc.ing.auto_commit true` works.
|
|
73
|
+
|
|
74
|
+
Unlimited is allowed, and `outsrc config` prints what it risks. With no job cap, a looping or confused bot can start many agents at once, slowing the computer and running up usage and rate limits. With no time limit, a stuck agent (waiting for input, a test runner in watch mode, a loop) runs until someone stops it.
|
|
75
|
+
|
|
76
|
+
## Callers
|
|
77
|
+
|
|
78
|
+
Each client connects as a named caller: `outsrc-mcp --caller grok`, or `--caller <id>` on CLI commands. A caller sees and controls only its own threads. Another caller's thread answers exactly like one that does not exist.
|
|
79
|
+
|
|
80
|
+
Every job runs with `OUTSRC_JOB=1` in its environment. An outsrc server started inside a job refuses `send`, so a delegated agent cannot hand work on to another agent. Only the caller that started a job can send more work.
|
|
81
|
+
|
|
82
|
+
## How a conversation works
|
|
83
|
+
|
|
84
|
+
1. Call `list_repos` and `list_targets`.
|
|
85
|
+
2. Call `send` with `repo`, `target` and `message`. Optional: `model`, `effort`, `kind` (`task`, `review` or `adversarial_review`), `ref` (the branch, tag or commit to start from) and `base` (what a review compares against).
|
|
86
|
+
3. Poll `inbox` at its `retry_after_seconds`. The delay grows from 30 seconds to 5 minutes as a run ages, and working runs include recent log lines.
|
|
87
|
+
4. If the status is `needs_input`, the agent stopped with a question. Answer with `send({thread_id, message})`. outsrc resumes the same vendor session in the same worktree. Finished threads accept follow-ups the same way.
|
|
88
|
+
|
|
89
|
+
Pass a stable `request_id` when retrying a `send`. The same request returns the original thread and run; the same ID with different content is an error.
|
|
90
|
+
|
|
91
|
+
## Tools
|
|
92
|
+
|
|
93
|
+
The MCP tools and the CLI commands return the same JSON.
|
|
94
|
+
|
|
95
|
+
| Tool | What it does |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `list_repos` | Repositories agents may work in |
|
|
98
|
+
| `list_targets` | Available agents, their models and effort levels, and the owner's cost notes |
|
|
99
|
+
| `send` | Start a task, or continue a finished or waiting thread |
|
|
100
|
+
| `inbox` | A thread's status, question, or final answer with findings |
|
|
101
|
+
| `threads` | This caller's threads and their status |
|
|
102
|
+
| `settings` | Every setting with its value, default and meaning (read-only) |
|
|
103
|
+
| `history` | Every run in a thread, with its message and result |
|
|
104
|
+
| `log` | A slice of a run's log |
|
|
105
|
+
| `diff` | Changes since the thread started, committed or not (256 KiB by default) |
|
|
106
|
+
| `stop` | Cancel a running thread |
|
|
107
|
+
| `discard` | Delete a finished thread's worktree and branch, keeping its results |
|
|
108
|
+
|
|
109
|
+
CLI only: `outsrc config set` and `unset` change settings, `outsrc prune` deletes finished worktrees past their retention period, `outsrc migrate` imports threads from earlier versions, and `outsrc plugins` checks the plugin engines.
|
|
110
|
+
|
|
111
|
+
## Repository options
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
[[repos]]
|
|
115
|
+
alias = "example"
|
|
116
|
+
path = "/absolute/path/example"
|
|
117
|
+
setup = [["npm", "ci"]] # runs once in a new task worktree, before the first run
|
|
118
|
+
auto_commit = true # commit a successful task's changes
|
|
119
|
+
retention_days = 30 # let prune remove finished worktrees after 30 days
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
All three options are optional. A failed setup fails the run. outsrc does not copy `.env` files or other untracked files into worktrees. Running and waiting threads are never pruned, and saved results outlive their worktrees.
|
|
123
|
+
|
|
124
|
+
## Targets
|
|
125
|
+
|
|
126
|
+
A target is one agent outsrc can start. `list_targets` shows what is configured, and `config.example.toml` has a starting point.
|
|
127
|
+
|
|
128
|
+
| Adapter | Runs |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `claude` | The Claude Code CLI |
|
|
131
|
+
| `codex`, `grok` | The Codex or Grok Build CLI directly |
|
|
132
|
+
| `codex-plugin`, `grok-plugin` | The vendors' plugin engines (below) |
|
|
133
|
+
| `custom` | Any command that prints outsrc's JSON result |
|
|
134
|
+
|
|
135
|
+
`permissions = "auto"` lets a task edit files and run commands without approval: Claude `--permission-mode bypassPermissions`, Codex `--approve-for-me`, Grok `--always-approve`, or `--write` for a plugin. `permissions = "ask"` leaves those flags off. Reviews always run as `ask`, whatever the target says, and without the branch's agent settings (see Security).
|
|
136
|
+
|
|
137
|
+
A custom target prints one JSON object with `kind` (`completed`, `needs_input` or `failed`), `message`, `sessionId` and `findings`. Its `args` may use `{session_id}` (required to resume), `{worktree}`, `{model}`, `{effort}` and `{schema}`. outsrc appends the prompt as the last argument.
|
|
138
|
+
|
|
139
|
+
Each finding has `priority` (`P0` to `P3`), `title`, `body`, `path` (or null) and `line` (or null).
|
|
140
|
+
|
|
141
|
+
## Vendor plugin engines
|
|
142
|
+
|
|
143
|
+
The `codex-plugin` and `grok-plugin` adapters drive the vendors' own Claude Code plugins, [openai/codex-plugin-cc](https://github.com/openai/codex-plugin-cc) and [xai-org/grok-build-plugin-cc](https://github.com/xai-org/grok-build-plugin-cc), both Apache-2.0. The plugin launches the CLI and supplies the review prompts, output schema and, for Codex, the built-in reviewer. outsrc keeps the mailbox, worktrees, repository list and filtered environment. It runs the engine scripts directly; no Claude Code session is involved.
|
|
144
|
+
|
|
145
|
+
`engines.lock.json` pins each plugin to one git commit that passed the live smoke test, the way a lock file pins dependencies. `init` fetches exactly that commit. An upstream change reaches you only when an outsrc release moves the pin. If the pinned engine is missing, outsrc falls back to a Claude Code install of the same plugin, which is not pinned. A target's `command` can point at any copy of `codex-companion.mjs` or `grok-bridge.mjs`.
|
|
146
|
+
|
|
147
|
+
| Request | Plugin command |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `task` | Codex `task`, Grok `run`, with `--write` when permissions are `auto` |
|
|
150
|
+
| follow-up | the same command with `--resume-last` |
|
|
151
|
+
| `review` with `base` | the plugin's diff review against `base` |
|
|
152
|
+
| `adversarial_review` with `base` | Codex `adversarial-review` or Grok `critique`, with the message as the focus |
|
|
153
|
+
| review without `base` | a read-only task with review instructions |
|
|
154
|
+
| `stop` | the plugin's `cancel` or `stop`, then the job's process group |
|
|
155
|
+
|
|
156
|
+
Each thread has its own plugin data directory, and after every Codex run outsrc stops the app-server that the plugin leaves running.
|
|
157
|
+
|
|
158
|
+
Grok and Docker Desktop on macOS: Grok's read-only sandbox refuses to start when `/var/run/docker.sock` is a symlink, which Docker Desktop creates by default. In Docker Desktop, turn off Settings → Advanced → "Allow the default Docker socket to be used", then run `sudo rm /var/run/docker.sock` in a terminal. The `docker` CLI keeps working. This affects Grok reviews, not Grok write tasks.
|
|
159
|
+
|
|
160
|
+
### When a plugin changes
|
|
161
|
+
|
|
162
|
+
The plugin scripts are the vendors' internal CLIs, not a documented API. outsrc checks them three ways:
|
|
163
|
+
|
|
164
|
+
- `outsrc plugins` checks the installed engines without running a model: the subcommands and flags outsrc uses, the environment names, the Codex shutdown hook, and a JSON status call. Each engine is `ok`, `unverified` (compatible, but not the pinned version) or `broken`.
|
|
165
|
+
- `doctor` and `list_targets` include the same result, so a caller sees a warning too.
|
|
166
|
+
- `.github/workflows/plugin-drift.yml` runs the check daily against both upstream repositories and opens an issue when either drifts.
|
|
167
|
+
|
|
168
|
+
To move a pin, run `outsrc engines pin <codex-plugin|grok-plugin> <commit|branch|tag>`. It updates `engines.lock.json` only if the contract check passes. Then run `npm run smoke:plugins` and keep the change only if that passes too.
|
|
169
|
+
|
|
170
|
+
## Limits
|
|
171
|
+
|
|
172
|
+
outsrc checks that thread and run paths stay inside its state directory, and it signals a process only if it is still the job outsrc started. By default at most 4 threads work at once and a run is stopped after 2 hours (both settings). A run log stops growing at 1 MiB, and `log` returns at most 8 KiB.
|
|
173
|
+
|
|
174
|
+
None of this confines a task. A worktree is not a sandbox, and the filtered environment is not a credential boundary. The prompt asks agents not to push or open pull requests, but nothing enforces that.
|
|
175
|
+
|
|
176
|
+
A task trusts the code it starts from: a task on someone else's branch (`ref`) runs that branch's code and settings with the task's permissions, so review such a branch first. A reviewed branch's `CLAUDE.md` or `AGENTS.md` still reaches the reviewer and can try to steer its answer, though the reviewer cannot edit or run anything. Grok skips a project's settings only while the folder is untrusted, so do not mark your home folder or `~/.outsrc` as trusted in Grok.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
From a checkout: `npm install`, `npm run build`, then `npm link` to put the checkout's `outsrc` on your PATH.
|
|
181
|
+
|
|
182
|
+
`npm run verify` builds, type-checks, runs the tests, then repeats the MCP workflow against the built server. `npm test` runs the tests alone. Tests use real git worktrees and child processes with a fixture agent.
|
|
183
|
+
|
|
184
|
+
Two live checks use your own provider accounts and can cost money: `npm run smoke:native` runs a question and a follow-up through each CLI target, and `npm run smoke:plugins` does the same plus an adversarial review through each plugin engine.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copy to ~/.outsrc/config.toml (or let `outsrc init` write one).
|
|
2
|
+
# Paths must be absolute git checkouts on THIS machine. Agents cannot add repos via tools.
|
|
3
|
+
|
|
4
|
+
[[repos]]
|
|
5
|
+
alias = "example"
|
|
6
|
+
path = "/absolute/path/to/your/repo"
|
|
7
|
+
# Optional setup commands run once in a new worktree, before the first run.
|
|
8
|
+
# setup = [["npm", "ci"]]
|
|
9
|
+
# auto_commit = true
|
|
10
|
+
# retention_days = 30
|
|
11
|
+
|
|
12
|
+
# Drives openai/codex-plugin-cc, resolved from the pin in engines.lock.json (or Claude Code's copy).
|
|
13
|
+
# Set command to codex-companion.mjs in a clone of that repository when neither is present.
|
|
14
|
+
# adapter = "codex" calls the Codex CLI directly instead.
|
|
15
|
+
[targets.codex]
|
|
16
|
+
adapter = "codex-plugin"
|
|
17
|
+
permissions = "auto"
|
|
18
|
+
description = "Coding tasks and code review through the Codex plugin, including its built-in and adversarial reviewers."
|
|
19
|
+
cost_note = "Uses this machine's Codex account; pricing depends on that account and selected model."
|
|
20
|
+
|
|
21
|
+
[targets.codex.models]
|
|
22
|
+
default = "gpt-5.6"
|
|
23
|
+
allowed = ["gpt-5.6"]
|
|
24
|
+
|
|
25
|
+
[targets.codex.effort]
|
|
26
|
+
default = "medium"
|
|
27
|
+
allowed = ["none", "minimal", "low", "medium", "high", "xhigh"]
|
|
28
|
+
|
|
29
|
+
[targets.claude]
|
|
30
|
+
adapter = "claude"
|
|
31
|
+
permissions = "auto"
|
|
32
|
+
command = "claude"
|
|
33
|
+
args = []
|
|
34
|
+
description = "Coding tasks and review through the locally installed Claude Code CLI."
|
|
35
|
+
cost_note = "Uses this machine's Claude account. Configure model choices for your plan."
|
|
36
|
+
|
|
37
|
+
[targets.claude.effort]
|
|
38
|
+
default = "medium"
|
|
39
|
+
allowed = ["low", "medium", "high", "xhigh", "max"]
|
|
40
|
+
|
|
41
|
+
# Drives xai-org/grok-build-plugin-cc. adapter = "grok" calls the Grok Build CLI directly instead.
|
|
42
|
+
[targets.grok]
|
|
43
|
+
adapter = "grok-plugin"
|
|
44
|
+
permissions = "auto"
|
|
45
|
+
description = "Coding tasks and critique through the Grok Build plugin."
|
|
46
|
+
cost_note = "Uses this machine's Grok account. Configure model choices for your plan."
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type PluginAdapter } from "./plugins.js";
|
|
3
|
+
import type { TaskKind } from "./types.js";
|
|
4
|
+
export type Adapter = "claude" | "codex" | "grok" | "custom" | PluginAdapter;
|
|
5
|
+
export type AdapterTarget = {
|
|
6
|
+
adapter: Adapter;
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
};
|
|
10
|
+
export type InvocationOptions = {
|
|
11
|
+
target: AdapterTarget;
|
|
12
|
+
worktree: string;
|
|
13
|
+
prompt: string;
|
|
14
|
+
model: string | null;
|
|
15
|
+
effort: string | null;
|
|
16
|
+
sessionId: string | null;
|
|
17
|
+
schemaPath: string;
|
|
18
|
+
permissions: "auto" | "ask";
|
|
19
|
+
kind: TaskKind;
|
|
20
|
+
base: string | null;
|
|
21
|
+
dataDir: string;
|
|
22
|
+
threadId: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const FindingSchema: z.ZodObject<{
|
|
25
|
+
priority: z.ZodEnum<{
|
|
26
|
+
P0: "P0";
|
|
27
|
+
P1: "P1";
|
|
28
|
+
P2: "P2";
|
|
29
|
+
P3: "P3";
|
|
30
|
+
}>;
|
|
31
|
+
title: z.ZodString;
|
|
32
|
+
body: z.ZodString;
|
|
33
|
+
path: z.ZodNullable<z.ZodString>;
|
|
34
|
+
line: z.ZodNullable<z.ZodNumber>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export type Finding = z.infer<typeof FindingSchema>;
|
|
37
|
+
declare const ResultSchema: z.ZodObject<{
|
|
38
|
+
kind: z.ZodEnum<{
|
|
39
|
+
completed: "completed";
|
|
40
|
+
failed: "failed";
|
|
41
|
+
needs_input: "needs_input";
|
|
42
|
+
}>;
|
|
43
|
+
message: z.ZodString;
|
|
44
|
+
findings: z.ZodArray<z.ZodObject<{
|
|
45
|
+
priority: z.ZodEnum<{
|
|
46
|
+
P0: "P0";
|
|
47
|
+
P1: "P1";
|
|
48
|
+
P2: "P2";
|
|
49
|
+
P3: "P3";
|
|
50
|
+
}>;
|
|
51
|
+
title: z.ZodString;
|
|
52
|
+
body: z.ZodString;
|
|
53
|
+
path: z.ZodNullable<z.ZodString>;
|
|
54
|
+
line: z.ZodNullable<z.ZodNumber>;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
export type AgentOutput = z.infer<typeof ResultSchema> & {
|
|
58
|
+
sessionId: string | null;
|
|
59
|
+
};
|
|
60
|
+
export declare const AGENT_OUTPUT_SCHEMA: {
|
|
61
|
+
type: string;
|
|
62
|
+
properties: {
|
|
63
|
+
kind: {
|
|
64
|
+
type: string;
|
|
65
|
+
enum: string[];
|
|
66
|
+
};
|
|
67
|
+
message: {
|
|
68
|
+
type: string;
|
|
69
|
+
};
|
|
70
|
+
findings: {
|
|
71
|
+
type: string;
|
|
72
|
+
items: {
|
|
73
|
+
type: string;
|
|
74
|
+
properties: {
|
|
75
|
+
priority: {
|
|
76
|
+
type: string;
|
|
77
|
+
enum: string[];
|
|
78
|
+
};
|
|
79
|
+
title: {
|
|
80
|
+
type: string;
|
|
81
|
+
};
|
|
82
|
+
body: {
|
|
83
|
+
type: string;
|
|
84
|
+
};
|
|
85
|
+
path: {
|
|
86
|
+
type: string[];
|
|
87
|
+
};
|
|
88
|
+
line: {
|
|
89
|
+
type: string[];
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
required: string[];
|
|
93
|
+
additionalProperties: boolean;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
required: string[];
|
|
98
|
+
additionalProperties: boolean;
|
|
99
|
+
};
|
|
100
|
+
export declare function buildInvocation(options: InvocationOptions): {
|
|
101
|
+
command: string;
|
|
102
|
+
args: string[];
|
|
103
|
+
env?: Record<string, string>;
|
|
104
|
+
};
|
|
105
|
+
export declare function parseAgentOutput(input: {
|
|
106
|
+
adapter: Adapter;
|
|
107
|
+
stdout: string;
|
|
108
|
+
exitCode: number | null;
|
|
109
|
+
}): AgentOutput;
|
|
110
|
+
export {};
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { permissionFlags } from "./permissions.js";
|
|
3
|
+
import { isPluginAdapter, parsePluginOutput, pluginInvocation } from "./plugins.js";
|
|
4
|
+
export const FindingSchema = z.object({
|
|
5
|
+
priority: z.enum(["P0", "P1", "P2", "P3"]),
|
|
6
|
+
title: z.string().min(1),
|
|
7
|
+
body: z.string().min(1),
|
|
8
|
+
path: z.string().nullable(),
|
|
9
|
+
line: z.number().int().positive().nullable(),
|
|
10
|
+
});
|
|
11
|
+
const ResultSchema = z.object({
|
|
12
|
+
kind: z.enum(["completed", "needs_input", "failed"]),
|
|
13
|
+
message: z.string().trim().min(1),
|
|
14
|
+
findings: z.array(FindingSchema),
|
|
15
|
+
});
|
|
16
|
+
export const AGENT_OUTPUT_SCHEMA = {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
kind: { type: "string", enum: ["completed", "needs_input"] },
|
|
20
|
+
message: { type: "string" },
|
|
21
|
+
findings: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
priority: { type: "string", enum: ["P0", "P1", "P2", "P3"] },
|
|
27
|
+
title: { type: "string" },
|
|
28
|
+
body: { type: "string" },
|
|
29
|
+
path: { type: ["string", "null"] },
|
|
30
|
+
line: { type: ["integer", "null"] },
|
|
31
|
+
},
|
|
32
|
+
required: ["priority", "title", "body", "path", "line"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["kind", "message", "findings"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
};
|
|
40
|
+
function substitute(args, options) {
|
|
41
|
+
const variables = {
|
|
42
|
+
worktree: options.worktree,
|
|
43
|
+
model: options.model ?? "",
|
|
44
|
+
effort: options.effort ?? "",
|
|
45
|
+
session_id: options.sessionId ?? "",
|
|
46
|
+
schema: options.schemaPath,
|
|
47
|
+
};
|
|
48
|
+
return args.map((arg) => {
|
|
49
|
+
let value = arg;
|
|
50
|
+
for (const [name, replacement] of Object.entries(variables)) {
|
|
51
|
+
value = value.replaceAll(`{${name}}`, replacement);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function nativeArgs(adapter, args) {
|
|
57
|
+
const values = new Set([
|
|
58
|
+
"--model", "-m", "--effort", "--reasoning-effort", "--output-format",
|
|
59
|
+
"--json-schema", "--output-schema", "--resume", "-r",
|
|
60
|
+
]);
|
|
61
|
+
const result = [];
|
|
62
|
+
for (let index = 0; index < args.length; index++) {
|
|
63
|
+
const arg = args[index];
|
|
64
|
+
if (arg === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
if (values.has(arg)) {
|
|
67
|
+
index++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if ([...values].some((flag) => arg.startsWith(`${flag}=`)))
|
|
71
|
+
continue;
|
|
72
|
+
if (adapter === "codex") {
|
|
73
|
+
if (index === 0 && arg === "exec")
|
|
74
|
+
continue;
|
|
75
|
+
if (arg === "--json")
|
|
76
|
+
continue;
|
|
77
|
+
if ((arg === "-c" || arg === "--config") &&
|
|
78
|
+
/^(model_reasoning_effort|model_service_tier)=/.test(args[index + 1] ?? "")) {
|
|
79
|
+
index++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (arg === "-p" || arg === "--print" || arg === "--single") {
|
|
84
|
+
if (adapter === "grok" && args[index + 1] && !args[index + 1]?.startsWith("-"))
|
|
85
|
+
index++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
result.push(arg);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
export function buildInvocation(options) {
|
|
93
|
+
const { target, model, effort, sessionId, prompt } = options;
|
|
94
|
+
// Review and adversarial_review are always read-only, whatever the target's permissions setting.
|
|
95
|
+
const effectivePermissions = options.kind === "task" ? options.permissions : "ask";
|
|
96
|
+
if (isPluginAdapter(target.adapter)) {
|
|
97
|
+
return pluginInvocation(target.adapter, {
|
|
98
|
+
script: target.command, worktree: options.worktree, prompt, model, effort, sessionId,
|
|
99
|
+
kind: options.kind, base: options.base, permissions: effectivePermissions,
|
|
100
|
+
dataDir: options.dataDir, threadId: options.threadId,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const adapter = target.adapter;
|
|
104
|
+
const configured = substitute(target.args, options);
|
|
105
|
+
if (adapter === "custom") {
|
|
106
|
+
if (sessionId && !target.args.some((arg) => arg.includes("{session_id}"))) {
|
|
107
|
+
throw new Error("custom adapter cannot resume without a {session_id} argument");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
command: target.command,
|
|
111
|
+
args: [...configured, ...permissionFlags(target.command, configured, effectivePermissions), prompt],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const extra = nativeArgs(adapter, configured);
|
|
115
|
+
const permissions = permissionFlags(target.command, extra, effectivePermissions);
|
|
116
|
+
const modelArgs = model === null ? [] : ["--model", model];
|
|
117
|
+
switch (adapter) {
|
|
118
|
+
case "claude":
|
|
119
|
+
return {
|
|
120
|
+
command: target.command,
|
|
121
|
+
args: [
|
|
122
|
+
...extra, "-p", "--output-format", "json", "--json-schema", JSON.stringify(AGENT_OUTPUT_SCHEMA),
|
|
123
|
+
...modelArgs, ...(effort === null ? [] : ["--effort", effort]),
|
|
124
|
+
...(sessionId === null ? [] : ["--resume", sessionId]), ...permissions,
|
|
125
|
+
// A review reads someone else's branch: load only the user's own settings, and no MCP servers.
|
|
126
|
+
...(options.kind === "task" ? [] : ["--setting-sources", "user", "--strict-mcp-config"]), "--", prompt,
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
case "codex": {
|
|
130
|
+
const configuration = [
|
|
131
|
+
"-c", "model_service_tier=fast",
|
|
132
|
+
...(effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`]),
|
|
133
|
+
];
|
|
134
|
+
const output = ["--json", "--output-schema", options.schemaPath, ...modelArgs];
|
|
135
|
+
return {
|
|
136
|
+
command: target.command,
|
|
137
|
+
args: [
|
|
138
|
+
"exec", ...extra, ...configuration, ...permissions,
|
|
139
|
+
...(sessionId === null ? output : ["resume", ...output, sessionId]),
|
|
140
|
+
"--", prompt,
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
case "grok":
|
|
145
|
+
return {
|
|
146
|
+
command: target.command,
|
|
147
|
+
args: [
|
|
148
|
+
...extra, "--output-format", "json", "--json-schema", JSON.stringify(AGENT_OUTPUT_SCHEMA),
|
|
149
|
+
...modelArgs, ...(effort === null ? [] : ["--reasoning-effort", effort]),
|
|
150
|
+
...(sessionId === null ? [] : ["--resume", sessionId]), ...permissions,
|
|
151
|
+
...(prompt.startsWith("-") ? [`--single=${prompt}`] : ["-p", prompt]),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
default: {
|
|
155
|
+
const exhaustive = adapter;
|
|
156
|
+
throw new Error(`unknown adapter: ${exhaustive}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const EnvelopeSchema = z.object({
|
|
161
|
+
type: z.string().optional(),
|
|
162
|
+
session_id: z.string().min(1).optional(),
|
|
163
|
+
sessionId: z.string().min(1).optional(),
|
|
164
|
+
structured_output: z.unknown().optional(),
|
|
165
|
+
structuredOutput: z.unknown().optional(),
|
|
166
|
+
result: z.string().optional(),
|
|
167
|
+
text: z.string().optional(),
|
|
168
|
+
is_error: z.boolean().optional(),
|
|
169
|
+
errors: z.array(z.string()).optional(),
|
|
170
|
+
message: z.unknown().optional(),
|
|
171
|
+
});
|
|
172
|
+
const CodexEventSchema = z.object({
|
|
173
|
+
type: z.string(),
|
|
174
|
+
thread_id: z.string().min(1).optional(),
|
|
175
|
+
message: z.string().optional(),
|
|
176
|
+
error: z.object({ message: z.string() }).optional(),
|
|
177
|
+
item: z.object({ type: z.string(), text: z.string().optional() }).optional(),
|
|
178
|
+
});
|
|
179
|
+
function failed(message, sessionId = null) {
|
|
180
|
+
return { kind: "failed", message, sessionId, findings: [] };
|
|
181
|
+
}
|
|
182
|
+
export function parseAgentOutput(input) {
|
|
183
|
+
if (isPluginAdapter(input.adapter))
|
|
184
|
+
return parsePluginOutput(input.stdout, input.exitCode);
|
|
185
|
+
let sessionId = null;
|
|
186
|
+
try {
|
|
187
|
+
let value;
|
|
188
|
+
if (input.adapter === "codex") {
|
|
189
|
+
let finalText;
|
|
190
|
+
let complete = false;
|
|
191
|
+
for (const line of input.stdout.trim().split("\n").filter((line) => line.trim())) {
|
|
192
|
+
const event = CodexEventSchema.parse(JSON.parse(line));
|
|
193
|
+
if (event.type === "thread.started")
|
|
194
|
+
sessionId = event.thread_id ?? sessionId;
|
|
195
|
+
if (event.type === "turn.failed" || event.type === "error") {
|
|
196
|
+
return failed(event.error?.message ?? event.message ?? "Codex turn failed", sessionId);
|
|
197
|
+
}
|
|
198
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
199
|
+
finalText = event.item.text;
|
|
200
|
+
}
|
|
201
|
+
if (event.type === "turn.completed")
|
|
202
|
+
complete = true;
|
|
203
|
+
}
|
|
204
|
+
if (!complete || !finalText) {
|
|
205
|
+
const message = input.exitCode === 0 ? "Codex did not return a completed turn" :
|
|
206
|
+
`Agent process exited with ${input.exitCode ?? "a signal"} before completing Codex turn`;
|
|
207
|
+
return failed(message, sessionId);
|
|
208
|
+
}
|
|
209
|
+
value = JSON.parse(finalText);
|
|
210
|
+
}
|
|
211
|
+
else if (input.adapter === "custom") {
|
|
212
|
+
const custom = ResultSchema.extend({ sessionId: z.string().min(1).nullable() }).parse(JSON.parse(input.stdout));
|
|
213
|
+
sessionId = custom.sessionId;
|
|
214
|
+
value = custom;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const raw = JSON.parse(input.stdout);
|
|
218
|
+
const messages = z.array(EnvelopeSchema).safeParse(raw);
|
|
219
|
+
const envelope = messages.success ? messages.data.findLast((message) => message.type === "result") : EnvelopeSchema.parse(raw);
|
|
220
|
+
if (!envelope)
|
|
221
|
+
return failed("Agent output did not include a final result", sessionId);
|
|
222
|
+
sessionId = envelope.session_id ?? envelope.sessionId ?? null;
|
|
223
|
+
if (envelope.is_error || envelope.type === "error") {
|
|
224
|
+
const message = typeof envelope.message === "string" ? envelope.message : "Agent reported an error";
|
|
225
|
+
return failed(envelope.errors?.join("\n") || envelope.result || message, sessionId);
|
|
226
|
+
}
|
|
227
|
+
value = envelope.structured_output ?? envelope.structuredOutput;
|
|
228
|
+
if (value === undefined && input.adapter === "grok" && envelope.text) {
|
|
229
|
+
value = JSON.parse(envelope.text);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (input.exitCode !== 0)
|
|
233
|
+
return failed(`Agent process exited with ${input.exitCode ?? "a signal"}`, sessionId);
|
|
234
|
+
const result = ResultSchema.parse(value);
|
|
235
|
+
if (result.kind === "needs_input" && sessionId === null) {
|
|
236
|
+
return failed("Agent requested input without a resumable session ID", sessionId);
|
|
237
|
+
}
|
|
238
|
+
return { ...result, sessionId };
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
if (input.exitCode !== 0) {
|
|
242
|
+
return failed(`Agent process exited with ${input.exitCode ?? "a signal"} without valid ${input.adapter} output`, sessionId);
|
|
243
|
+
}
|
|
244
|
+
return failed(`Invalid ${input.adapter} agent output`, sessionId);
|
|
245
|
+
}
|
|
246
|
+
}
|
package/dist/cli.d.ts
ADDED