@dfsramos/foreman 0.0.0-stage → 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/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +385 -2
- package/dist/agent/adapters/agy.js +96 -0
- package/dist/agent/adapters/claude.js +152 -0
- package/dist/agent/adapters/codex.js +107 -0
- package/dist/agent/adapters/opencode.js +91 -0
- package/dist/agent/config.js +169 -0
- package/dist/agent/process.js +230 -0
- package/dist/agent/prompt.js +184 -0
- package/dist/agent/registry.js +57 -0
- package/dist/agent/types.js +12 -0
- package/dist/cli-args.js +70 -0
- package/dist/cli.js +184 -0
- package/dist/config.js +102 -0
- package/dist/daemon.js +109 -0
- package/dist/db.js +467 -0
- package/dist/git/taskstore.js +224 -0
- package/dist/git/worktree.js +248 -0
- package/dist/imports/connections.js +57 -0
- package/dist/imports/destination.js +2 -0
- package/dist/imports/linear.js +40 -0
- package/dist/imports/registry.js +4 -0
- package/dist/imports/types.js +1 -0
- package/dist/kaneo/client.js +47 -0
- package/dist/kaneo/issues.js +138 -0
- package/dist/kaneo/workspace.js +24 -0
- package/dist/linear/browse.js +55 -0
- package/dist/linear/client.js +48 -0
- package/dist/linear/issues.js +99 -0
- package/dist/linear/workspace.js +30 -0
- package/dist/paths.js +12 -0
- package/dist/pidfile.js +35 -0
- package/dist/projects.js +211 -0
- package/dist/review/decide.js +186 -0
- package/dist/review/loop.js +114 -0
- package/dist/runner.js +1281 -0
- package/dist/server/app.js +344 -0
- package/dist/server/imports.js +182 -0
- package/dist/server/tasks.js +77 -0
- package/dist/sources/branch.js +17 -0
- package/dist/sources/git-import.js +60 -0
- package/dist/sources/git.js +430 -0
- package/dist/sources/kaneo.js +156 -0
- package/dist/sources/lease.js +38 -0
- package/dist/sources/linear.js +186 -0
- package/dist/sources/options.js +26 -0
- package/dist/sources/registry.js +6 -0
- package/dist/sources/taskfile.js +190 -0
- package/dist/sources/types.js +1 -0
- package/package.json +63 -4
- package/public/agents.js +435 -0
- package/public/app.js +1200 -0
- package/public/imports.js +481 -0
- package/public/index.html +19 -0
- package/public/projects.js +481 -0
- package/public/style.css +1614 -0
- package/public/tasks.js +253 -0
- package/public/ui.js +165 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to foreman are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/); while the version starts with 0, a minor release may change settings or behaviour.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-10-26
|
|
8
|
+
|
|
9
|
+
The first public release, installed with `npm install -g @dfsramos/foreman`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The `foreman` command (also `fm`): `start` runs foreman in the background, `stop` stops it, `status` shows its projects and runs in progress, `logs` prints or follows the log, and `run` keeps it in the foreground.
|
|
13
|
+
- Projects that poll Linear, Kaneo or a branch of a git repository for tasks, run a coding agent (Claude Code, Codex CLI, opencode or Antigravity CLI) in a fresh worktree for each one, and follow its pull request through review, CI and merge.
|
|
14
|
+
- A web console to manage projects and agents and follow the runs.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Data is kept in the platform's standard directory (`~/.local/share/foreman` on Linux and WSL, `~/Library/Application Support/foreman` on macOS) instead of `./data`; `DATA_DIR` still overrides it, and `.env` is read from that directory.
|
|
18
|
+
- Stopping foreman with SIGTERM (`foreman stop`) now waits for the runs in progress to finish; SIGINT (`foreman stop --now`, or Ctrl-C under `foreman run`) aborts them as before.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Ramos
|
|
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
CHANGED
|
@@ -1,3 +1,386 @@
|
|
|
1
|
-
#
|
|
1
|
+
# foreman
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A polling-and-dispatch daemon that connects issue trackers to coding-agent CLIs such as Claude Code.
|
|
4
|
+
|
|
5
|
+
Foreman runs one or more projects. Each project polls its tracker (Linear, Kaneo or a branch of a git repository) for issues in a target workflow state. For each one foreman claims the issue, runs a coding agent in a fresh git worktree, checks that the agent opened a pull request, and reports back to the tracker. A web console starts and stops the runner, shows the live agent logs and run history, manages the projects and the agents, and lists and creates the tasks of a project that keeps them on a git branch.
|
|
6
|
+
|
|
7
|
+
It runs as many issues at once as the enabled agents allow, each in its own worktree. There is no queue beyond the tracker itself and no automatic retry.
|
|
8
|
+
|
|
9
|
+
## How a run works
|
|
10
|
+
|
|
11
|
+
| Step | What foreman does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Claim | Moves the issue to the in-progress state and comments that it was picked up. If the move fails, the issue is skipped and nothing is recorded. |
|
|
14
|
+
| Worktree | Runs `git fetch`, then creates `<worktree root>/<issue identifier>` on a new branch (Linear's suggested branch name) from the repository's default branch. |
|
|
15
|
+
| Agent | Runs the chosen agent CLI headless in the worktree, with that agent's own permission settings. The prompt contains the full issue body, the worktree path and the repository path. The agent commits, pushes and opens the pull request. |
|
|
16
|
+
| Pull request | Looks up the open pull request for the branch with `gh pr list --head <branch>`. |
|
|
17
|
+
| Report | Moves the issue to the review state, comments with the pull request link, and removes the worktree. In the Copilot review mode it also asks Copilot to review (see [Review modes](#review-modes)). |
|
|
18
|
+
|
|
19
|
+
After that the issue is in review, and foreman keeps following its pull request (see [The review loop](#the-review-loop)).
|
|
20
|
+
|
|
21
|
+
If any step fails, the issue stays in the in-progress state with a comment explaining why, the worktree is kept, and the run is shown as failed. Foreman never picks that issue up again on its own.
|
|
22
|
+
|
|
23
|
+
**When the agent needs input.** An agent that cannot do the issue as written, because it needs a decision, information or access it does not have, is told not to guess: it opens no pull request and ends with a line `FOREMAN_BLOCKED: <what it needs>`. Foreman then posts that question on the issue, which stays in the in-progress state, and shows the run as **Needs input**. It removes the worktree and deletes the branch, so that a retry starts clean, unless the agent left uncommitted files or commits there; the comment says what was kept. To retry, add the answer to the issue's description (the agent reads the description, not the comments) and move the issue back to the target state. A run that opened a pull request counts as finished whatever its last message says.
|
|
24
|
+
|
|
25
|
+
**Aborting a run.** **Abort run**, on a run's header, kills that run's agent and everything it started. The issue stays claimed in its tracker with a comment. Other runs carry on.
|
|
26
|
+
|
|
27
|
+
**Restarts.** If foreman is killed mid-run rather than stopped (see [Commands](#commands)), the agent keeps running on its own until foreman starts again. On the next start the interrupted run is marked failed, the agent and the commands it started are stopped, and the issue gets a comment.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Linux, macOS, or Windows through WSL
|
|
32
|
+
- Node.js 24 or later
|
|
33
|
+
- `git`, with each project's repository cloned locally and an `origin` remote
|
|
34
|
+
- GitHub CLI (`gh`), authenticated for those repositories
|
|
35
|
+
- At least one supported agent CLI, installed and signed in (see [Agents](#agents))
|
|
36
|
+
- An API key for each project's tracker: a Linear personal API key, or a Kaneo API key. A project whose tasks live on a git branch needs push access to that repository instead (see [Git branch](#git-branch))
|
|
37
|
+
|
|
38
|
+
Foreman finds the default branch through `origin/HEAD`. If a run fails with "Could not determine the default branch", run `git remote set-head origin --auto` in the repository.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm install -g @dfsramos/foreman
|
|
44
|
+
foreman start
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`foreman start` runs foreman in the background and prints the console's address, http://127.0.0.1:4310 by default. Open it, add a project on the **Projects** tab, and press **Start**.
|
|
48
|
+
|
|
49
|
+
`fm` is the same command as `foreman`, for anyone whose `foreman` is already the Ruby Procfile runner of that name.
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `foreman start` | Starts foreman in the background, writing its output to `foreman.log` in the data directory. Does nothing if it is already running. |
|
|
56
|
+
| `foreman stop` | Stops polling, waits for the runs in progress to finish, then exits. The console stays reachable while it waits. |
|
|
57
|
+
| `foreman stop --now` | Aborts the runs in progress instead of waiting. Their issues stay in the in-progress state, as after any failed run. |
|
|
58
|
+
| `foreman status` | Shows the console's address, the data directory, each project's state and the runs in progress. Exits with 3 when foreman is not running. |
|
|
59
|
+
| `foreman logs [-n N] [-f]` | Prints the last N lines of the log (default 100); `-f` keeps following it. |
|
|
60
|
+
| `foreman run` | Runs foreman in the foreground, printing to the terminal; for a service manager or debugging. Ctrl-C aborts the runs in progress. |
|
|
61
|
+
| `foreman version`, `foreman help` | Print the version, or the list of commands. |
|
|
62
|
+
|
|
63
|
+
Sending SIGTERM to foreman waits for runs in progress as `foreman stop` does; SIGINT, or a second signal while waiting, aborts them as `foreman stop --now` does. Under a service manager, allow for that wait: systemd, for one, kills a service 90 seconds after asking it to stop unless `TimeoutStopSec` says otherwise.
|
|
64
|
+
|
|
65
|
+
Foreman finds the agent CLIs, `git` and `gh` on the `PATH` it was started with, and passes its environment on to them. `foreman start` from your shell inherits your shell's; a service manager usually starts with a shorter `PATH`, which then needs to include where those CLIs are installed.
|
|
66
|
+
|
|
67
|
+
## Settings
|
|
68
|
+
|
|
69
|
+
The process itself needs only three settings, all optional, read from the environment or from `.env` in the data directory:
|
|
70
|
+
|
|
71
|
+
| Variable | Default | Purpose |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `DATA_DIR` | `~/.local/share/foreman` on Linux and WSL (or `$XDG_DATA_HOME/foreman`), `~/Library/Application Support/foreman` on macOS | Where foreman keeps everything (below). Set it in the environment only, since `.env` is found through it. |
|
|
74
|
+
| `HOST` | `127.0.0.1` | Address the console listens on. |
|
|
75
|
+
| `PORT` | `4310` | Port the console listens on. |
|
|
76
|
+
|
|
77
|
+
A value already set in the environment wins over `.env`. Everything else is stored in the database and edited in the console.
|
|
78
|
+
|
|
79
|
+
The data directory holds:
|
|
80
|
+
|
|
81
|
+
| Path | Contents |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `foreman.db` | The SQLite database: projects, agents, run history and tracker credentials. |
|
|
84
|
+
| `.env` | Optional settings, as above. |
|
|
85
|
+
| `foreman.log` | Output of `foreman start`. Moved to `foreman.log.1` on start once it is over 10 MB. |
|
|
86
|
+
| `foreman.pid` | The process ID while foreman is running. |
|
|
87
|
+
| `tasks/` | Clones of the branches that hold Git branch tasks. |
|
|
88
|
+
| `worktrees/` | The agents' worktrees, unless a project sets its own worktree root. |
|
|
89
|
+
|
|
90
|
+
### Upgrading from a checkout
|
|
91
|
+
|
|
92
|
+
Before 0.2.0, foreman ran from a copy of its source code, kept its data in `./data` in the directory it was started from, and read `.env` from that directory. To keep using that data, stop foreman and either set `DATA_DIR` to the old directory, or move it, before the new location exists (an installed foreman that has already started creates it; remove it first if it holds nothing you need):
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
mv ./data ~/.local/share/foreman # Linux and WSL
|
|
96
|
+
mv ./data ~/Library/Application\ Support/foreman # macOS
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then move the old `.env`, if there is one, into the data directory, and drop any `DATA_DIR` line from it.
|
|
100
|
+
|
|
101
|
+
After moving the data, two things still point at the old place, because both are stored as absolute paths:
|
|
102
|
+
|
|
103
|
+
- **Worktree roots.** A project created with the default worktree root has the old `data/worktrees/<project>` path saved. Edit each such project's worktree root in the console to the new location.
|
|
104
|
+
- **Open worktrees.** Git records where each worktree is. In each repository that still has a foreman worktree, run `git worktree repair <new worktree path>`, or move the data only once those worktrees are gone.
|
|
105
|
+
|
|
106
|
+
**Upgrading from a single-project setup.** If the environment or `.env` still has `LINEAR_API_KEY`, `LINEAR_TEAM_ID`, `LINEAR_PROJECT_ID` and `REPO_PATH`, foreman turns them into the first project the first time it starts with no projects, and assigns the existing run history to it. After that those variables are ignored and can be removed. `AGENT_ALLOWED_TOOLS` and `CLAUDE_BIN` are likewise only read as initial values for the claude agent.
|
|
107
|
+
|
|
108
|
+
## Projects
|
|
109
|
+
|
|
110
|
+
A project connects one tracker source to one repository. On the Projects tab each project has:
|
|
111
|
+
|
|
112
|
+
| Setting | Meaning |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Name | Shown on runs and in the history. Unique. |
|
|
115
|
+
| Priority | Low, Normal or High: how often the project gets an agent when projects compete (see below). |
|
|
116
|
+
| Source | The tracker and its settings. For Linear: API key, team, project, the five workflow states (target, in-progress, review, approval, done), and a label. |
|
|
117
|
+
| Repository path | A local git repository. A repository can belong to only one project. |
|
|
118
|
+
| Worktree root | Where this project's worktrees go. Defaults to `<DATA_DIR>/worktrees/<project>`. |
|
|
119
|
+
| Poll interval | Seconds between polls, 10 to 86400. |
|
|
120
|
+
| Agent timeout | Longest an agent may run on one issue, 1 to 1440 minutes. |
|
|
121
|
+
| Allowed agents | Which agents may work on this project. None selected means any. |
|
|
122
|
+
| Start with foreman | Begin polling when foreman starts. A new project is created stopped. |
|
|
123
|
+
| Rounds of feedback per issue | How many rounds of follow-up an issue gets (review feedback, merge conflicts and agent reviews all count) before a person takes over. 0 turns the review loop off. Default 5. |
|
|
124
|
+
| Ignore comments from | GitHub accounts whose pull request comments are not review feedback, such as deployment bots. |
|
|
125
|
+
| Review | Who is asked to review a pull request once it opens: No review (the default), Copilot review, Another agent, or Any agent (prefers another). See [Review modes](#review-modes). |
|
|
126
|
+
| Wait for green CI | On by default. Failing checks go back to the agent, and approvals wait until the checks pass. See [CI](#ci). |
|
|
127
|
+
|
|
128
|
+
**Label.** A Linear project only picks up issues that carry its label, `foreman` by default, matched without regard to case. This lets ordinary issues sit in the target state without being dispatched. Clear the label setting to pick up every issue in the target state. Foreman does not create the label; add it in Linear.
|
|
129
|
+
|
|
130
|
+
**Load teams and projects** uses the API key to list what it can see, so the team, the project and the three states can be chosen from lists instead of typed; the project and state lists follow the chosen team. For a project that already has a key this happens when you open the form. The values can still be entered by hand, as IDs and state names, if the list cannot be loaded.
|
|
131
|
+
|
|
132
|
+
**Check source** tries the source settings against the tracker without saving, and reports, for example, a state name that does not exist. Creating or saving a project runs the same check and shows its result above the project list; the project is saved whatever the check finds, and the message says what to fix. A new project offers the Git branch source first.
|
|
133
|
+
|
|
134
|
+
State names are matched case-insensitively on every poll. A problem with one project's source is shown against that project and does not affect the others.
|
|
135
|
+
|
|
136
|
+
**Starting and stopping.** Each project is started and stopped on its own, from the Run tab or the Projects tab; the header has Start all and Stop all. Stopping a project ends its polling and lets its runs in progress finish. A project can be deleted once it is stopped and has no runs in progress; its past runs stay in the history.
|
|
137
|
+
|
|
138
|
+
**Sharing agents between projects.** All projects draw on the same agent slots. A slot that frees up goes at once to a project with work waiting. When several are waiting they take weighted turns: for every one turn of a Low project, a Normal project gets two and a High project three. No project is starved, however busy the others are. Within a project, issues are taken by the tracker's own priority, then oldest first.
|
|
139
|
+
|
|
140
|
+
**Trackers.** Linear, Kaneo and a git branch (one Markdown file per task on a branch of a git repository, source **Git branch**) are supported; see [Kaneo](#kaneo) and [Git branch](#git-branch) below. Others can be added (see [Development](#development)).
|
|
141
|
+
|
|
142
|
+
## Kaneo
|
|
143
|
+
|
|
144
|
+
[Kaneo](https://github.com/usekaneo/kaneo) is a small self-hosted tracker (one app container and Postgres, MIT licence), which makes it a good place to keep the tasks foreman works from. To use it:
|
|
145
|
+
|
|
146
|
+
1. Run Kaneo (its own README has a `compose.yml`) and create a workspace and a project.
|
|
147
|
+
2. In the project, make sure these five columns exist: **To Do**, **In Progress**, **In Review**, **Pending approval** and **Done**. Kaneo has the other four by default; create *Pending approval* yourself. Names are matched without regard to case on every poll, and the names can be changed in the project's source settings.
|
|
148
|
+
3. Create an API key in Kaneo (Settings, API keys).
|
|
149
|
+
4. Add a project here with the source **Kaneo**: the Kaneo address (for example `http://localhost:5173`; the API is under `/api`), the key, the Kaneo project, the five columns and the label.
|
|
150
|
+
|
|
151
|
+
**Label.** As with Linear, only tasks carrying the label (`foreman` by default) are picked up. Create the label in Kaneo, or let an import create it. Clear the setting to pick up every task in the target column.
|
|
152
|
+
|
|
153
|
+
**Check source** reports a missing column by name and lists the columns that exist.
|
|
154
|
+
|
|
155
|
+
Kaneo limits an API key to 100 requests per window. A poll costs one column lookup plus one listing per column read, so a handful of projects is fine; a rate-limit error shows against the project and the next poll tries again.
|
|
156
|
+
|
|
157
|
+
## Git branch
|
|
158
|
+
|
|
159
|
+
The **Git branch** source keeps a project's tasks as one Markdown file each, in a directory on a branch of a git repository: by default an orphan branch `foreman` of the project's own repository, under `.foreman/tasks`. There is no tracking service to run. Foreman reads and writes the files through a clone under `<DATA_DIR>/tasks/` and pushes every change at once, so any machine with push access sees the same tasks and can take a project over, mid review loop included. A project picks it as its source like Linear or Kaneo.
|
|
160
|
+
|
|
161
|
+
| Setting | Meaning |
|
|
162
|
+
|---|---|
|
|
163
|
+
| Repository | URL or path of the repository that holds the task files. Blank means the project's own `origin` remote. Point it at a separate private repository to keep foreman's data out of the project; several projects can share one, with a directory each. |
|
|
164
|
+
| Branch | `foreman` by default. **Check source** creates it on the remote when it does not exist. Mind the spelling: a misspelt name becomes a new branch. |
|
|
165
|
+
| Directory | `.foreman/tasks` by default, relative to the repository root. |
|
|
166
|
+
| Identifier prefix | `FM` by default: the task in `12-add-login-page.md` is `FM-12`. |
|
|
167
|
+
|
|
168
|
+
**Creating the branch.** Foreman creates it, as one empty commit with no parent, the first time the project is saved or checked; the task files stay out of the project's history. A second worktree on the branch is the place to write tasks from the command line, once it exists:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
git fetch origin foreman
|
|
172
|
+
git worktree add ../myproject-tasks foreman
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A branch created by mistake, under a misspelt name say, is that one empty commit; remove it with `git push origin --delete <name>`. **Check source** reports that pushes work, how many tasks the directory holds in each state, and which machine polls the project.
|
|
176
|
+
|
|
177
|
+
**The file format.** A task is `<number>-<slug>.md`: an optional frontmatter block of flat `key: value` lines between two `---` lines, a `# ` title, the description, and a `## Log` that foreman appends to. This one is being worked on:
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
---
|
|
181
|
+
state: in_progress
|
|
182
|
+
priority: 2
|
|
183
|
+
created: 2026-10-23T10:00:00Z
|
|
184
|
+
claimed_by: laptop
|
|
185
|
+
claimed_at: 2026-10-23T10:05:00Z
|
|
186
|
+
branch: fm-12-add-login-page
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
# Add login page
|
|
190
|
+
|
|
191
|
+
Users sign in with their email address and a password. Keep the form on `/login`.
|
|
192
|
+
|
|
193
|
+
## Log
|
|
194
|
+
|
|
195
|
+
### 2026-10-23T10:05:00Z
|
|
196
|
+
|
|
197
|
+
Foreman: picked up. Claude Code is working on it.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `state` is `todo`, `in_progress`, `in_review`, `pending_approval` or `done`; a file with no frontmatter, or without `state`, is a todo. `priority` is 0 to 4: 0 is none, 1 urgent, 4 low. Todo tasks are taken by priority (none last), then by number.
|
|
201
|
+
- You write `state`, `priority`, the title and the description, and may add keys of your own; foreman keeps keys it does not know.
|
|
202
|
+
- Foreman writes `claimed_by` and `claimed_at` (the machine that holds the task and since when), `branch` (the agent's branch, fixed when the task is claimed), `in_review_since` and `review` (its review progress, as one JSON line), moves `state`, and appends to the log where other trackers would get a comment. The Import tab and the Tasks tab write `created`. Each change is one commit on the branch, as `foreman <foreman@<hostname>>`, with a message such as `Claim FM-12 on laptop.` or `Move FM-12 to in review.`; a push that is rejected because another machine pushed first is re-read and retried.
|
|
203
|
+
|
|
204
|
+
**Writing a task.** Create `<number>-<slug>.md` in the directory with the next free number, commit and push; a file holding only `# Add login page` is a valid todo. The number is the identifier (`FM-12`) and must not be reused; a done task's file stays where it is. Two files with one number stop the poll with a message against the project in the console, until one of them is renamed. The Tasks tab does the same from the console, and the Import tab copies tasks in from Linear.
|
|
205
|
+
|
|
206
|
+
**Retrying.** A failed or blocked task stays `in_progress` with its claim, as with any tracker. To retry it, set `state: todo`, remove `claimed_by` and `claimed_at`, commit and push; foreman picks it up on the next poll. Remove the worktree and the branch first, as under [Retrying a failed issue](#retrying-a-failed-issue).
|
|
207
|
+
|
|
208
|
+
**One machine at a time.** A project is polled, dispatched and reviewed by one machine at a time, the one that holds its lease: the file `.lease` in the task directory, one JSON line naming the holder and when the lease runs out, ten minutes after it was taken or last renewed. The holder renews it while it polls. Other machines read the tasks but start no agent and act on no review. When a project is stopped, its machine hands the lease back once the project's runs are over, and the next machine that polls takes it over at once; a machine that is cut off leaves a lease that lapses within ten minutes, and the next poll after that takes it over. Either takeover clears the claims the previous machine left, with a log entry on each task (`Clear claims left by <host>.`); those tasks keep their state, where a failed run leaves them, and are retried as above. A run that outlives the lease still finishes under its own claim; one whose machine was cut off for longer than that finds its claim cleared when it finishes, and fails there. The Tasks tab and **Check source** show who holds the lease.
|
|
209
|
+
|
|
210
|
+
**Credentials.** Foreman itself pushes, so the account the daemon runs under needs push access to the repository without a prompt: for an HTTPS remote, `gh auth setup-git` or another credential helper; for SSH, a key without a passphrase or an agent that has it loaded. **Check source** runs a dry-run push and reports git's message when that fails, with the fix for an HTTPS or SSH remote when credentials are the cause; a machine that starts the project without them shows the same message against the project on its first poll. This source stores no key of its own.
|
|
211
|
+
|
|
212
|
+
**Using `main` instead.** Branch can be set to `main`, or any other existing branch, and the task files then live in that branch's tree; but every claim, comment, lease renewal and state move is then a commit on `main`, and every workflow with `on: push` and no branch filter runs on each of them. The orphan branch carries no workflow files, so GitHub Actions does not run on it; a CI system that triggers on every branch of the repository does, and needs a branch filter.
|
|
213
|
+
|
|
214
|
+
**Tasks tab.** The console's **Tasks** tab lists a Git branch project's tasks grouped by state, shows which machine holds the project's lease, and has a form that creates a task file on the branch (one commit, `Add FM-13: <title>.`). It is the way to add a task from a machine that has no worktree on the task branch.
|
|
215
|
+
|
|
216
|
+
## Importing from Linear
|
|
217
|
+
|
|
218
|
+
The **Import** tab copies tasks from another tracker into a Kaneo project or a Git branch project, so Linear can stay a place to browse and plan while foreman only works from the copy. Foreman only reads from Linear: it never adds a label, a comment or a state change there.
|
|
219
|
+
|
|
220
|
+
1. Add an import connection (a Linear API key). **Load teams** lists what the key can see.
|
|
221
|
+
2. Choose the connection, the project to copy into (Kaneo or Git branch), the team, optionally a Linear project and the states (Backlog and Todo by default), and press **Show tasks**.
|
|
222
|
+
3. Tick tasks and import them. Each lands in the target project's target state, so foreman picks it up on its next poll: in Kaneo, in the target column with the label; on a git branch, as a new `todo` task file (one commit each, `Add FM-13: <title>.`).
|
|
223
|
+
|
|
224
|
+
An import is a one-time copy. The task's description ends with a line `Imported from <identifier> · <url>`, and in Kaneo the task also has a link back to Linear. That line is also how foreman recognises a task it has already copied: those are shown as imported and skipped. Edits made in Linear afterwards are not carried over.
|
|
225
|
+
|
|
226
|
+
## The review loop
|
|
227
|
+
|
|
228
|
+
Opening the pull request is not the end. An issue moves through these states, and foreman acts in each:
|
|
229
|
+
|
|
230
|
+
| State | How an issue gets there | What foreman does |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| Target (e.g. Todo) | You put it there. | Picks it up, if it carries the project's label. |
|
|
233
|
+
| In progress | Foreman, when an agent starts on it. | The agent works; a comment says which agent. |
|
|
234
|
+
| Review (e.g. In Review) | Foreman, once the pull request is open. | Watches the pull request for new feedback and for merge conflicts. |
|
|
235
|
+
| Approval (e.g. Pending approval) | Foreman, once the review is positive. | Waits for a person to approve and merge; watches for the merge, for further feedback and for merge conflicts. |
|
|
236
|
+
| Done | Foreman, once the pull request is merged. | Nothing more. |
|
|
237
|
+
|
|
238
|
+
**Feedback.** New reviews, comments and line comments on the pull request are handed to an agent together. The issue goes back to in-progress while the agent works. The agent decides what the feedback amounts to and ends with a verdict:
|
|
239
|
+
|
|
240
|
+
- it made changes and pushed them: the issue returns to review, for another look;
|
|
241
|
+
- the feedback approves the pull request or recommends approval: the issue moves to the approval state;
|
|
242
|
+
- nothing needed changing and it is not an approval: the issue returns to review.
|
|
243
|
+
|
|
244
|
+
Whenever an issue returns to review, foreman asks the authors of that round's feedback to review again (`gh pr edit --add-reviewer`, with Copilot requested as `@copilot`). The pull request's own author is never asked, and an account GitHub will not accept as a reviewer is noted in the run log and skipped.
|
|
245
|
+
|
|
246
|
+
**Merge conflicts.** On each check foreman also asks GitHub whether the pull request can still be merged. One that conflicts with its base branch is handed to an agent, which merges the base into the branch, resolves the conflicts and pushes, without rebasing or force-pushing. This counts as a round, and happens for issues in review and for issues awaiting approval; the issue goes back to where it was afterwards. A conflict and new feedback found together are handled in one round. If the conflict is still there when the round limit is reached, foreman hands the issue to a person and does not raise that conflict again.
|
|
247
|
+
|
|
248
|
+
A review that is a bare GitHub approval, with no text and nothing else alongside, moves the issue to the approval state without using an agent.
|
|
249
|
+
|
|
250
|
+
**Limits and safeguards.**
|
|
251
|
+
|
|
252
|
+
- After the configured number of rounds on one issue, foreman stops, comments that a person is needed, and moves the issue to the approval state.
|
|
253
|
+
- Feedback is handled once. Everything on the pull request when a round ends, including the agent's own replies, counts as dealt with. Feedback posted while an agent is mid-round is therefore not picked up separately; the agent may or may not have seen it.
|
|
254
|
+
- Comments from the accounts in "Ignore comments from" never start a round. The default list covers common deployment and tracker bots; review bots are not on it, so their reviews are acted on.
|
|
255
|
+
- Pull requests are checked at most every two minutes per project.
|
|
256
|
+
- A pull request closed without merging is left alone, as is an issue with no pull request for its branch.
|
|
257
|
+
- A failed review run leaves the issue in progress with a comment, like any failed run.
|
|
258
|
+
- With "Wait for green CI" on: at most 2 reruns of failed checks per pull request head, and checks pending for 2 hours hand the issue to a person.
|
|
259
|
+
|
|
260
|
+
### CI
|
|
261
|
+
|
|
262
|
+
With **Wait for green CI** on (the default), the pull request's checks take part in the loop. Foreman reads the checks branch protection marks as required; when the branch has none, which is always the case for a private repository on a free GitHub plan, every check counts. A pull request with no checks counts as green.
|
|
263
|
+
|
|
264
|
+
- **Failing checks** become a round for the agent that wrote the pull request. Its prompt lists each failing check with its link and, for GitHub Actions, an excerpt of the failed job's log up to its last error. The agent fixes and pushes, or says the failure is not caused by the change. This counts as a round toward "Rounds of feedback per issue".
|
|
265
|
+
- **Approvals wait for green.** A bare GitHub approval, a round whose verdict is approval, and an agent reviewer's approval move the issue to the approval state only when the checks pass or there are none. Otherwise the approval is kept for that head and applied once the checks pass.
|
|
266
|
+
- **Agent reviews** run only on a head whose checks pass or that has none.
|
|
267
|
+
- **Pending checks** wait. Checks still pending 2 hours after foreman first saw them hand the issue to a person.
|
|
268
|
+
|
|
269
|
+
**Failures the agent cannot fix.** Not every red check is the code's fault:
|
|
270
|
+
|
|
271
|
+
- A GitHub Actions job that was cancelled, timed out or failed to start is rerun (`gh run rerun <id> --failed`) without an agent and without using a round.
|
|
272
|
+
- In a CI round, the agent ends with `FOREMAN_CI: fixed`, or with `FOREMAN_CI: not_code <reason>` when the failure is not caused by the change: a missing secret, an outage, a quota, a test that also fails on the base branch. After `not_code`, foreman reruns the failed jobs once.
|
|
273
|
+
- One head gets at most 2 reruns, counting both kinds. Checks that still fail after the rerun the agent asked for, `not_code` on checks foreman cannot rerun (only GitHub Actions jobs can be), and an agent that says it fixed the checks but pushed nothing all hand the issue to a person, with the agent's reason and the failing checks in the comment.
|
|
274
|
+
- After a hand-over, that head's failing checks are not raised again. A new push starts the count afresh.
|
|
275
|
+
|
|
276
|
+
### Review modes
|
|
277
|
+
|
|
278
|
+
Each project's **Review** setting decides who is asked to review a pull request once it opens. Feedback from people and bots is handled as described above in every mode.
|
|
279
|
+
|
|
280
|
+
| Mode | When the pull request opens | Afterwards |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| No review (default) | Nothing. A person requests or does the review. | Whatever review arrives goes through the loop above. |
|
|
283
|
+
| Copilot review | Foreman requests a review from GitHub Copilot. If GitHub refuses, for example because Copilot review is not available for the repository, the comment on the issue says so. | Copilot's review is ordinary feedback, and Copilot is asked again after each round. |
|
|
284
|
+
| Another agent | An enabled agent other than the one that wrote the pull request reviews it. With no other agent enabled, nothing happens. | See below. |
|
|
285
|
+
| Any agent (prefers another) | As Another agent, but when no other agent can take it, the author's own agent reviews it in a fresh session, as a self-review. | See below. |
|
|
286
|
+
|
|
287
|
+
**Agent reviews.** The reviewing agent reads the diff, can run the project's tests, changes nothing, and posts one comment on the pull request (`gh pr review --comment`). It ends with a verdict:
|
|
288
|
+
|
|
289
|
+
- **approve**: the issue moves to the approval state, and a person approves and merges;
|
|
290
|
+
- **changes**: its findings are handed to the author's agent as the next round of feedback; once that agent pushes, the new head is reviewed again.
|
|
291
|
+
|
|
292
|
+
An agent review counts as a round toward "Rounds of feedback per issue", so two agents that keep disagreeing end with the issue handed to a person. The review loop must be on (rounds above 0) for agent reviews to happen. Each head is reviewed once: a review that fails, or ends without a verdict, is not repeated until something new is pushed. While an agent reviews, the issue stays in review in the tracker.
|
|
293
|
+
|
|
294
|
+
Both agents post as the same GitHub account, and GitHub does not let an account approve its own pull request, so an agent's approval lives in foreman (the issue's state), not in GitHub's review status. Branch protection that requires an approving review still needs a person.
|
|
295
|
+
|
|
296
|
+
**Tracker automations.** If the tracker also moves issues when pull requests change, the two will fight. In Linear, switch off the team's Git automations for "pull request opened" and "review requested" (Team settings → Workflow), or foreman's states will be overwritten.
|
|
297
|
+
|
|
298
|
+
## Agents
|
|
299
|
+
|
|
300
|
+
Foreman drives agent CLIs through adapters. At startup, and whenever you press **Check again** on the Agents tab, it looks for each CLI on the `PATH` and shows whether it is installed and which version.
|
|
301
|
+
|
|
302
|
+
**Sign-in check.** Before an agent is given an issue or a review round, foreman asks its CLI whether it is signed in. A signed-out agent takes no work: the issue is left where it is in the tracker, untouched, and the Agents tab and the project show "Not signed in". Foreman asks again a minute later, and the agent resumes by itself once signed in. Only Claude Code has this check (`claude auth status`); for the other agents, and whenever the answer cannot be read, foreman cannot tell, and a sign-in problem appears as a failed run.
|
|
303
|
+
|
|
304
|
+
| Agent | CLI | Status |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| Claude Code | `claude` | Verified end to end. Enabled by default. |
|
|
307
|
+
| Codex CLI | `codex` | Written from its documentation; not yet run against the real CLI. |
|
|
308
|
+
| opencode | `opencode` | Written from its documentation; not yet run against the real CLI. |
|
|
309
|
+
| Antigravity CLI | `agy` | Written from its documentation; not yet run against the real CLI. Needs 1.2.10 or later. |
|
|
310
|
+
|
|
311
|
+
Unverified adapters are labelled as such in the console and are disabled until you enable them.
|
|
312
|
+
|
|
313
|
+
Each agent has, on the Agents tab:
|
|
314
|
+
|
|
315
|
+
- **Enabled** and **Run at once**: how many issues it may work on at the same time (0 pauses it).
|
|
316
|
+
- **Its own permission settings**, in that CLI's terms: an allow-list for Claude Code, a sandbox mode for Codex, a permission config for opencode, and so on.
|
|
317
|
+
- **Binary**: a command name or absolute path, if it is not the default.
|
|
318
|
+
|
|
319
|
+
**Plan usage.** Claude Code reports how much of the account's subscription limits has been used, per limit window (5-hour and 7-day). Foreman shows the latest figures in the header and on the Agents tab, and uses them in two ways:
|
|
320
|
+
|
|
321
|
+
- **Pause above (% of plan)**: the agent takes no new issues once any window reaches this percentage (default 90). It resumes by itself when that window resets. Runs already in progress are not interrupted.
|
|
322
|
+
- **When spending paid overage**: pause (the default) or keep working.
|
|
323
|
+
|
|
324
|
+
Each run also shows an estimate of how much of each window it used. The figures are for the whole account, so anything else using the same subscription at the same time is included, and a run that overlapped another is marked as shared. The dollar cost shown for Claude Code is the list-price equivalent, not a charge on a subscription. This comes from an undocumented event in the CLI's output, so it may stop working with a future CLI version; the other agents do not report plan usage.
|
|
325
|
+
|
|
326
|
+
**Routing.** On each poll, an issue goes to the first enabled, installed agent with a free slot, in the order shown on the Agents tab; use the arrows to change the order. When every slot is taken, the remaining issues wait in the tracker for a later poll. The agent that ran an issue is recorded on the run.
|
|
327
|
+
|
|
328
|
+
**Record.** Each agent's card shows its record over every finished run: how many issues it took and how many of those opened a pull request, failed, needed input or were aborted; how many review rounds it handled; the median time per issue; and the list-price cost, where the CLI reports one. Use it to decide the order above. The counts cover all projects together and all time.
|
|
329
|
+
|
|
330
|
+
The unverified adapters default to the settings under which each CLI can commit, push and open a pull request, which for all three means running without a sandbox or with every permission granted. Read the notes on each agent's card before enabling it.
|
|
331
|
+
|
|
332
|
+
## Retrying a failed issue
|
|
333
|
+
|
|
334
|
+
1. Read the failure reason on the run in the console, or in the comment on the issue (the task file's log, for a Git branch project).
|
|
335
|
+
2. Inspect the kept worktree if useful, then remove it and its branch:
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
git -C <repository> worktree remove --force <worktree root>/<identifier>
|
|
339
|
+
git -C <repository> branch -D <branch>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
3. Move the issue back to the target state (for a Git branch project, see [Retrying](#git-branch) there). Foreman picks it up on the next poll.
|
|
343
|
+
|
|
344
|
+
Foreman refuses to start a run if the worktree directory or the branch already exists, so that a previous attempt is never overwritten. The exception is a review round: a worktree left by an earlier round that failed is reused when it is clean, on the pull request branch and not diverged from what was pushed. One that is not is left for you to look at, and the round fails until it is removed.
|
|
345
|
+
|
|
346
|
+
## Security
|
|
347
|
+
|
|
348
|
+
- **No authentication.** The console can start and stop the runner and change what the agent is allowed to run. It listens on `127.0.0.1` by default, and rejects requests addressed to other hostnames and cross-origin writes. Setting `HOST` to a non-loopback address removes the hostname check and exposes the console to that network; put it behind something that authenticates if you do.
|
|
349
|
+
- **Each agent's permission settings are its boundary.** Issue text is untrusted input to the agent. Claude Code's default allow-list includes `Bash(git:*)`, which permits any git command, including a force push; narrow it for repositories where that matters. The other agents' defaults are broader still (see [Agents](#agents)).
|
|
350
|
+
- **Tracker credentials are stored in plain text** in `<DATA_DIR>/foreman.db`, which foreman keeps readable by its owner only. The console can set or replace a key but never shows one. Anyone who can read that file can read the keys.
|
|
351
|
+
- **Agents do not get tracker credentials.** They are never placed in the agent's environment, and any `LINEAR_*` variable is removed from it. Everything else in foreman's environment is inherited by the agent, so do not keep unrelated secrets in `.env`.
|
|
352
|
+
- **Foreman pushes to the task branch and nothing else.** With the Git branch source, foreman commits and pushes to the task branch of the repository the project names, with the git credentials of the account it runs under; it never pushes to any other branch. An agent working on such a project has the same credentials and could reach the task branch too, so its prompt tells it to leave the task directory alone on every branch; the agent's permission settings remain the boundary.
|
|
353
|
+
|
|
354
|
+
## Limits
|
|
355
|
+
|
|
356
|
+
- Issues are taken by the tracker's priority (urgent first, "no priority" last), then oldest first. Concurrency is bounded by the agents' limits, up to 16 per agent.
|
|
357
|
+
- Runs in the same repository set up their worktrees one after another, then work in parallel.
|
|
358
|
+
- No retries of failed runs. A failed issue needs a person.
|
|
359
|
+
- At most 2000 issues in a project's target state are considered per poll.
|
|
360
|
+
- A worktree is kept after a successful run if the agent left uncommitted files in it; the run log says so.
|
|
361
|
+
- The agent runs headless, so it cannot wait on anything interactive. A run whose agent ends its turn without opening a pull request is reported as failed, unless the agent said it needs input (see [How a run works](#how-a-run-works)).
|
|
362
|
+
|
|
363
|
+
## Development
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
npm ci # not npm install, which rewrites the lockfile
|
|
367
|
+
npm run dev # foreman run from source, reloading on change, with data in ./data
|
|
368
|
+
npm test # unit tests (Vitest)
|
|
369
|
+
npm run typecheck
|
|
370
|
+
npm run lint
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`npm run dev` and `npm start` (the built version) keep their data, and read `.env`, in `./data`, apart from an installed foreman's.
|
|
374
|
+
|
|
375
|
+
**Releasing.** Describe each user-visible change under `## [Unreleased]` in `CHANGELOG.md` as it lands. To release, rename that section to the new version and date (`## [0.3.0] - 2026-11-02`), add a fresh `## [Unreleased]` above it, commit, then:
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
npm version 0.3.0 # sets package.json and package-lock.json, commits and tags v0.3.0
|
|
379
|
+
git push --follow-tags
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The tag starts `.github/workflows/release.yml`, which checks the tag matches `package.json`, runs the checks, publishes to npm and creates the GitHub release from that version's changelog section.
|
|
383
|
+
|
|
384
|
+
Tests use fakes for Linear, git and the agent CLIs (`test/fixtures/`); none of them touch the network.
|
|
385
|
+
|
|
386
|
+
To add a tracker, implement `SourceType` (`src/sources/types.ts`) in `src/sources/` and register it in `src/sources/registry.ts`. Its `create` and `check` receive the settings and a `SourceContext`, the project's repository path and foreman's data directory, which a tracker that lives elsewhere can ignore; a tracker that can carry review progress between machines also implements the optional `loadProgress` and `saveProgress`, otherwise the progress stays in SQLite. To add an agent, implement `AgentAdapter` (`src/agent/types.ts`) in `src/agent/adapters/` and register it in `src/agent/registry.ts`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { listSetting, textSetting, truncate } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Google Antigravity CLI (`agy`), the successor to Gemini CLI: `agy --output-format stream-json -p`.
|
|
4
|
+
*
|
|
5
|
+
* Written from the documentation; not yet run against the real CLI, which changes weekly. The
|
|
6
|
+
* prompt goes on the command line because no plain-text stdin mode is documented. Whether events
|
|
7
|
+
* name themselves with `type` or `event` was not confirmed, so both are accepted.
|
|
8
|
+
*/
|
|
9
|
+
export const agyAdapter = {
|
|
10
|
+
id: 'agy',
|
|
11
|
+
name: 'Antigravity CLI',
|
|
12
|
+
defaultBin: 'agy',
|
|
13
|
+
versionArgs: ['--version'],
|
|
14
|
+
verified: false,
|
|
15
|
+
notes: [
|
|
16
|
+
'Needs version 1.2.10 or later: earlier releases hung or printed nothing without a terminal, and left a daemon running.',
|
|
17
|
+
'In a headless run shell commands are denied unless allowed in ~/.gemini/antigravity-cli/settings.json, or all permissions are skipped. A denied command does not fail the run.',
|
|
18
|
+
'Signs in through the OS keyring, which may not be available to a background process; an API key in its settings is the alternative.',
|
|
19
|
+
],
|
|
20
|
+
fields: [
|
|
21
|
+
{
|
|
22
|
+
key: 'permissions',
|
|
23
|
+
label: 'Permissions',
|
|
24
|
+
type: 'select',
|
|
25
|
+
options: ['skip-all', 'settings-file'],
|
|
26
|
+
help: 'skip-all passes --dangerously-skip-permissions. settings-file relies on the allow-list in your agy settings.',
|
|
27
|
+
},
|
|
28
|
+
{ key: 'extraArgs', label: 'Extra arguments', type: 'list', help: 'Placed before -p, one argument per row (e.g. --sandbox).' },
|
|
29
|
+
],
|
|
30
|
+
defaultSettings: { permissions: 'skip-all', extraArgs: [] },
|
|
31
|
+
requiresResult: false,
|
|
32
|
+
buildCommand({ prompt, settings }) {
|
|
33
|
+
const args = ['--output-format', 'stream-json'];
|
|
34
|
+
if (textSetting(settings, 'permissions', 'skip-all') === 'skip-all')
|
|
35
|
+
args.push('--dangerously-skip-permissions');
|
|
36
|
+
args.push(...listSetting(settings, 'extraArgs'));
|
|
37
|
+
// -p takes the prompt as its value, so every other flag has to come first.
|
|
38
|
+
args.push('-p', prompt);
|
|
39
|
+
return { args };
|
|
40
|
+
},
|
|
41
|
+
createParser() {
|
|
42
|
+
let parsed = null;
|
|
43
|
+
const text = new Map();
|
|
44
|
+
return {
|
|
45
|
+
push(line) {
|
|
46
|
+
let event;
|
|
47
|
+
try {
|
|
48
|
+
event = JSON.parse(line);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [{ kind: 'foreman', text: `unparsed output: ${truncate(line)}` }];
|
|
52
|
+
}
|
|
53
|
+
const events = [];
|
|
54
|
+
const kind = event.type ?? event.event;
|
|
55
|
+
if (kind === 'step_update') {
|
|
56
|
+
const index = event.step_index ?? -1;
|
|
57
|
+
if (event.step_type === 'agent_response') {
|
|
58
|
+
// The response arrives as deltas; one log line per finished response is enough.
|
|
59
|
+
text.set(index, (text.get(index) ?? '') + (event.text_delta ?? ''));
|
|
60
|
+
if (event.state === 'DONE') {
|
|
61
|
+
events.push({ kind: 'assistant', text: text.get(index) ?? '' });
|
|
62
|
+
text.delete(index);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (event.step_type === 'tool') {
|
|
66
|
+
const name = event.tool_info?.name ?? event.tool_name ?? 'tool';
|
|
67
|
+
if (event.state === 'ACTIVE') {
|
|
68
|
+
events.push({ kind: 'tool_use', text: `${name} ${truncate(JSON.stringify(event.tool_info?.parameters ?? {}))}` });
|
|
69
|
+
}
|
|
70
|
+
else if (event.state === 'DONE') {
|
|
71
|
+
const failure = event.tool_info?.error;
|
|
72
|
+
const output = failure ? `${failure.type ?? 'error'}: ${failure.message ?? ''}` : event.tool_info?.output;
|
|
73
|
+
events.push({
|
|
74
|
+
kind: 'tool_result',
|
|
75
|
+
text: truncate(typeof output === 'string' ? output : JSON.stringify(output ?? 'done')),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (kind === 'result') {
|
|
81
|
+
const ok = event.status === 'SUCCESS';
|
|
82
|
+
parsed = {
|
|
83
|
+
isError: !ok,
|
|
84
|
+
resultText: typeof event.response === 'string' && event.response ? event.response : ok ? null : `Run ended with status ${event.status}`,
|
|
85
|
+
costUsd: null,
|
|
86
|
+
numTurns: typeof event.num_turns === 'number' ? event.num_turns : null,
|
|
87
|
+
};
|
|
88
|
+
if (!ok)
|
|
89
|
+
events.push({ kind: 'result', text: parsed.resultText ?? 'error' });
|
|
90
|
+
}
|
|
91
|
+
return events;
|
|
92
|
+
},
|
|
93
|
+
result: () => parsed,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|