@gevezex/gdt 0.2.0 → 0.4.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 +89 -41
- package/dist/agents/claude.js +2 -0
- package/dist/agents/codex.js +2 -0
- package/dist/agents/mcode.js +2 -0
- package/dist/agents/omp.js +2 -0
- package/dist/agents/opencode.js +2 -0
- package/dist/agents/pi.js +2 -0
- package/dist/backends/headless.js +3 -0
- package/dist/backends/herdr.js +174 -19
- package/dist/backends/index.js +1 -0
- package/dist/cli.js +187 -8
- package/dist/config.js +188 -26
- package/dist/doctor.js +4 -4
- package/dist/github.js +24 -0
- package/dist/init.js +307 -0
- package/dist/prompts.js +22 -1
- package/dist/steering.js +13 -1
- package/dist/supervisor.js +15 -0
- package/dist/workflow.js +8 -5
- package/package.json +1 -1
- package/skill/SKILL.md +14 -0
package/README.md
CHANGED
|
@@ -121,8 +121,24 @@ evidence too.
|
|
|
121
121
|
| Node 24 LTS | runs gdt |
|
|
122
122
|
| `git` | the shared checkout the roles work in |
|
|
123
123
|
| `gh`, logged in (`gh auth login`) | issues, pull requests, comments, checks |
|
|
124
|
-
| at least one agent CLI | `claude`, `codex`, `opencode`, `mcode`, `pi` or `omp` |
|
|
125
|
-
| [herdr](https://herdr.dev) 0.9.1+
|
|
124
|
+
| at least one agent CLI | `claude`, `codex`, `opencode`, `mcode`, `pi` or `omp` (see below) |
|
|
125
|
+
| [herdr](https://herdr.dev) 0.9.1+ | the default way to watch the roles live, one tab per role; on machines without herdr set `workflow.terminal = "headless"` |
|
|
126
|
+
|
|
127
|
+
### Agent prerequisites
|
|
128
|
+
|
|
129
|
+
Before the first `gdt start`, every agent CLI you name in the user config
|
|
130
|
+
(`~/.config/gdt/config.toml`) must already work on your machine with the exact
|
|
131
|
+
model id you set there. For each role:
|
|
132
|
+
|
|
133
|
+
1. install the agent CLI,
|
|
134
|
+
2. log in or configure its API key or subscription,
|
|
135
|
+
3. run it once successfully with the model id from `roles.<role>.model` (for
|
|
136
|
+
example `opencode/deepseek/deepseek-v4-flash`).
|
|
137
|
+
|
|
138
|
+
gdt runs the agent CLI as is, so the agent uses its own login, tokens and
|
|
139
|
+
credits. gdt stores no credentials and does not log in, choose a plan, or track
|
|
140
|
+
token use or costs. `gdt doctor` only checks that the CLI is on `PATH`, not that
|
|
141
|
+
its login, API key or model works.
|
|
126
142
|
|
|
127
143
|
## Install
|
|
128
144
|
|
|
@@ -156,28 +172,25 @@ It copies `skill/SKILL.md` into the skill directory of every agent CLI it finds
|
|
|
156
172
|
|
|
157
173
|
### 1. Configure the target repository
|
|
158
174
|
|
|
159
|
-
In the repository you want gdt to work on,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
language = "en" # language for issue and PR text: "en" or "nl"
|
|
163
|
-
|
|
164
|
-
[roles.developer]
|
|
165
|
-
agent = "opencode"
|
|
166
|
-
model = "deepseek/deepseek-v4-flash"
|
|
175
|
+
In the repository you want gdt to work on, create `.gdt/config.toml` with
|
|
176
|
+
`gdt init` instead of writing TOML by hand. Ask your coding agent, or run it
|
|
177
|
+
yourself:
|
|
167
178
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
[roles.reviewer]
|
|
173
|
-
agent = "codex"
|
|
174
|
-
model = "gpt-5.6-luna"
|
|
175
|
-
|
|
176
|
-
[workflow]
|
|
177
|
-
required_checks = ["test"] # CI check names that must be green
|
|
178
|
-
terminal = "herdr" # or "headless"
|
|
179
|
+
```bash
|
|
180
|
+
gdt init --developer opencode/deepseek/deepseek-v4-flash \
|
|
181
|
+
--tester claude/claude-sonnet-5 \
|
|
182
|
+
--reviewer codex/gpt-5.6-luna
|
|
179
183
|
```
|
|
180
184
|
|
|
185
|
+
`gdt init` writes the roles to your user config (`~/.config/gdt/config.toml`,
|
|
186
|
+
or `$XDG_CONFIG_HOME/gdt/config.toml`), writes the project settings to
|
|
187
|
+
`.gdt/config.toml`, runs `gdt doctor` and installs the operator skill. Without the
|
|
188
|
+
three role options, it reuses the roles from your user config when they are all
|
|
189
|
+
there; otherwise it only reports what it found (agents on `PATH`, the terminal,
|
|
190
|
+
the detected CI checks) so your agent can discuss the roles with you first. It
|
|
191
|
+
never overwrites an existing config without `--force`, and it requires at least
|
|
192
|
+
one required check unless you pass `--allow-no-required-checks`.
|
|
193
|
+
|
|
181
194
|
Then check your setup:
|
|
182
195
|
|
|
183
196
|
```bash
|
|
@@ -292,6 +305,7 @@ Every command supports `--help`; `status` and `wait` also support `--json`.
|
|
|
292
305
|
|
|
293
306
|
| Command | Effect |
|
|
294
307
|
|---|---|
|
|
308
|
+
| `gdt init` | Write the roles to the user config and `.gdt/config.toml`, install the operator skill |
|
|
295
309
|
| `gdt doctor` | Check tools, GitHub login, agents, herdr and `.gdt/config.toml` |
|
|
296
310
|
| `gdt check-issue <n>` | Validate an issue body against the contract |
|
|
297
311
|
| `gdt start <n>` | Preflight, start the supervisor and workers, return |
|
|
@@ -308,22 +322,55 @@ Every command supports `--help`; `status` and `wait` also support `--json`.
|
|
|
308
322
|
|
|
309
323
|
## Configuration
|
|
310
324
|
|
|
311
|
-
|
|
312
|
-
|
|
325
|
+
Roles belong to a person, not to a repository, so gdt keeps them in a per-user
|
|
326
|
+
config. gdt loads three files and merges them in this order, with the later file
|
|
327
|
+
winning per key:
|
|
328
|
+
|
|
329
|
+
1. the **user config** — `$XDG_CONFIG_HOME/gdt/config.toml`, or
|
|
330
|
+
`~/.config/gdt/config.toml` when `XDG_CONFIG_HOME` is not set. It holds only
|
|
331
|
+
`[roles.*]`. A relative `XDG_CONFIG_HOME` is ignored.
|
|
332
|
+
2. the **repository config** — `.gdt/config.toml`, committed. It holds the project
|
|
333
|
+
settings and must not contain `[roles.*]`.
|
|
334
|
+
3. the **local config** — `.gdt/config.local.toml`, never committed. It overrides
|
|
335
|
+
any key, for example one role's model on this machine.
|
|
336
|
+
|
|
337
|
+
`gdt init` writes the roles to the user config and the project settings to
|
|
338
|
+
`.gdt/config.toml`. Ready-made files are in [`examples/`](examples/):
|
|
339
|
+
[`examples/user-config.toml`](examples/user-config.toml),
|
|
340
|
+
[`examples/config.toml`](examples/config.toml) and
|
|
341
|
+
[`examples/config.local.toml`](examples/config.local.toml).
|
|
342
|
+
|
|
343
|
+
| Key | File | Default | Meaning |
|
|
344
|
+
|---|---|---|---|
|
|
345
|
+
| `roles.<role>.agent` | user | required | `claude`, `codex`, `opencode`, `mcode`, `pi` or `omp` |
|
|
346
|
+
| `roles.<role>.model` | user | required | Model id for that agent |
|
|
347
|
+
| `language` | repository | `"en"` | Language of issue and PR text (`en`, `nl`) |
|
|
348
|
+
| `workflow.required_checks` | repository | required | CI checks that must be green before `ready_to_merge` |
|
|
349
|
+
| `workflow.allow_no_required_checks` | repository | `false` | Allow an empty `required_checks` list |
|
|
350
|
+
| `workflow.max_correction_rounds` | repository | `2` | Correction rounds after round 0 |
|
|
351
|
+
| `workflow.terminal` | repository | `"herdr"` | `"herdr"` or `"headless"` |
|
|
352
|
+
| `workflow.supervisor_pane` | repository | `false` | herdr: also show the supervisor in a pane |
|
|
353
|
+
| `workflow.herdr_layout` | repository | `tabs` | herdr: `"tabs"` (one tab per pane) or `"split"` (panes side by side in one tab) |
|
|
354
|
+
| `workflow.poll_seconds` | repository | `30` | How often the supervisor reads GitHub |
|
|
355
|
+
| `contract.max_acceptance_criteria` | repository | `8` | Maximum number of ACs per issue |
|
|
356
|
+
| `contract.extra_rules` | repository | none | File with project rules added to every role prompt |
|
|
357
|
+
|
|
358
|
+
### Per-role rules
|
|
359
|
+
|
|
360
|
+
Each workflow role can have supplementary rules of its own, in the target repository:
|
|
361
|
+
|
|
362
|
+
| File | Role |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `.gdt/roles/developer.md` | developer |
|
|
365
|
+
| `.gdt/roles/tester.md` | tester |
|
|
366
|
+
| `.gdt/roles/reviewer.md` | reviewer |
|
|
313
367
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
| `workflow.allow_no_required_checks` | `false` | Allow an empty `required_checks` list |
|
|
321
|
-
| `workflow.max_correction_rounds` | `2` | Correction rounds after round 0 |
|
|
322
|
-
| `workflow.terminal` | `"herdr"` | `"herdr"` or `"headless"` |
|
|
323
|
-
| `workflow.supervisor_pane` | `false` | herdr: also show the supervisor in a pane |
|
|
324
|
-
| `workflow.poll_seconds` | `30` | How often the supervisor reads GitHub |
|
|
325
|
-
| `contract.max_acceptance_criteria` | `8` | Maximum number of ACs per issue |
|
|
326
|
-
| `contract.extra_rules` | none | File with project rules added to every role prompt |
|
|
368
|
+
When a file has content, gdt appends it to that role's prompt on every turn under a
|
|
369
|
+
`## Role rules` heading, after `## Project rules` when `contract.extra_rules` sets one.
|
|
370
|
+
An empty or missing file adds nothing and is not an error. This only adds: gdt's own role
|
|
371
|
+
files in the package are never replaced, so the protocol stays intact. `gdt init` creates
|
|
372
|
+
the three files empty so you can see where your rules go; commit them like
|
|
373
|
+
`.gdt/config.toml`.
|
|
327
374
|
|
|
328
375
|
How each agent CLI is invoked is documented in [docs/agents.md](docs/agents.md).
|
|
329
376
|
|
|
@@ -331,11 +378,12 @@ How each agent CLI is invoked is documented in [docs/agents.md](docs/agents.md).
|
|
|
331
378
|
|
|
332
379
|
```text
|
|
333
380
|
herdr workspace "gdt-251" headless
|
|
334
|
-
|
|
335
|
-
│ developer
|
|
336
|
-
│ opencode ·
|
|
337
|
-
│ RUNNING
|
|
338
|
-
|
|
381
|
+
┌────────────┐ ┌────────────┐ ┌────────────┐
|
|
382
|
+
│ developer │ │ tester │ │ reviewer │ detached processes,
|
|
383
|
+
│ opencode · │ │ claude · │ │ codex · │ one log file each in
|
|
384
|
+
│ RUNNING │ │ WAITING │ │ WAITING │ .git/gdt/issue-251/logs/
|
|
385
|
+
└────────────┘ └────────────┘ └────────────┘
|
|
386
|
+
one tab per role, labelled developer, tester and reviewer
|
|
339
387
|
supervisor runs detached → logs/supervisor.log
|
|
340
388
|
```
|
|
341
389
|
|
package/dist/agents/claude.js
CHANGED
|
@@ -3,6 +3,8 @@ export const claude = {
|
|
|
3
3
|
binary: "claude",
|
|
4
4
|
title: "Claude Code",
|
|
5
5
|
install: "Install Claude Code: https://docs.anthropic.com/en/docs/claude-code",
|
|
6
|
+
modelFormat: "<model>",
|
|
7
|
+
modelExample: "claude-sonnet-5",
|
|
6
8
|
buildInvocation: (_role, model, promptFile) => ({
|
|
7
9
|
argv: ["claude", "-p", "--model", model, "--permission-mode", "bypassPermissions", "--no-session-persistence"],
|
|
8
10
|
env: {},
|
package/dist/agents/codex.js
CHANGED
|
@@ -3,6 +3,8 @@ export const codex = {
|
|
|
3
3
|
binary: "codex",
|
|
4
4
|
title: "Codex",
|
|
5
5
|
install: "Install Codex: npm i -g @openai/codex",
|
|
6
|
+
modelFormat: "<model>",
|
|
7
|
+
modelExample: "gpt-5.6-luna",
|
|
6
8
|
buildInvocation: (_role, model, promptFile, cwd) => ({
|
|
7
9
|
argv: ["codex", "exec", "--model", model, "--cd", cwd, "--dangerously-bypass-approvals-and-sandbox", "--ephemeral", "-"],
|
|
8
10
|
env: {},
|
package/dist/agents/mcode.js
CHANGED
|
@@ -4,6 +4,8 @@ export const mcode = {
|
|
|
4
4
|
binary: "mcode",
|
|
5
5
|
title: "MCode",
|
|
6
6
|
install: "Install MiniMax Code: npm i -g @minimax-ai/code",
|
|
7
|
+
modelFormat: "provider/model",
|
|
8
|
+
modelExample: "minimax/MiniMax-M3",
|
|
7
9
|
buildInvocation: (_role, model, promptFile, cwd) => ({
|
|
8
10
|
argv: ["mcode", "exec", "--model", model, "--cwd", cwd, "--permission", "full", "--input", "-"],
|
|
9
11
|
env: {},
|
package/dist/agents/omp.js
CHANGED
|
@@ -4,6 +4,8 @@ export const omp = {
|
|
|
4
4
|
binary: "omp",
|
|
5
5
|
title: "omp",
|
|
6
6
|
install: "Install omp: https://omp.sh",
|
|
7
|
+
modelFormat: "provider/id",
|
|
8
|
+
modelExample: "openai/gpt-5.2",
|
|
7
9
|
buildInvocation: (_role, model, promptFile) => ({
|
|
8
10
|
argv: ["omp", "--print", "--model", model, "--no-session", "--auto-approve"],
|
|
9
11
|
env: {},
|
package/dist/agents/opencode.js
CHANGED
|
@@ -5,6 +5,8 @@ export const opencode = {
|
|
|
5
5
|
binary: "opencode",
|
|
6
6
|
title: "OpenCode",
|
|
7
7
|
install: "Install OpenCode: https://opencode.ai",
|
|
8
|
+
modelFormat: "provider/model",
|
|
9
|
+
modelExample: "deepseek/deepseek-v4-flash",
|
|
8
10
|
buildInvocation: (_role, model, promptFile) => ({
|
|
9
11
|
argv: ["opencode", "run", "--model", model, "--auto", "--file", promptFile, OPENCODE_MESSAGE],
|
|
10
12
|
env: {},
|
package/dist/agents/pi.js
CHANGED
|
@@ -4,6 +4,8 @@ export const pi = {
|
|
|
4
4
|
binary: "pi",
|
|
5
5
|
title: "pi",
|
|
6
6
|
install: "Install pi: npm i -g --ignore-scripts @earendil-works/pi-coding-agent",
|
|
7
|
+
modelFormat: "provider/id",
|
|
8
|
+
modelExample: "anthropic/claude-sonnet-4",
|
|
7
9
|
buildInvocation: (_role, model, promptFile) => ({
|
|
8
10
|
argv: ["pi", "--print", "--model", model, "--no-session", "--no-approve"],
|
|
9
11
|
env: {},
|
|
@@ -37,6 +37,9 @@ export function headless(logs, cwd, env) {
|
|
|
37
37
|
setTitle: () => {
|
|
38
38
|
// Headless panes have no title.
|
|
39
39
|
},
|
|
40
|
+
setDisplayAgent: () => {
|
|
41
|
+
// Headless panes have no agents overview.
|
|
42
|
+
},
|
|
40
43
|
reportState: () => {
|
|
41
44
|
// AC-7: headless mode reports no agent state.
|
|
42
45
|
},
|
package/dist/backends/herdr.js
CHANGED
|
@@ -45,6 +45,19 @@ function reportAgent(opts, paneId, label, state) {
|
|
|
45
45
|
throw new Error(`herdr pane report-agent: ${reason}`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* AC-1: sets the display-only agent label of a pane. herdr 0.9.1 requires the pane id before the
|
|
50
|
+
* options here (`herdr pane report-metadata <pane> --source gdt --display-agent <label>`); with the
|
|
51
|
+
* options first it exits 2 with `unknown option: gdt`.
|
|
52
|
+
*/
|
|
53
|
+
function reportDisplayAgent(opts, paneId, label) {
|
|
54
|
+
const args = ["pane", "report-metadata", paneId, "--source", "gdt", "--display-agent", label];
|
|
55
|
+
const result = spawnSync(bin(opts.env), args, { cwd: opts.root, env: opts.env, encoding: "utf8" });
|
|
56
|
+
if (result.status !== 0) {
|
|
57
|
+
const reason = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `herdr exited with ${result.status ?? result.signal}`;
|
|
58
|
+
throw new Error(`herdr pane report-metadata: ${reason}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
48
61
|
/** Parses the `{ "result": ... }` envelope; a herdr `error` becomes a thrown Error. */
|
|
49
62
|
function resultOf(out, command) {
|
|
50
63
|
let data;
|
|
@@ -69,12 +82,59 @@ function workspaceList(opts) {
|
|
|
69
82
|
return [];
|
|
70
83
|
return list.filter((w) => isRecord(w) && typeof w.workspace_id === "string");
|
|
71
84
|
}
|
|
72
|
-
|
|
85
|
+
/** `pane list --workspace`, with the tab each pane currently lives in. */
|
|
86
|
+
function paneLocations(opts, workspaceId) {
|
|
73
87
|
const result = resultOf(call(["pane", "list", "--workspace", workspaceId], opts), "pane list");
|
|
74
88
|
const list = result.panes;
|
|
75
89
|
if (!Array.isArray(list))
|
|
76
90
|
return [];
|
|
77
|
-
return list.flatMap((p) =>
|
|
91
|
+
return list.flatMap((p) => isRecord(p) && typeof p.pane_id === "string"
|
|
92
|
+
? [{ pane_id: p.pane_id, ...(typeof p.tab_id === "string" ? { tab_id: p.tab_id } : {}) }]
|
|
93
|
+
: []);
|
|
94
|
+
}
|
|
95
|
+
/** `tab list --workspace`, in herdr's order. */
|
|
96
|
+
function tabLocations(opts, workspaceId) {
|
|
97
|
+
const result = resultOf(call(["tab", "list", "--workspace", workspaceId], opts), "tab list");
|
|
98
|
+
const list = result.tabs;
|
|
99
|
+
if (!Array.isArray(list))
|
|
100
|
+
return [];
|
|
101
|
+
return list.flatMap((tab) => isRecord(tab) && typeof tab.tab_id === "string"
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
tab_id: tab.tab_id,
|
|
105
|
+
label: typeof tab.label === "string" ? tab.label : "",
|
|
106
|
+
pane_count: typeof tab.pane_count === "number" ? tab.pane_count : 0,
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
: []);
|
|
110
|
+
}
|
|
111
|
+
/** AC-3: creates a tab labelled with a pane name; its root pane becomes the managed pane. */
|
|
112
|
+
function createTab(opts, workspaceId, label) {
|
|
113
|
+
const args = ["tab", "create", "--workspace", workspaceId, "--cwd", opts.root, "--label", label, "--no-focus"];
|
|
114
|
+
const result = resultOf(call(args, opts), "tab create");
|
|
115
|
+
const pane = result.root_pane;
|
|
116
|
+
if (!isRecord(pane) || typeof pane.pane_id !== "string")
|
|
117
|
+
throw new Error("herdr tab create did not return a root pane");
|
|
118
|
+
return pane.pane_id;
|
|
119
|
+
}
|
|
120
|
+
/** AC-5: moves an existing pane into a tab of its own, labelled with the pane name. */
|
|
121
|
+
function movePaneToNewTab(opts, pane, label) {
|
|
122
|
+
call(["pane", "move", pane, "--new-tab", "--label", label, "--no-focus"], opts);
|
|
123
|
+
}
|
|
124
|
+
/** AC-5: moves an existing pane next to `anchor` in `tab`, keeping `ratio` of the anchor's width. */
|
|
125
|
+
function movePaneIntoTab(opts, pane, tab, anchor, ratio) {
|
|
126
|
+
const args = ["pane", "move", pane, "--tab", tab, "--split", "right", "--target-pane", anchor, "--ratio", ratio.toFixed(4), "--no-focus"];
|
|
127
|
+
call(args, opts);
|
|
128
|
+
}
|
|
129
|
+
function renameTab(opts, tab, label) {
|
|
130
|
+
call(["tab", "rename", tab, label], opts);
|
|
131
|
+
}
|
|
132
|
+
/** AC-5: a tab that lost its last pane during a move must not remain. */
|
|
133
|
+
function closeEmptyTabs(opts, workspaceId, keep) {
|
|
134
|
+
for (const tab of tabLocations(opts, workspaceId)) {
|
|
135
|
+
if (tab.pane_count === 0 && !keep.has(tab.tab_id))
|
|
136
|
+
call(["tab", "close", tab.tab_id], opts);
|
|
137
|
+
}
|
|
78
138
|
}
|
|
79
139
|
function createWorkspace(opts) {
|
|
80
140
|
const label = `gdt-${opts.issue}`;
|
|
@@ -110,6 +170,68 @@ function writePanes(file, state) {
|
|
|
110
170
|
function roleTitle(role, agent, state) {
|
|
111
171
|
return role === "supervisor" ? `supervisor · ${state}` : `${role} · ${agent} · ${state}`;
|
|
112
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* AC-4/AC-5: all managed panes in one tab, left to right in `names` order. This is a no-op when the
|
|
175
|
+
* panes already share a tab, so a workspace created in split layout is left exactly as before.
|
|
176
|
+
*/
|
|
177
|
+
function arrangeSplit(opts, workspaceId, names, mapping, paneTab) {
|
|
178
|
+
const paneOf = (name) => mapping[name]?.pane_id;
|
|
179
|
+
const first = names[0];
|
|
180
|
+
if (first === undefined)
|
|
181
|
+
return;
|
|
182
|
+
const targetPane = paneOf(first);
|
|
183
|
+
if (targetPane === undefined)
|
|
184
|
+
return;
|
|
185
|
+
const tabs = new Set(names.flatMap((name) => {
|
|
186
|
+
const pane = paneOf(name);
|
|
187
|
+
if (pane === undefined)
|
|
188
|
+
return [];
|
|
189
|
+
const tab = paneTab.get(pane);
|
|
190
|
+
return tab === undefined ? [] : [tab];
|
|
191
|
+
}));
|
|
192
|
+
if (tabs.size <= 1)
|
|
193
|
+
return;
|
|
194
|
+
const target = paneTab.get(targetPane);
|
|
195
|
+
if (target === undefined)
|
|
196
|
+
return;
|
|
197
|
+
let anchor = targetPane;
|
|
198
|
+
names.slice(1).forEach((name, index) => {
|
|
199
|
+
const pane = paneOf(name);
|
|
200
|
+
if (pane === undefined)
|
|
201
|
+
return;
|
|
202
|
+
if (paneTab.get(pane) !== target)
|
|
203
|
+
movePaneIntoTab(opts, pane, target, anchor, 1 / (names.length - (index + 1) + 1));
|
|
204
|
+
anchor = pane;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** AC-3/AC-5: one tab per managed pane, labelled with the pane name, in `names` order. */
|
|
208
|
+
function arrangeTabs(opts, workspaceId, names, mapping) {
|
|
209
|
+
const tabByPane = new Map();
|
|
210
|
+
const countByTab = new Map();
|
|
211
|
+
for (const pane of paneLocations(opts, workspaceId)) {
|
|
212
|
+
if (pane.tab_id === undefined)
|
|
213
|
+
continue;
|
|
214
|
+
tabByPane.set(pane.pane_id, pane.tab_id);
|
|
215
|
+
countByTab.set(pane.tab_id, (countByTab.get(pane.tab_id) ?? 0) + 1);
|
|
216
|
+
}
|
|
217
|
+
const labels = new Map(tabLocations(opts, workspaceId).map((tab) => [tab.tab_id, tab.label]));
|
|
218
|
+
const kept = new Set();
|
|
219
|
+
for (const name of names) {
|
|
220
|
+
const pane = mapping[name]?.pane_id;
|
|
221
|
+
if (pane === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
const tab = tabByPane.get(pane);
|
|
224
|
+
// Keep a pane that is already alone in a tab of its own; only relabel that tab when needed.
|
|
225
|
+
if (tab !== undefined && !kept.has(tab) && countByTab.get(tab) === 1) {
|
|
226
|
+
kept.add(tab);
|
|
227
|
+
if (labels.get(tab) !== name)
|
|
228
|
+
renameTab(opts, tab, name);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
movePaneToNewTab(opts, pane, name);
|
|
232
|
+
}
|
|
233
|
+
closeEmptyTabs(opts, workspaceId, kept);
|
|
234
|
+
}
|
|
113
235
|
/**
|
|
114
236
|
* One herdr workspace per issue, with four panes for supervisor, developer, tester and reviewer.
|
|
115
237
|
* Panes are reused across `start` calls; their ids live in `panes.json` so a restarted process can
|
|
@@ -145,7 +267,12 @@ export function herdr(opts) {
|
|
|
145
267
|
else
|
|
146
268
|
workspaceId = createWorkspace(opts).workspace_id;
|
|
147
269
|
}
|
|
148
|
-
const
|
|
270
|
+
const locations = paneLocations(opts, workspaceId);
|
|
271
|
+
const live = new Set(locations.map((pane) => pane.pane_id));
|
|
272
|
+
const paneTab = new Map();
|
|
273
|
+
for (const pane of locations)
|
|
274
|
+
if (pane.tab_id !== undefined)
|
|
275
|
+
paneTab.set(pane.pane_id, pane.tab_id);
|
|
149
276
|
// AC-4: a supervisor pane from an earlier run (while `supervisor_pane` was true) is closed; its
|
|
150
277
|
// entry never enters the new panes.json, so no role can adopt the pane.
|
|
151
278
|
if (!opts.supervisorPane && known?.workspace_id === workspaceId) {
|
|
@@ -153,6 +280,7 @@ export function herdr(opts) {
|
|
|
153
280
|
if (leftover !== undefined && live.has(leftover.pane_id)) {
|
|
154
281
|
call(["pane", "close", leftover.pane_id], opts);
|
|
155
282
|
live.delete(leftover.pane_id);
|
|
283
|
+
paneTab.delete(leftover.pane_id);
|
|
156
284
|
}
|
|
157
285
|
}
|
|
158
286
|
const names = paneNames(opts);
|
|
@@ -172,23 +300,41 @@ export function herdr(opts) {
|
|
|
172
300
|
mapping[name] = { pane_id: reuse };
|
|
173
301
|
}
|
|
174
302
|
}
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const record = mapping[name];
|
|
182
|
-
if (record !== undefined) {
|
|
183
|
-
previous = record.pane_id;
|
|
184
|
-
return;
|
|
303
|
+
// Provide a pane for every name that still lacks one, in `names` order.
|
|
304
|
+
if (opts.layout === "tabs") {
|
|
305
|
+
// AC-3: each missing pane gets its own tab, labelled with the pane name.
|
|
306
|
+
for (const name of names) {
|
|
307
|
+
if (mapping[name] === undefined)
|
|
308
|
+
mapping[name] = { pane_id: createTab(opts, workspaceId, name) };
|
|
185
309
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// AC-4: each new pane is split off the right of the previous one, so panes run left to right
|
|
313
|
+
// in `names` order. The anchor keeps 1/(panes still to fill), which makes a new workspace's
|
|
314
|
+
// panes equally wide.
|
|
315
|
+
const first = Object.values(mapping).flatMap((p) => (p === undefined ? [] : [p.pane_id])).find((id) => id !== "");
|
|
316
|
+
let previous;
|
|
317
|
+
names.forEach((name, index) => {
|
|
318
|
+
const record = mapping[name];
|
|
319
|
+
if (record !== undefined) {
|
|
320
|
+
previous = record.pane_id;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const anchor = previous ?? first;
|
|
324
|
+
if (anchor === undefined)
|
|
325
|
+
throw new Error(`herdr workspace ${workspaceId} has no pane to split`);
|
|
326
|
+
previous = splitPane(opts, anchor, 1 / (names.length - index + 1));
|
|
327
|
+
mapping[name] = { pane_id: previous };
|
|
328
|
+
const anchorTab = paneTab.get(anchor);
|
|
329
|
+
if (anchorTab !== undefined)
|
|
330
|
+
paneTab.set(previous, anchorTab);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// AC-3/AC-5: place the panes according to the configured layout.
|
|
334
|
+
if (opts.layout === "tabs")
|
|
335
|
+
arrangeTabs(opts, workspaceId, names, mapping);
|
|
336
|
+
else
|
|
337
|
+
arrangeSplit(opts, workspaceId, names, mapping, paneTab);
|
|
192
338
|
writePanes(opts.panesFile, { workspace_id: workspaceId, panes: mapping });
|
|
193
339
|
if (opts.supervisorPane)
|
|
194
340
|
rename("supervisor", roleTitle("supervisor", "", "starting"));
|
|
@@ -223,6 +369,15 @@ export function herdr(opts) {
|
|
|
223
369
|
return;
|
|
224
370
|
rename(name, title);
|
|
225
371
|
},
|
|
372
|
+
setDisplayAgent(name, label) {
|
|
373
|
+
// Without a supervisor pane there is no supervisor label to set.
|
|
374
|
+
if (name === "supervisor" && !opts.supervisorPane)
|
|
375
|
+
return;
|
|
376
|
+
const paneId = findPane(name);
|
|
377
|
+
if (paneId === null)
|
|
378
|
+
return;
|
|
379
|
+
reportDisplayAgent(opts, paneId, label);
|
|
380
|
+
},
|
|
226
381
|
reportState(name, state) {
|
|
227
382
|
// AC-6: without a supervisor pane no supervisor state is reported to herdr.
|
|
228
383
|
if (name === "supervisor" && !opts.supervisorPane)
|
package/dist/backends/index.js
CHANGED