@gevezex/gdt 0.2.0 → 0.3.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 +56 -26
- 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 +154 -3
- package/dist/config.js +4 -0
- package/dist/github.js +24 -0
- package/dist/init.js +92 -0
- package/dist/prompts.js +22 -1
- package/dist/steering.js +13 -1
- package/dist/supervisor.js +15 -0
- package/package.json +1 -1
- package/skill/SKILL.md +10 -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 `.gdt/config.toml` must
|
|
130
|
+
already work on your machine with the exact model id you set there. For each
|
|
131
|
+
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,22 @@ 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"
|
|
167
|
-
|
|
168
|
-
[roles.tester]
|
|
169
|
-
agent = "claude"
|
|
170
|
-
model = "claude-sonnet-5"
|
|
171
|
-
|
|
172
|
-
[roles.reviewer]
|
|
173
|
-
agent = "codex"
|
|
174
|
-
model = "gpt-5.6-luna"
|
|
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:
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 config, runs `gdt doctor` and installs the operator skill.
|
|
186
|
+
Without the three role options it only reports what it found (agents on `PATH`,
|
|
187
|
+
the terminal, the detected CI checks) so your agent can discuss the roles with
|
|
188
|
+
you first. It never overwrites an existing config without `--force`, and it
|
|
189
|
+
requires at least one required check unless you pass `--allow-no-required-checks`.
|
|
190
|
+
|
|
181
191
|
Then check your setup:
|
|
182
192
|
|
|
183
193
|
```bash
|
|
@@ -292,6 +302,7 @@ Every command supports `--help`; `status` and `wait` also support `--json`.
|
|
|
292
302
|
|
|
293
303
|
| Command | Effect |
|
|
294
304
|
|---|---|
|
|
305
|
+
| `gdt init` | Create `.gdt/config.toml` and install the operator skill |
|
|
295
306
|
| `gdt doctor` | Check tools, GitHub login, agents, herdr and `.gdt/config.toml` |
|
|
296
307
|
| `gdt check-issue <n>` | Validate an issue body against the contract |
|
|
297
308
|
| `gdt start <n>` | Preflight, start the supervisor and workers, return |
|
|
@@ -321,21 +332,40 @@ overrides (for example another model) and is not committed.
|
|
|
321
332
|
| `workflow.max_correction_rounds` | `2` | Correction rounds after round 0 |
|
|
322
333
|
| `workflow.terminal` | `"herdr"` | `"herdr"` or `"headless"` |
|
|
323
334
|
| `workflow.supervisor_pane` | `false` | herdr: also show the supervisor in a pane |
|
|
335
|
+
| `workflow.herdr_layout` | `tabs` | herdr: `"tabs"` (one tab per pane) or `"split"` (panes side by side in one tab) |
|
|
324
336
|
| `workflow.poll_seconds` | `30` | How often the supervisor reads GitHub |
|
|
325
337
|
| `contract.max_acceptance_criteria` | `8` | Maximum number of ACs per issue |
|
|
326
338
|
| `contract.extra_rules` | none | File with project rules added to every role prompt |
|
|
327
339
|
|
|
340
|
+
### Per-role rules
|
|
341
|
+
|
|
342
|
+
Each workflow role can have supplementary rules of its own, in the target repository:
|
|
343
|
+
|
|
344
|
+
| File | Role |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `.gdt/roles/developer.md` | developer |
|
|
347
|
+
| `.gdt/roles/tester.md` | tester |
|
|
348
|
+
| `.gdt/roles/reviewer.md` | reviewer |
|
|
349
|
+
|
|
350
|
+
When a file has content, gdt appends it to that role's prompt on every turn under a
|
|
351
|
+
`## Role rules` heading, after `## Project rules` when `contract.extra_rules` sets one.
|
|
352
|
+
An empty or missing file adds nothing and is not an error. This only adds: gdt's own role
|
|
353
|
+
files in the package are never replaced, so the protocol stays intact. `gdt init` creates
|
|
354
|
+
the three files empty so you can see where your rules go; commit them like
|
|
355
|
+
`.gdt/config.toml`.
|
|
356
|
+
|
|
328
357
|
How each agent CLI is invoked is documented in [docs/agents.md](docs/agents.md).
|
|
329
358
|
|
|
330
359
|
## Watching it: herdr or headless
|
|
331
360
|
|
|
332
361
|
```text
|
|
333
362
|
herdr workspace "gdt-251" headless
|
|
334
|
-
|
|
335
|
-
│ developer
|
|
336
|
-
│ opencode ·
|
|
337
|
-
│ RUNNING
|
|
338
|
-
|
|
363
|
+
┌────────────┐ ┌────────────┐ ┌────────────┐
|
|
364
|
+
│ developer │ │ tester │ │ reviewer │ detached processes,
|
|
365
|
+
│ opencode · │ │ claude · │ │ codex · │ one log file each in
|
|
366
|
+
│ RUNNING │ │ WAITING │ │ WAITING │ .git/gdt/issue-251/logs/
|
|
367
|
+
└────────────┘ └────────────┘ └────────────┘
|
|
368
|
+
one tab per role, labelled developer, tester and reviewer
|
|
339
369
|
supervisor runs detached → logs/supervisor.log
|
|
340
370
|
```
|
|
341
371
|
|
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
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,10 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { CONFIG_PATH, DEFAULT_LANGUAGE, DEFAULT_MAX_ACCEPTANCE_CRITERIA, loadConfig, ROLES } from "./config.js";
|
|
6
6
|
import { validateContract } from "./contract.js";
|
|
7
|
-
import { findRepository, runDoctor } from "./doctor.js";
|
|
8
|
-
import { issueBody } from "./github.js";
|
|
9
|
-
import {
|
|
7
|
+
import { findRepository, herdrPreflight, runDoctor } from "./doctor.js";
|
|
8
|
+
import { detectedChecks, issueBody } from "./github.js";
|
|
9
|
+
import { createRoleRulesFiles, parseRoleSpec, proposal, serializeConfig, writeConfig } from "./init.js";
|
|
10
|
+
import { loadLocale, shippedLanguages } from "./locale.js";
|
|
10
11
|
import { allowRound, answer, installSkill, pause, resume, setAgent, steer } from "./steering.js";
|
|
11
12
|
import { supervise } from "./supervisor.js";
|
|
12
13
|
import { work } from "./worker.js";
|
|
@@ -21,6 +22,7 @@ Usage:
|
|
|
21
22
|
gdt <command> [options]
|
|
22
23
|
|
|
23
24
|
Commands:
|
|
25
|
+
init Create .gdt/config.toml and install the operator skill
|
|
24
26
|
doctor Check tools, GitHub authentication and .gdt/config.toml
|
|
25
27
|
check-issue Validate an issue body against the issue contract
|
|
26
28
|
start Start the workflow for an issue in the background
|
|
@@ -47,6 +49,27 @@ Checks that git and gh are installed, gh is authenticated, and that
|
|
|
47
49
|
.gdt/config.toml (merged with .gdt/config.local.toml) is valid.
|
|
48
50
|
Exits with 1 when any finding has level "error".
|
|
49
51
|
`;
|
|
52
|
+
const INIT_HELP = `Usage: gdt init [--json]
|
|
53
|
+
gdt init --developer <agent>/<model> --tester <agent>/<model> --reviewer <agent>/<model>
|
|
54
|
+
[--language <lang>] [--terminal <herdr|headless>]
|
|
55
|
+
[--required-check <name>]... [--allow-no-required-checks] [--force]
|
|
56
|
+
|
|
57
|
+
Without the three role options, reports the supported agents, whether each is on
|
|
58
|
+
PATH, the usable terminal, the detected CI checks and the language, and writes
|
|
59
|
+
nothing. With them, writes .gdt/config.toml, runs "gdt doctor" and installs the
|
|
60
|
+
operator skill.
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--developer <agent>/<model> Agent and model for the developer role
|
|
64
|
+
--tester <agent>/<model> Agent and model for the tester role
|
|
65
|
+
--reviewer <agent>/<model> Agent and model for the reviewer role
|
|
66
|
+
--language <lang> Language for human-facing text (default en)
|
|
67
|
+
--terminal <herdr|headless> Terminal backend (default: herdr when usable)
|
|
68
|
+
--required-check <name> CI check that must pass; repeatable
|
|
69
|
+
--allow-no-required-checks Accept an empty required_checks list
|
|
70
|
+
--force Replace an existing .gdt/config.toml
|
|
71
|
+
--json Machine-readable proposal output
|
|
72
|
+
`;
|
|
50
73
|
const CHECK_ISSUE_HELP = `Usage: gdt check-issue <issue> [--json]
|
|
51
74
|
gdt check-issue --body-file <file> [--json]
|
|
52
75
|
|
|
@@ -150,6 +173,132 @@ function doctor(args, io) {
|
|
|
150
173
|
io.stdout(json ? `${JSON.stringify(report, null, 2)}\n` : formatFindings(report.findings));
|
|
151
174
|
return report.ok ? EXIT_OK : EXIT_FAILED;
|
|
152
175
|
}
|
|
176
|
+
function formatProposal(facts) {
|
|
177
|
+
const lines = ["agents:"];
|
|
178
|
+
for (const choice of facts.agents) {
|
|
179
|
+
lines.push(` ${choice.agent} ${choice.found ? "found" : "not found"} model: ${choice.model_format}, example: ${choice.example}`);
|
|
180
|
+
}
|
|
181
|
+
lines.push(`terminal: ${facts.terminal}`, `required_checks: ${facts.required_checks.length === 0 ? "none" : facts.required_checks.join(", ")}`, `language: ${facts.language}`, "", "Next: gdt init --developer <agent>/<model> --tester <agent>/<model> --reviewer <agent>/<model>");
|
|
182
|
+
return `${lines.join("\n")}\n`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `gdt init`: without role options, reports the proposal (AC-1); with all three, writes
|
|
186
|
+
* `.gdt/config.toml` (AC-2 to AC-5), then runs doctor and installs the skill (AC-6, AC-7).
|
|
187
|
+
*/
|
|
188
|
+
function initCommand(args, io) {
|
|
189
|
+
let developer;
|
|
190
|
+
let tester;
|
|
191
|
+
let reviewer;
|
|
192
|
+
let language;
|
|
193
|
+
let terminal;
|
|
194
|
+
const requiredChecks = [];
|
|
195
|
+
let allowNoRequiredChecks = false;
|
|
196
|
+
let force = false;
|
|
197
|
+
let json = false;
|
|
198
|
+
for (let i = 0; i < args.length; i++) {
|
|
199
|
+
const arg = args[i] ?? "";
|
|
200
|
+
if (arg === "--json")
|
|
201
|
+
json = true;
|
|
202
|
+
else if (arg === "--force")
|
|
203
|
+
force = true;
|
|
204
|
+
else if (arg === "--allow-no-required-checks")
|
|
205
|
+
allowNoRequiredChecks = true;
|
|
206
|
+
else if (arg === "--help" || arg === "-h") {
|
|
207
|
+
io.stdout(INIT_HELP);
|
|
208
|
+
return EXIT_OK;
|
|
209
|
+
}
|
|
210
|
+
else if (arg === "--developer" || arg === "--tester" || arg === "--reviewer" || arg === "--language" || arg === "--terminal" || arg === "--required-check") {
|
|
211
|
+
const given = args[++i];
|
|
212
|
+
if (given === undefined)
|
|
213
|
+
return usageError(io, `Missing value for "${arg}".`, "gdt init --help");
|
|
214
|
+
if (arg === "--developer")
|
|
215
|
+
developer = given;
|
|
216
|
+
else if (arg === "--tester")
|
|
217
|
+
tester = given;
|
|
218
|
+
else if (arg === "--reviewer")
|
|
219
|
+
reviewer = given;
|
|
220
|
+
else if (arg === "--language")
|
|
221
|
+
language = given;
|
|
222
|
+
else if (arg === "--terminal") {
|
|
223
|
+
if (given !== "herdr" && given !== "headless") {
|
|
224
|
+
return usageError(io, `Invalid terminal "${given}"; use herdr or headless.`, "gdt init --help");
|
|
225
|
+
}
|
|
226
|
+
terminal = given;
|
|
227
|
+
}
|
|
228
|
+
else
|
|
229
|
+
requiredChecks.push(given);
|
|
230
|
+
}
|
|
231
|
+
else if (arg.startsWith("-"))
|
|
232
|
+
return usageError(io, `Unknown option "${arg}" for "gdt init".`, "gdt init --help");
|
|
233
|
+
else
|
|
234
|
+
return usageError(io, `Unexpected argument "${arg}" for "gdt init".`, "gdt init --help");
|
|
235
|
+
}
|
|
236
|
+
const root = findRepository(io.cwd);
|
|
237
|
+
if (root === null) {
|
|
238
|
+
io.stderr(`${io.cwd} is not inside a Git repository. Run gdt from a checkout of the target repository.\n`);
|
|
239
|
+
return EXIT_FAILED;
|
|
240
|
+
}
|
|
241
|
+
const specs = { developer, tester, reviewer };
|
|
242
|
+
const given = ROLES.filter((role) => specs[role] !== undefined);
|
|
243
|
+
if (given.length === 0) {
|
|
244
|
+
const facts = proposal(root, io.env);
|
|
245
|
+
io.stdout(json ? `${JSON.stringify(facts, null, 2)}\n` : formatProposal(facts));
|
|
246
|
+
return EXIT_OK;
|
|
247
|
+
}
|
|
248
|
+
if (given.length < ROLES.length) {
|
|
249
|
+
const missing = ROLES.filter((role) => specs[role] === undefined)
|
|
250
|
+
.map((role) => `"--${role}"`)
|
|
251
|
+
.join(", ");
|
|
252
|
+
return usageError(io, `Missing ${missing} for "gdt init".`, "gdt init --help");
|
|
253
|
+
}
|
|
254
|
+
const roles = {};
|
|
255
|
+
for (const role of ROLES) {
|
|
256
|
+
const parsed = parseRoleSpec(specs[role] ?? "");
|
|
257
|
+
if ("error" in parsed) {
|
|
258
|
+
io.stderr(`${parsed.error}\n`);
|
|
259
|
+
return EXIT_FAILED;
|
|
260
|
+
}
|
|
261
|
+
roles[role] = parsed;
|
|
262
|
+
}
|
|
263
|
+
const languages = shippedLanguages();
|
|
264
|
+
if (language !== undefined && !languages.includes(language)) {
|
|
265
|
+
io.stderr(`No locale for language "${language}"; available languages: ${languages.join(", ")}\n`);
|
|
266
|
+
return EXIT_FAILED;
|
|
267
|
+
}
|
|
268
|
+
if (existsSync(join(root, CONFIG_PATH)) && !force) {
|
|
269
|
+
io.stderr(`${CONFIG_PATH} already exists; use --force to replace it\n`);
|
|
270
|
+
return EXIT_FAILED;
|
|
271
|
+
}
|
|
272
|
+
const resolvedTerminal = terminal ?? (herdrPreflight(io.env) === null ? "herdr" : "headless");
|
|
273
|
+
const checks = requiredChecks.length > 0 ? requiredChecks : detectedChecks(root, io.env);
|
|
274
|
+
if (checks.length === 0 && !allowNoRequiredChecks) {
|
|
275
|
+
io.stderr("No required checks detected; pass --required-check <name> for each check, or --allow-no-required-checks to accept none\n");
|
|
276
|
+
return EXIT_FAILED;
|
|
277
|
+
}
|
|
278
|
+
const text = serializeConfig({
|
|
279
|
+
roles,
|
|
280
|
+
language: language ?? DEFAULT_LANGUAGE,
|
|
281
|
+
terminal: resolvedTerminal,
|
|
282
|
+
requiredChecks: checks,
|
|
283
|
+
allowNoRequiredChecks,
|
|
284
|
+
});
|
|
285
|
+
const writeError = writeConfig(root, text);
|
|
286
|
+
if (writeError !== null) {
|
|
287
|
+
io.stderr(`${writeError}\n`);
|
|
288
|
+
return EXIT_FAILED;
|
|
289
|
+
}
|
|
290
|
+
// AC-4: the three supplementary role rules files; existing files are left unchanged.
|
|
291
|
+
for (const path of createRoleRulesFiles(root))
|
|
292
|
+
io.stdout(`created ${path}\n`);
|
|
293
|
+
// AC-7: report the new config with doctor, then install the skill; the config stays in place either way.
|
|
294
|
+
const report = runDoctor(root, io.env);
|
|
295
|
+
io.stdout(formatFindings(report.findings));
|
|
296
|
+
const installed = installSkill(io.env);
|
|
297
|
+
io.stdout(installed.stdout);
|
|
298
|
+
if (installed.stderr !== "")
|
|
299
|
+
io.stderr(installed.stderr);
|
|
300
|
+
return report.ok ? EXIT_OK : EXIT_FAILED;
|
|
301
|
+
}
|
|
153
302
|
function checkIssue(args, io) {
|
|
154
303
|
let json = false;
|
|
155
304
|
let issue;
|
|
@@ -412,6 +561,8 @@ export function run(argv, io) {
|
|
|
412
561
|
return EXIT_OK;
|
|
413
562
|
case "doctor":
|
|
414
563
|
return doctor(rest, io);
|
|
564
|
+
case "init":
|
|
565
|
+
return initCommand(rest, io);
|
|
415
566
|
case "check-issue":
|
|
416
567
|
return checkIssue(rest, io);
|
|
417
568
|
case "start":
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,8 @@ import { z } from "zod";
|
|
|
5
5
|
export const AGENTS = ["claude", "codex", "opencode", "mcode", "pi", "omp"];
|
|
6
6
|
export const ROLES = ["developer", "tester", "reviewer"];
|
|
7
7
|
export const TERMINALS = ["herdr", "headless"];
|
|
8
|
+
/** AC-2: how the herdr backend places the managed panes. */
|
|
9
|
+
export const HERDR_LAYOUTS = ["split", "tabs"];
|
|
8
10
|
export const CONFIG_PATH = ".gdt/config.toml";
|
|
9
11
|
export const LOCAL_CONFIG_PATH = ".gdt/config.local.toml";
|
|
10
12
|
export const DEFAULT_LANGUAGE = "en";
|
|
@@ -41,6 +43,8 @@ function configSchemaFor(testAgents) {
|
|
|
41
43
|
allow_no_required_checks: z.boolean().default(false),
|
|
42
44
|
terminal: z.enum(TERMINALS).default("herdr"),
|
|
43
45
|
supervisor_pane: z.boolean().default(false),
|
|
46
|
+
// Default `tabs`: one tab per managed pane. `split` keeps the panes in one tab.
|
|
47
|
+
herdr_layout: z.enum(HERDR_LAYOUTS).default("tabs"),
|
|
44
48
|
poll_seconds: z.number().positive().default(30),
|
|
45
49
|
handoff_checks: z.int().min(1).default(5),
|
|
46
50
|
}),
|
package/dist/github.js
CHANGED
|
@@ -65,6 +65,30 @@ export function repository(cwd, env) {
|
|
|
65
65
|
export function viewer(cwd, env) {
|
|
66
66
|
return ghJson(["api", "user"], cwd, env).login;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The distinct names of the check runs and commit statuses on the latest commit of the repository's
|
|
70
|
+
* default branch, sorted alphabetically. Returns `[]` when `gh` cannot read them (no remote, no
|
|
71
|
+
* authentication, no GitHub): `gdt init` then treats the repository as having no detected checks.
|
|
72
|
+
*/
|
|
73
|
+
export function detectedChecks(cwd, env) {
|
|
74
|
+
try {
|
|
75
|
+
const repo = ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"], cwd, env);
|
|
76
|
+
const name = typeof repo.nameWithOwner === "string" ? repo.nameWithOwner : null;
|
|
77
|
+
const branch = typeof repo.defaultBranchRef?.name === "string" ? repo.defaultBranchRef.name : null;
|
|
78
|
+
if (name === null || branch === null)
|
|
79
|
+
return [];
|
|
80
|
+
const runs = ghJson(["api", `repos/${name}/commits/${branch}/check-runs`], cwd, env);
|
|
81
|
+
const statuses = ghJson(["api", `repos/${name}/commits/${branch}/status`], cwd, env);
|
|
82
|
+
const names = [
|
|
83
|
+
...(runs.check_runs ?? []).map((run) => run.name),
|
|
84
|
+
...(statuses.statuses ?? []).map((status) => status.context),
|
|
85
|
+
].filter((value) => typeof value === "string" && value !== "");
|
|
86
|
+
return [...new Set(names)].sort();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
68
92
|
/** The issue body and the open pull requests that close the issue ("Closes #n"). */
|
|
69
93
|
export function issueSnapshot(issue, cwd, env) {
|
|
70
94
|
const data = ghJson(["issue", "view", String(issue), "--json", "body,closedByPullRequestsReferences"], cwd, env);
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { adapterFor, supportedAgents } from "./agents/index.js";
|
|
4
|
+
import { CONFIG_PATH, DEFAULT_LANGUAGE, ROLES } from "./config.js";
|
|
5
|
+
import { herdrPreflight, which } from "./doctor.js";
|
|
6
|
+
import { detectedChecks } from "./github.js";
|
|
7
|
+
import { ROLE_RULES_DIR, roleRulesPath } from "./prompts.js";
|
|
8
|
+
/** The proposal: one entry per supported agent, the usable terminal, detected checks and language. */
|
|
9
|
+
export function proposal(root, env) {
|
|
10
|
+
const agents = supportedAgents().flatMap((agent) => {
|
|
11
|
+
const adapter = adapterFor(agent);
|
|
12
|
+
if (adapter === undefined)
|
|
13
|
+
return [];
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
agent,
|
|
17
|
+
found: which(adapter.binary, env) !== null,
|
|
18
|
+
model_format: adapter.modelFormat,
|
|
19
|
+
example: adapter.modelExample,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
agents,
|
|
25
|
+
terminal: herdrPreflight(env) === null ? "herdr" : "headless",
|
|
26
|
+
required_checks: detectedChecks(root, env),
|
|
27
|
+
language: DEFAULT_LANGUAGE,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Parses `<agent>/<model>` at the first `/`, the same way `gdt set-agent` does. */
|
|
31
|
+
export function parseRoleSpec(spec) {
|
|
32
|
+
const slash = spec.indexOf("/");
|
|
33
|
+
const agent = slash === -1 ? spec : spec.slice(0, slash);
|
|
34
|
+
const model = slash === -1 ? "" : spec.slice(slash + 1);
|
|
35
|
+
const supported = supportedAgents();
|
|
36
|
+
if (!supported.includes(agent)) {
|
|
37
|
+
return { error: `Unsupported agent "${agent}"; supported agents: ${supported.join(", ")}` };
|
|
38
|
+
}
|
|
39
|
+
if (model.trim() === "")
|
|
40
|
+
return { error: `Missing model in "${spec}"; use <agent>/<model>` };
|
|
41
|
+
return { agent: agent, model };
|
|
42
|
+
}
|
|
43
|
+
/** The `.gdt/config.toml` text; a key that keeps its schema default is not written (AC-2). */
|
|
44
|
+
export function serializeConfig(config) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (config.language !== DEFAULT_LANGUAGE)
|
|
47
|
+
lines.push(`language = ${JSON.stringify(config.language)}`, "");
|
|
48
|
+
for (const role of ROLES) {
|
|
49
|
+
const { agent, model } = config.roles[role];
|
|
50
|
+
lines.push(`[roles.${role}]`, `agent = ${JSON.stringify(agent)}`, `model = ${JSON.stringify(model)}`, "");
|
|
51
|
+
}
|
|
52
|
+
lines.push("[workflow]", `required_checks = [${config.requiredChecks.map((name) => JSON.stringify(name)).join(", ")}]`);
|
|
53
|
+
if (config.allowNoRequiredChecks)
|
|
54
|
+
lines.push("allow_no_required_checks = true");
|
|
55
|
+
if (config.terminal !== "herdr")
|
|
56
|
+
lines.push(`terminal = ${JSON.stringify(config.terminal)}`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
/** Writes the config, creating `.gdt/` when needed; returns an error message, or null on success. */
|
|
61
|
+
export function writeConfig(root, text) {
|
|
62
|
+
try {
|
|
63
|
+
const path = join(root, CONFIG_PATH);
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
65
|
+
writeFileSync(path, text);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return `Cannot write ${CONFIG_PATH}: ${err instanceof Error ? err.message : String(err)}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* AC-4: creates each missing `.gdt/roles/<role>.md` as an empty file and returns its
|
|
74
|
+
* repository-relative path. An existing file is never touched, not even with `--force`.
|
|
75
|
+
*/
|
|
76
|
+
export function createRoleRulesFiles(root) {
|
|
77
|
+
const created = [];
|
|
78
|
+
for (const role of ROLES) {
|
|
79
|
+
const path = roleRulesPath(root, role);
|
|
80
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
81
|
+
try {
|
|
82
|
+
writeFileSync(path, "", { flag: "wx" });
|
|
83
|
+
created.push(`${ROLE_RULES_DIR}/${role}.md`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// Exists already: leave it byte-for-byte unchanged.
|
|
87
|
+
if (err.code !== "EEXIST")
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return created;
|
|
92
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { loadLocale, SECTION_KEYS } from "./locale.js";
|
|
5
5
|
import { marker, schemas } from "./protocol.js";
|
|
@@ -7,9 +7,26 @@ export const ROLE_FILES = ["developer", "tester", "reviewer", "issue-writer"];
|
|
|
7
7
|
/** The record kind each workflow role writes. */
|
|
8
8
|
export const RECORD_OF = { developer: "handoff", tester: "test", reviewer: "review" };
|
|
9
9
|
const ROLES_DIR = new URL("../roles/", import.meta.url);
|
|
10
|
+
/** The directory, relative to a target repository's root, holding the per-role supplementary rules. */
|
|
11
|
+
export const ROLE_RULES_DIR = ".gdt/roles";
|
|
10
12
|
export function roleFile(name) {
|
|
11
13
|
return readFileSync(new URL(`${name}.md`, ROLES_DIR), "utf8");
|
|
12
14
|
}
|
|
15
|
+
/** The absolute path of a workflow role's supplementary rules file in `root`. */
|
|
16
|
+
export function roleRulesPath(root, role) {
|
|
17
|
+
return join(root, ROLE_RULES_DIR, `${role}.md`);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The role's supplementary rules, trimmed, or `null` when the file is missing or empty (whitespace
|
|
21
|
+
* only). Read on every call, so a change reaches the role's next turn without a restart (AC-3).
|
|
22
|
+
*/
|
|
23
|
+
export function roleRules(root, role) {
|
|
24
|
+
const path = roleRulesPath(root, role);
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return null;
|
|
27
|
+
const text = readFileSync(path, "utf8").trimEnd();
|
|
28
|
+
return text.trim() === "" ? null : text;
|
|
29
|
+
}
|
|
13
30
|
/**
|
|
14
31
|
* Trusted directives for `role` posted after that role's previous dispatch, i.e. with a comment id
|
|
15
32
|
* above `afterCommentId` (the highest comment id seen at that dispatch; 0 before the first one).
|
|
@@ -76,5 +93,9 @@ export function buildPrompt(role, dispatch, project) {
|
|
|
76
93
|
if (existsSync(path))
|
|
77
94
|
parts.push(["## Project rules", "", readFileSync(path, "utf8").trimEnd()].join("\n"));
|
|
78
95
|
}
|
|
96
|
+
// AC-1/AC-2: a role's own supplementary rules, after the global project rules; empty adds nothing.
|
|
97
|
+
const rules = roleRules(project.root, role);
|
|
98
|
+
if (rules !== null)
|
|
99
|
+
parts.push(["## Role rules", "", rules].join("\n"));
|
|
79
100
|
return `${parts.join("\n\n")}\n`;
|
|
80
101
|
}
|
package/dist/steering.js
CHANGED
|
@@ -3,7 +3,8 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { adapterFor, supportedAgents } from "./agents/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { backendFor } from "./backends/index.js";
|
|
7
|
+
import { loadConfig, ROLES } from "./config.js";
|
|
7
8
|
import { findRepository, which } from "./doctor.js";
|
|
8
9
|
import { comments, issueSnapshot, postIssueComment, postPullRequestComment, repository } from "./github.js";
|
|
9
10
|
import { formatRecord, parseRecords } from "./protocol.js";
|
|
@@ -129,6 +130,17 @@ export function setAgent(issue, role, spec, cwd, env) {
|
|
|
129
130
|
if (readState(p) === null)
|
|
130
131
|
return fail(`No workflow for #${issue}. Next: gdt start ${issue}\n`);
|
|
131
132
|
writeJsonAtomic(p.overrides, { ...readOverrides(p), [checked]: { agent: agent, model } });
|
|
133
|
+
// AC-1: the agents overview must name the role's new agent. Best effort: the pane may not exist
|
|
134
|
+
// yet, the terminal may be headless, or herdr may be missing; the override still applies.
|
|
135
|
+
try {
|
|
136
|
+
const { report } = loadConfig(p.root, env);
|
|
137
|
+
if (report.valid && report.workflow.terminal === "herdr") {
|
|
138
|
+
backendFor(report, p.root, issue, env, p).setDisplayAgent(checked, `${checked} · ${agent}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// The override is written; the label is refreshed on the next `gdt start`.
|
|
143
|
+
}
|
|
132
144
|
return ok(`${checked} for #${issue} now uses ${agent} with model ${model} from its next turn.\n`);
|
|
133
145
|
}
|
|
134
146
|
/** `gdt install-skill`: copies `skill/SKILL.md` into every detected harness; idempotent. */
|
package/dist/supervisor.js
CHANGED
|
@@ -107,6 +107,18 @@ class Supervisor {
|
|
|
107
107
|
this.backend.setTitle(role, this.roleTitle(role, state));
|
|
108
108
|
this.reportState(role, ROLE_PANE_STATE[state]);
|
|
109
109
|
}
|
|
110
|
+
/** AC-1: sets the display-only agent label, for example `developer · opencode` or `gdt · supervisor`. */
|
|
111
|
+
setDisplay(name, label) {
|
|
112
|
+
try {
|
|
113
|
+
this.backend.setDisplayAgent(name, label);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (this.reportWarned)
|
|
117
|
+
return;
|
|
118
|
+
this.reportWarned = true;
|
|
119
|
+
log(`warning: herdr display label failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
110
122
|
/** Records a status; notifies once when entering a notifying status. */
|
|
111
123
|
setStatus(status, reason, extra = {}) {
|
|
112
124
|
if (this.state.status !== status || this.state.reason !== reason)
|
|
@@ -128,10 +140,13 @@ class Supervisor {
|
|
|
128
140
|
}
|
|
129
141
|
startWorkers() {
|
|
130
142
|
this.backend.ensureWorkspace();
|
|
143
|
+
// AC-1: the agents overview shows the role next to the agent name, once per `gdt start`.
|
|
144
|
+
this.setDisplay("supervisor", "gdt · supervisor");
|
|
131
145
|
this.backend.setTitle("supervisor", "supervisor · starting");
|
|
132
146
|
this.reportState("supervisor", supervisorAgentState(this.state.status));
|
|
133
147
|
for (const role of ROLES) {
|
|
134
148
|
this.setRoleState(role, "WAITING");
|
|
149
|
+
this.setDisplay(role, `${role} · ${this.agents[role]}`);
|
|
135
150
|
if (this.backend.alive(this.state.pids.workers[role] ?? -1))
|
|
136
151
|
continue;
|
|
137
152
|
this.state.pids.workers[role] = this.backend.spawnPane(role, [process.execPath, cliPath(), "_worker", String(this.issue), role]);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
You are the operator: the user talks to you in their own language, and you run
|
|
4
4
|
gdt for them. They should never have to memorise a gdt command.
|
|
5
5
|
|
|
6
|
+
## First-time setup
|
|
7
|
+
|
|
8
|
+
- With no `.gdt/config.toml`, run `gdt init --json`. It writes nothing and reports
|
|
9
|
+
the supported agents, which are on PATH, the terminal and the detected CI checks.
|
|
10
|
+
- Present that proposal to the user and ask which agent and model each role
|
|
11
|
+
(developer, tester, reviewer) uses. Do not pick models for them.
|
|
12
|
+
- Run `gdt init` with their choices, for example
|
|
13
|
+
`gdt init --developer opencode/deepseek/deepseek-v4-flash --tester claude/claude-sonnet-5 --reviewer codex/gpt-5.6-luna`.
|
|
14
|
+
It writes the config, runs `gdt doctor` and installs this skill.
|
|
15
|
+
|
|
6
16
|
## Start
|
|
7
17
|
|
|
8
18
|
- `gdt start <issue>` returns immediately; the supervisor keeps running on its own.
|