@tiptask/runner 0.0.0-stage → 0.7.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/README.md +387 -2
- package/dist/cli.js +1477 -0
- package/package.json +57 -4
package/README.md
CHANGED
|
@@ -1,3 +1,388 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tiptask/runner
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TipTask is a self-hosted issue tracker. An AI
|
|
4
|
+
agent in it is a project member: it has a role, permissions, and its own issues.
|
|
5
|
+
|
|
6
|
+
This package runs such agents on your own machine — one, or several at once.
|
|
7
|
+
|
|
8
|
+
- Polls your instance for the agent's queued runs. Runs each one with a coding agent CLI.
|
|
9
|
+
Reports the result.
|
|
10
|
+
- Has presets for Claude Code, Codex, opencode, Antigravity CLI, GitHub Copilot CLI,
|
|
11
|
+
Pi, and Oh My Pi (`omp`). Each preset sets the unattended flags and the session resume.
|
|
12
|
+
- Runs your own `command` instead, if you prefer. The command takes the task on stdin.
|
|
13
|
+
- Answers the agent's chat. Sends the text and the tool calls while the CLI prints them.
|
|
14
|
+
- Sends the messages and the tool calls of a queued run to the issue while the run works.
|
|
15
|
+
- Receives the tasks on a port instead of polling, for an agent whose Connection is
|
|
16
|
+
Endpoint. See [Receiving tasks on a port](#receiving-tasks-on-a-port).
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
**1. Get the agent's key.** In your project, go to Settings, AI agents. Create an agent
|
|
21
|
+
of kind External. The key appears one time only, at creation.
|
|
22
|
+
|
|
23
|
+
**2. Enable MCP** for the project, in Settings, MCP Server. MCP is off by default. The
|
|
24
|
+
agent reads the issue and writes its result through it.
|
|
25
|
+
|
|
26
|
+
**3. Give the coding agent the address of that server.** Claude Code, Oh My Pi, and Pi
|
|
27
|
+
(through `pi-mcp-adapter`) read `.mcp.json` from the folder they run in. For the other
|
|
28
|
+
CLIs, see
|
|
29
|
+
[Coding agent setup](../../docs/runner.md).
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"tiptask": {
|
|
35
|
+
"type": "http",
|
|
36
|
+
"url": "http://localhost:3000/mcp",
|
|
37
|
+
"headers": { "Authorization": "Bearer ${TIPTASK_API_KEY}" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The runner sets `TIPTASK_API_KEY` for the command that it starts. The key thus stays in
|
|
44
|
+
one file.
|
|
45
|
+
|
|
46
|
+
**4. Write `tiptask-runner.json`** in the same folder:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"url": "http://localhost:3000",
|
|
51
|
+
"apiKey": "the key you just copied",
|
|
52
|
+
"agent": "claude"
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**5. Start it:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx -y @tiptask/runner
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The project shows the agent as online in a few seconds. Delegate an issue to the agent,
|
|
63
|
+
or mention it in a comment. The task then starts in your terminal.
|
|
64
|
+
|
|
65
|
+
## Which coding agent
|
|
66
|
+
|
|
67
|
+
`agent` names a CLI that the runner knows. The runner then builds the command itself: the
|
|
68
|
+
flags for an unattended run, and the session resume. Every preset keeps a chat session.
|
|
69
|
+
|
|
70
|
+
| Agent | Runs |
|
|
71
|
+
| ------------- | ------------------ |
|
|
72
|
+
| `claude` | Claude Code |
|
|
73
|
+
| `codex` | Codex CLI |
|
|
74
|
+
| `opencode` | opencode |
|
|
75
|
+
| `antigravity` | Antigravity CLI |
|
|
76
|
+
| `copilot` | GitHub Copilot CLI |
|
|
77
|
+
| `pi` | Pi |
|
|
78
|
+
| `omp` | Oh My Pi |
|
|
79
|
+
|
|
80
|
+
Each preset needs the CLI's own MCP config.
|
|
81
|
+
[Coding agent setup](../../docs/runner.md) holds
|
|
82
|
+
that file, and the notes for each CLI.
|
|
83
|
+
|
|
84
|
+
`--agent` on the command line has priority over the file, except over an `agents` entry
|
|
85
|
+
that names its own. The runner appends the arguments after `--` to the ones from the
|
|
86
|
+
preset:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx -y @tiptask/runner --agent claude -- --model opus
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Write the same arguments in the file if a service manager starts the runner:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"url": "http://localhost:3000",
|
|
97
|
+
"apiKey": "the key you copied on creation",
|
|
98
|
+
"agent": "claude",
|
|
99
|
+
"cwd": "/Users/me/work/my-repo",
|
|
100
|
+
"args": ["--model", "opus"]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Your arguments come after the preset's own. A repeated flag thus replaces its value.
|
|
105
|
+
|
|
106
|
+
`command` replaces the preset with your own shell command. The command receives the task
|
|
107
|
+
on stdin. The runner keeps no session for it, because it does not know how to resume one.
|
|
108
|
+
|
|
109
|
+
## Several agents on one runner
|
|
110
|
+
|
|
111
|
+
One key is one agent. One runner can serve several agents. It polls for each key
|
|
112
|
+
separately, and every agent uses the `agent`, `cwd` and `args` from the file.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"url": "http://localhost:3000",
|
|
117
|
+
"agent": "claude",
|
|
118
|
+
"cwd": "/Users/me/work/my-repo",
|
|
119
|
+
"apiKeys": ["the first agent's key", "the second agent's key"]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The runner gives each command its own `TIPTASK_API_KEY`, so one MCP config in that
|
|
124
|
+
folder works for all agents.
|
|
125
|
+
|
|
126
|
+
Use `agents` instead of `apiKeys` when an agent needs its own settings. Each entry sets
|
|
127
|
+
its own `apiKey`. The other fields that an entry sets replace the shared ones, and the
|
|
128
|
+
entry inherits the rest. `name` is the agent's label in the log. Without it, the log
|
|
129
|
+
numbers the agents `#1`, `#2` in the order of the file.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"url": "http://localhost:3000",
|
|
134
|
+
"agent": "claude",
|
|
135
|
+
"cwd": "/Users/me/work/my-repo",
|
|
136
|
+
"agents": [
|
|
137
|
+
{ "apiKey": "the first agent's key", "name": "planner" },
|
|
138
|
+
{
|
|
139
|
+
"apiKey": "the second agent's key",
|
|
140
|
+
"cwd": "/Users/me/work/other-repo",
|
|
141
|
+
"args": ["--model", "opus"]
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`concurrency` counts per agent and per feed. A runner with three agents at
|
|
148
|
+
`concurrency: 2` can run six queued runs and six chat answers at once, each in its own
|
|
149
|
+
CLI process.
|
|
150
|
+
|
|
151
|
+
If the instance refuses the key of an agent, that agent stops. The other agents continue.
|
|
152
|
+
|
|
153
|
+
## Receiving tasks on a port
|
|
154
|
+
|
|
155
|
+
By default the runner polls the instance for tasks. With `listen`, the instance sends each
|
|
156
|
+
task to the runner when the task is created. The runner then does not poll.
|
|
157
|
+
|
|
158
|
+
Use `listen` for a runner on a server that the instance can reach. On a laptop behind a
|
|
159
|
+
router, use polling. Polling needs no setup. The chat is as fast with polling, because a
|
|
160
|
+
chat claim waits on the server until a message comes.
|
|
161
|
+
|
|
162
|
+
**1.** In the agent's settings, open Connection and select Endpoint. Enter the public
|
|
163
|
+
HTTPS address of the runner's port (see [Reaching the port](#reaching-the-port)), and
|
|
164
|
+
save. Copy the signing secret.
|
|
165
|
+
|
|
166
|
+
**2.** Add `listen` and `signingSecret` to `tiptask-runner.json`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"url": "https://plan.example.com",
|
|
171
|
+
"apiKey": "the agent's key",
|
|
172
|
+
"agent": "claude",
|
|
173
|
+
"listen": 4000,
|
|
174
|
+
"signingSecret": "whsec_…"
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**3.** Start the runner. It logs `listening on port 4000`. In the agent's settings, click
|
|
179
|
+
Send test. The result must be `200`.
|
|
180
|
+
|
|
181
|
+
The runner answers each task immediately. Then it runs the task as it runs a polled task,
|
|
182
|
+
with the same command, heartbeats and result. The rules for a request:
|
|
183
|
+
|
|
184
|
+
- The runner accepts only a `POST` request signed with the secret of one of its agents.
|
|
185
|
+
- It refuses a request whose signature time is more than five minutes from its clock.
|
|
186
|
+
- If the instance sends a task again while the task runs, the runner does not start it a
|
|
187
|
+
second time.
|
|
188
|
+
|
|
189
|
+
Several agents can use one port. The runner identifies the agent by the secret, so each
|
|
190
|
+
agent needs its own `signingSecret`. Put the agents in `agents`:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"url": "https://plan.example.com",
|
|
195
|
+
"agent": "claude",
|
|
196
|
+
"listen": 4000,
|
|
197
|
+
"agents": [
|
|
198
|
+
{ "apiKey": "the first agent's key", "signingSecret": "whsec_first…" },
|
|
199
|
+
{ "apiKey": "the second agent's key", "signingSecret": "whsec_second…" }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
In the same file, the runner polls for each agent that has no `listen`.
|
|
205
|
+
|
|
206
|
+
### Reaching the port
|
|
207
|
+
|
|
208
|
+
The runner serves plain HTTP. In production, the instance requires HTTPS. Put a tunnel or a
|
|
209
|
+
reverse proxy in front of the port:
|
|
210
|
+
|
|
211
|
+
- **Reverse proxy** on a server with a domain. Caddy gets the certificate automatically:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
runner.example.com {
|
|
215
|
+
reverse_proxy localhost:4000
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- **Cloudflare Tunnel.** `cloudflared tunnel --url http://localhost:4000` prints a
|
|
220
|
+
`trycloudflare.com` address. The address changes each time the tunnel starts, and then
|
|
221
|
+
you must change the agent's URL too. A named tunnel on your own domain keeps the same
|
|
222
|
+
address.
|
|
223
|
+
- **ngrok.** `ngrok http 4000`. A static domain in your ngrok account keeps the same
|
|
224
|
+
address.
|
|
225
|
+
- **Tailscale Funnel.** `tailscale funnel 4000` publishes the port at the machine's
|
|
226
|
+
`ts.net` address.
|
|
227
|
+
|
|
228
|
+
On a private network without a tunnel, add the runner's host to `SSRF_ALLOWED_HOSTS` on
|
|
229
|
+
the instance. That host must also use HTTPS. When `NODE_ENV` on the instance is not
|
|
230
|
+
`production` or `test`, the instance also accepts `http://` and private addresses, for
|
|
231
|
+
example `http://localhost:4000`.
|
|
232
|
+
|
|
233
|
+
## Settings
|
|
234
|
+
|
|
235
|
+
The runner reads `./tiptask-runner.json`. For a different file, give the path as an
|
|
236
|
+
argument or in `TIPTASK_RUNNER_CONFIG`:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npx -y @tiptask/runner /path/to/config.json
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The environment variables have priority over the file. The command-line options have
|
|
243
|
+
priority over both. An `agents` entry has priority over all three, for the fields it sets.
|
|
244
|
+
|
|
245
|
+
| Field | Environment variable | Default | What it does |
|
|
246
|
+
| ---------------- | --------------------------- | ------------------ | ---------------------------------------------------------------------- |
|
|
247
|
+
| `url` | `TIPTASK_URL` | required | Your TipTask instance |
|
|
248
|
+
| `apiKey` | `TIPTASK_API_KEY` | required | The external agent's key. Replaced by `apiKeys` or `agents` |
|
|
249
|
+
| `apiKeys` | | — | Several keys, all with these settings. Not with `agents` |
|
|
250
|
+
| `agents` | | — | Several agents, each with its own key and settings. Not with `apiKeys` |
|
|
251
|
+
| `agent` | `TIPTASK_AGENT` | — | The coding agent to run |
|
|
252
|
+
| `args` | | `[]` | Arguments added after the preset's |
|
|
253
|
+
| `command` | `TIPTASK_COMMAND` | — | Your own command, instead of `agent` |
|
|
254
|
+
| `cwd` | `TIPTASK_CWD` | where you start it | Working directory for the command |
|
|
255
|
+
| `env` | | `{}` | Extra variables for the command |
|
|
256
|
+
| `concurrency` | `TIPTASK_CONCURRENCY` | `1` | Tasks at once, per agent. Queued runs and chat answers count apart |
|
|
257
|
+
| `pollIntervalMs` | `TIPTASK_POLL_INTERVAL_MS` | `3000` | Wait after an empty queue. Minimum 1000 |
|
|
258
|
+
| `timeoutMs` | `TIPTASK_TIMEOUT_MS` | `1800000` | Time before the runner stops a task |
|
|
259
|
+
| `outputFormat` | `TIPTASK_OUTPUT_FORMAT` | the preset's | How the runner reads the output. See [The output](#the-output) |
|
|
260
|
+
| `listen` | `TIPTASK_LISTEN` | — | Port that receives the tasks. The runner does not poll for the agent |
|
|
261
|
+
| `signingSecret` | `TIPTASK_SIGNING_SECRET` | — | The agent's signing secret. Required with `listen` |
|
|
262
|
+
|
|
263
|
+
Set `agent` or `command`. With the environment you need no file at all:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
export TIPTASK_API_KEY=…
|
|
267
|
+
npx -y @tiptask/runner --agent claude
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## What the coding agent receives
|
|
271
|
+
|
|
272
|
+
### The task text
|
|
273
|
+
|
|
274
|
+
The server writes the task text. The text states what occurred, what to do, and to post
|
|
275
|
+
the result as a comment on the issue. Your own `command` receives the text on **stdin**. A
|
|
276
|
+
preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude`, `codex`
|
|
277
|
+
and `pi`, as an argument for `antigravity`, `copilot`, `opencode` and `omp`.
|
|
278
|
+
|
|
279
|
+
The agent reads all other data through the MCP server at `$TIPTASK_URL/mcp`. The agent
|
|
280
|
+
sends `$TIPTASK_API_KEY` as a bearer token, and acts as its own user with its role.
|
|
281
|
+
|
|
282
|
+
### The environment
|
|
283
|
+
|
|
284
|
+
Every run holds these variables:
|
|
285
|
+
|
|
286
|
+
| Variable | What it holds |
|
|
287
|
+
| ------------------------ | --------------------------------------------------------------------------- |
|
|
288
|
+
| `TIPTASK_URL` | the instance that sent the task |
|
|
289
|
+
| `TIPTASK_API_KEY` | the agent's key, for the API and the MCP server |
|
|
290
|
+
| `TIPTASK_TRIGGER` | `mention`, `delegation`, `field`, `schedule`, `manual`, `status`, or `chat` |
|
|
291
|
+
| `TIPTASK_SYSTEM_PROMPT` | the context of the run, for a command that takes a system prompt |
|
|
292
|
+
| `TIPTASK_TIMEOUT_MS` | the task timeout in milliseconds |
|
|
293
|
+
|
|
294
|
+
A command can read `TIPTASK_TIMEOUT_MS` to set its own timeout. The value is the
|
|
295
|
+
`timeoutMs` of that agent, and it replaces a value from the environment or from `env`.
|
|
296
|
+
The time starts when the command starts.
|
|
297
|
+
|
|
298
|
+
A queued run adds three more:
|
|
299
|
+
|
|
300
|
+
| Variable | What it holds |
|
|
301
|
+
| ------------------- | --------------------------------------------------------------- |
|
|
302
|
+
| `TIPTASK_RUN_ID` | the run's id |
|
|
303
|
+
| `TIPTASK_ISSUE` | the issue key, for example `MKT-42`. Empty if there is no issue |
|
|
304
|
+
| `TIPTASK_ISSUE_ID` | the numeric id of the issue. Empty if there is no issue |
|
|
305
|
+
|
|
306
|
+
A chat message adds three others in their place, and `TIPTASK_TRIGGER` is `chat`:
|
|
307
|
+
|
|
308
|
+
| Variable | What it holds |
|
|
309
|
+
| --------------------- | ----------------------------------------------------------------- |
|
|
310
|
+
| `TIPTASK_THREAD_ID` | the conversation's id |
|
|
311
|
+
| `TIPTASK_MESSAGE_ID` | the id of the message to answer |
|
|
312
|
+
| `TIPTASK_SESSION_ID` | the coding agent's session to resume. Empty for the first message |
|
|
313
|
+
|
|
314
|
+
The task text of a chat message includes the conversation, unless a session holds it. See
|
|
315
|
+
[Sessions](#sessions).
|
|
316
|
+
|
|
317
|
+
## The output
|
|
318
|
+
|
|
319
|
+
Exit code `0` is a success. Any other code is a failure, and the end of stderr becomes the
|
|
320
|
+
error.
|
|
321
|
+
|
|
322
|
+
`outputFormat` tells the runner how to read stdout:
|
|
323
|
+
|
|
324
|
+
- `text` — all of stdout is the answer.
|
|
325
|
+
- `claude-stream-json`, `codex-jsonl`, `opencode-json`, `antigravity-stream-json`,
|
|
326
|
+
`copilot-json`, `pi-json` — the event stream of that CLI. `pi` and `omp` both use
|
|
327
|
+
`pi-json`. The runner reads the text and the tool calls from it.
|
|
328
|
+
|
|
329
|
+
A preset sets `outputFormat` for you. Set it yourself only with your own `command`. If the
|
|
330
|
+
output does not agree with the format, the runner uses the output as plain text.
|
|
331
|
+
|
|
332
|
+
A queued run reports one answer as its output. With an event stream format, the answer is
|
|
333
|
+
the text that the agent wrote after its last tool call.
|
|
334
|
+
|
|
335
|
+
### Run transcripts
|
|
336
|
+
|
|
337
|
+
While a queued run works, the runner sends the agent's messages and tool calls to the
|
|
338
|
+
instance. The issue shows them as the run's transcript.
|
|
339
|
+
|
|
340
|
+
- The tool calls need an event stream format. With `text`, the transcript holds only the
|
|
341
|
+
output.
|
|
342
|
+
- With `codex-jsonl`, the run shows an **Open in Codex** link. The link opens the session
|
|
343
|
+
only where that session is: in the Codex app of the viewer, or on a connected host. The
|
|
344
|
+
link does not copy the session.
|
|
345
|
+
- The instance keeps up to 2,000 events or 1 MB for each attempt, and marks a transcript
|
|
346
|
+
that it cut. The cut does not change the result of the run.
|
|
347
|
+
- If the last upload fails with a network or server error, the runner tries it one more
|
|
348
|
+
time. Then it reports the result of the command, even with an incomplete transcript.
|
|
349
|
+
Any other refusal fails the run.
|
|
350
|
+
|
|
351
|
+
## Chat
|
|
352
|
+
|
|
353
|
+
The runner streams the answer to the chat while the command runs. It sends the tool calls
|
|
354
|
+
with the answer.
|
|
355
|
+
|
|
356
|
+
### Sessions
|
|
357
|
+
|
|
358
|
+
Each conversation resumes one session of the coding agent, which keeps its full context.
|
|
359
|
+
Without a session the server sends the last 20 messages of the conversation as plain text.
|
|
360
|
+
|
|
361
|
+
- The chat header shows the session id. Run `claude --resume <id>` in the runner's working
|
|
362
|
+
directory to open the same session in your terminal.
|
|
363
|
+
- Your own `command` keeps no session. Each chat message then includes the conversation.
|
|
364
|
+
- A session stays on the machine that started it. If you delete its files or move the
|
|
365
|
+
runner, the answers fail. Start a new chat then.
|
|
366
|
+
|
|
367
|
+
## Requirements
|
|
368
|
+
|
|
369
|
+
- **Node 20 or later.**
|
|
370
|
+
- **An TipTask instance 1.7.0 or later.**
|
|
371
|
+
- **An external agent in your project.** The runner authenticates with its API key.
|
|
372
|
+
- **The coding agent, installed and signed in on the same machine.** The runner starts the
|
|
373
|
+
CLI as you, but it cannot sign in for you.
|
|
374
|
+
|
|
375
|
+
macOS and Linux need nothing else. The runner starts a preset directly, so its arguments
|
|
376
|
+
need no quotes. Your own `command` goes to `sh -c`, so write it as POSIX shell.
|
|
377
|
+
|
|
378
|
+
Windows needs WSL2, because `cmd.exe` and PowerShell have no `sh`:
|
|
379
|
+
|
|
380
|
+
```powershell
|
|
381
|
+
wsl --install # installs Ubuntu, then reboot
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Do everything else in WSL: Node, the coding agent, the config file, and the runner.
|
|
385
|
+
|
|
386
|
+
- Keep the working directory in the WSL file system (`~/work/repo`), not under
|
|
387
|
+
`/mnt/c/...`. Access between the two file systems is slow.
|
|
388
|
+
- An instance on the Windows host is available at `http://localhost:3000`.
|