@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/dist/init.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, 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, userConfigPath } 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 without roles; a key that keeps its schema default is not written. */
|
|
44
|
+
export function serializeConfig(config) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (config.language !== DEFAULT_LANGUAGE)
|
|
47
|
+
lines.push(`language = ${JSON.stringify(config.language)}`, "");
|
|
48
|
+
lines.push("[workflow]", `required_checks = [${config.requiredChecks.map((name) => JSON.stringify(name)).join(", ")}]`);
|
|
49
|
+
if (config.allowNoRequiredChecks)
|
|
50
|
+
lines.push("allow_no_required_checks = true");
|
|
51
|
+
if (config.terminal !== "herdr")
|
|
52
|
+
lines.push(`terminal = ${JSON.stringify(config.terminal)}`);
|
|
53
|
+
lines.push("");
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function roleTable(role, spec) {
|
|
57
|
+
return `[roles.${role}]\nagent = ${JSON.stringify(spec.agent)}\nmodel = ${JSON.stringify(spec.model)}\n`;
|
|
58
|
+
}
|
|
59
|
+
/** The user config text for `roles`, used when the file does not exist yet. */
|
|
60
|
+
export function serializeUserConfig(roles) {
|
|
61
|
+
return ROLES.map((role) => roleTable(role, roles[role])).join("\n");
|
|
62
|
+
}
|
|
63
|
+
/** Strips the surrounding quotes of one TOML key segment. */
|
|
64
|
+
function unquoteKey(segment) {
|
|
65
|
+
if (segment.length >= 2 && segment.startsWith('"') && segment.endsWith('"')) {
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(segment);
|
|
68
|
+
if (typeof value === "string")
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Not JSON-compatible; fall back to a plain strip.
|
|
73
|
+
}
|
|
74
|
+
return segment.slice(1, -1);
|
|
75
|
+
}
|
|
76
|
+
if (segment.length >= 2 && segment.startsWith("'") && segment.endsWith("'"))
|
|
77
|
+
return segment.slice(1, -1);
|
|
78
|
+
return segment;
|
|
79
|
+
}
|
|
80
|
+
/** Splits a TOML dotted key into its segments, honoring quoted segments (e.g. `roles."a.b"`). */
|
|
81
|
+
function keySegments(key) {
|
|
82
|
+
const segments = [];
|
|
83
|
+
let current = "";
|
|
84
|
+
let quote = null;
|
|
85
|
+
for (let i = 0; i < key.length; i += 1) {
|
|
86
|
+
const ch = key[i] ?? "";
|
|
87
|
+
if (quote === '"') {
|
|
88
|
+
current += ch;
|
|
89
|
+
if (ch === "\\") {
|
|
90
|
+
current += key[i + 1] ?? "";
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
else if (ch === '"')
|
|
94
|
+
quote = null;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (quote === "'") {
|
|
98
|
+
current += ch;
|
|
99
|
+
if (ch === "'")
|
|
100
|
+
quote = null;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"' || ch === "'") {
|
|
104
|
+
quote = ch;
|
|
105
|
+
current += ch;
|
|
106
|
+
}
|
|
107
|
+
else if (ch === ".") {
|
|
108
|
+
segments.push(unquoteKey(current.trim()));
|
|
109
|
+
current = "";
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
current += ch;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
segments.push(unquoteKey(current.trim()));
|
|
116
|
+
return segments;
|
|
117
|
+
}
|
|
118
|
+
const HEADER_RE = /^\s*\[\[?\s*(.*?)\s*\]\]?\s*(?:#.*)?$/;
|
|
119
|
+
/** The dotted path of a `[table]` / `[[array]]` header, or null when the line is not a header. */
|
|
120
|
+
function headerSegments(line) {
|
|
121
|
+
const match = HEADER_RE.exec(line);
|
|
122
|
+
return match === null ? null : keySegments(match[1] ?? "");
|
|
123
|
+
}
|
|
124
|
+
/** Splits `key = value` at the first `=` outside quoted key segments. */
|
|
125
|
+
function splitAssignment(line) {
|
|
126
|
+
let quote = null;
|
|
127
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
128
|
+
const ch = line[i] ?? "";
|
|
129
|
+
if (quote === '"') {
|
|
130
|
+
if (ch === "\\")
|
|
131
|
+
i += 1;
|
|
132
|
+
else if (ch === '"')
|
|
133
|
+
quote = null;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (quote === "'") {
|
|
137
|
+
if (ch === "'")
|
|
138
|
+
quote = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === '"' || ch === "'")
|
|
142
|
+
quote = ch;
|
|
143
|
+
else if (ch === "=")
|
|
144
|
+
return { key: line.slice(0, i), value: line.slice(i + 1) };
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/** Tracks one TOML value across lines so multi-line values stay attached to their assignment. */
|
|
149
|
+
class ValueScan {
|
|
150
|
+
depth = 0;
|
|
151
|
+
mode = "normal";
|
|
152
|
+
/** Feeds one line; returns true once the value is complete at the end of that line. */
|
|
153
|
+
feed(line) {
|
|
154
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
155
|
+
const ch = line[i] ?? "";
|
|
156
|
+
switch (this.mode) {
|
|
157
|
+
case "normal":
|
|
158
|
+
if (ch === "#")
|
|
159
|
+
i = line.length;
|
|
160
|
+
else if (ch === '"') {
|
|
161
|
+
if (line.startsWith('"""', i)) {
|
|
162
|
+
this.mode = "basic-multi";
|
|
163
|
+
i += 2;
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
this.mode = "basic";
|
|
167
|
+
}
|
|
168
|
+
else if (ch === "'") {
|
|
169
|
+
if (line.startsWith("'''", i)) {
|
|
170
|
+
this.mode = "literal-multi";
|
|
171
|
+
i += 2;
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
this.mode = "literal";
|
|
175
|
+
}
|
|
176
|
+
else if (ch === "[" || ch === "{")
|
|
177
|
+
this.depth += 1;
|
|
178
|
+
else if (ch === "]" || ch === "}")
|
|
179
|
+
this.depth -= 1;
|
|
180
|
+
break;
|
|
181
|
+
case "basic":
|
|
182
|
+
if (ch === "\\")
|
|
183
|
+
i += 1;
|
|
184
|
+
else if (ch === '"')
|
|
185
|
+
this.mode = "normal";
|
|
186
|
+
break;
|
|
187
|
+
case "literal":
|
|
188
|
+
if (ch === "'")
|
|
189
|
+
this.mode = "normal";
|
|
190
|
+
break;
|
|
191
|
+
case "basic-multi":
|
|
192
|
+
if (line.startsWith('"""', i)) {
|
|
193
|
+
this.mode = "normal";
|
|
194
|
+
i += 2;
|
|
195
|
+
}
|
|
196
|
+
else if (ch === "\\")
|
|
197
|
+
i += 1;
|
|
198
|
+
break;
|
|
199
|
+
case "literal-multi":
|
|
200
|
+
if (line.startsWith("'''", i)) {
|
|
201
|
+
this.mode = "normal";
|
|
202
|
+
i += 2;
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return this.mode === "normal" && this.depth === 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* AC-5: replaces the `roles` key of `existing`, keeping every other line (comments, other keys and
|
|
212
|
+
* tables) byte-for-byte. Roles are removed whatever valid TOML form they were written in — role
|
|
213
|
+
* tables, a `[roles]` table with inline entries, a top-level inline `roles = { ... }`, or dotted
|
|
214
|
+
* keys — so a rewrite never leaves a duplicate definition behind. The new tables are appended.
|
|
215
|
+
*/
|
|
216
|
+
export function upsertRoles(existing, roles) {
|
|
217
|
+
const kept = [];
|
|
218
|
+
let table = null;
|
|
219
|
+
let pending = null;
|
|
220
|
+
for (const line of existing.split("\n")) {
|
|
221
|
+
if (pending !== null) {
|
|
222
|
+
const complete = pending.scan.feed(line);
|
|
223
|
+
if (!pending.drop)
|
|
224
|
+
kept.push(line);
|
|
225
|
+
if (complete)
|
|
226
|
+
pending = null;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
const inRoleRegion = table !== null && table[0] === "roles";
|
|
231
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
232
|
+
if (!inRoleRegion)
|
|
233
|
+
kept.push(line);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const header = headerSegments(line);
|
|
237
|
+
if (header !== null) {
|
|
238
|
+
table = header;
|
|
239
|
+
if (header[0] !== "roles")
|
|
240
|
+
kept.push(line);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const assignment = splitAssignment(line);
|
|
244
|
+
if (assignment === null) {
|
|
245
|
+
if (!inRoleRegion)
|
|
246
|
+
kept.push(line);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const drop = inRoleRegion || (table === null && keySegments(assignment.key)[0] === "roles");
|
|
250
|
+
if (!drop)
|
|
251
|
+
kept.push(line);
|
|
252
|
+
const scan = new ValueScan();
|
|
253
|
+
if (!scan.feed(assignment.value))
|
|
254
|
+
pending = { scan, drop };
|
|
255
|
+
}
|
|
256
|
+
while (kept.length > 0 && (kept[kept.length - 1] ?? "").trim() === "")
|
|
257
|
+
kept.pop();
|
|
258
|
+
const prefix = kept.length > 0 ? `${kept.join("\n")}\n\n` : "";
|
|
259
|
+
const blocks = ROLES.map((role) => roleTable(role, roles[role]).trimEnd()).join("\n\n");
|
|
260
|
+
return `${prefix}${blocks}\n`;
|
|
261
|
+
}
|
|
262
|
+
/** Writes the roles to the user config, creating its directory; returns an error message or null. */
|
|
263
|
+
export function writeUserConfig(env, roles) {
|
|
264
|
+
const path = userConfigPath(env);
|
|
265
|
+
try {
|
|
266
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
267
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
268
|
+
writeFileSync(path, existing === null ? serializeUserConfig(roles) : upsertRoles(existing, roles));
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
return `Cannot write ${path}: ${err instanceof Error ? err.message : String(err)}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Writes the config, creating `.gdt/` when needed; returns an error message, or null on success. */
|
|
276
|
+
export function writeConfig(root, text) {
|
|
277
|
+
try {
|
|
278
|
+
const path = join(root, CONFIG_PATH);
|
|
279
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
280
|
+
writeFileSync(path, text);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
return `Cannot write ${CONFIG_PATH}: ${err instanceof Error ? err.message : String(err)}`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* AC-4: creates each missing `.gdt/roles/<role>.md` as an empty file and returns its
|
|
289
|
+
* repository-relative path. An existing file is never touched, not even with `--force`.
|
|
290
|
+
*/
|
|
291
|
+
export function createRoleRulesFiles(root) {
|
|
292
|
+
const created = [];
|
|
293
|
+
for (const role of ROLES) {
|
|
294
|
+
const path = roleRulesPath(root, role);
|
|
295
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
296
|
+
try {
|
|
297
|
+
writeFileSync(path, "", { flag: "wx" });
|
|
298
|
+
created.push(`${ROLE_RULES_DIR}/${role}.md`);
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
// Exists already: leave it byte-for-byte unchanged.
|
|
302
|
+
if (err.code !== "EEXIST")
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return created;
|
|
307
|
+
}
|
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/dist/workflow.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
3
3
|
import { relative } from "node:path";
|
|
4
4
|
import { headless } from "./backends/headless.js";
|
|
5
5
|
import { backendFor } from "./backends/index.js";
|
|
6
|
-
import { loadConfig, ROLES } from "./config.js";
|
|
6
|
+
import { loadConfig, ROLES, userConfigPath } from "./config.js";
|
|
7
7
|
import { validateContract } from "./contract.js";
|
|
8
8
|
import { findRepository, herdrPreflight, unsupportedAgentFindings } from "./doctor.js";
|
|
9
9
|
import { changedFiles } from "./git.js";
|
|
@@ -55,10 +55,13 @@ export function start(issue, cwd, env) {
|
|
|
55
55
|
const root = findRepository(cwd);
|
|
56
56
|
if (root === null)
|
|
57
57
|
return fail(`${cwd} is not inside a Git repository. Run gdt from a checkout of the target repository.\n`);
|
|
58
|
-
const { report } = loadConfig(root, env);
|
|
59
|
-
if (!report.valid)
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
const { report, findings } = loadConfig(root, env);
|
|
59
|
+
if (!report.valid) {
|
|
60
|
+
// AC-2: `gdt start` refuses with the same config errors as `gdt doctor`.
|
|
61
|
+
const errors = findings.filter((finding) => finding.level === "error").map((finding) => finding.message);
|
|
62
|
+
return fail(`${errors.join("\n")}\nRun "gdt doctor" for details.\n`);
|
|
63
|
+
}
|
|
64
|
+
const unsupported = unsupportedAgentFindings(report.roles, userConfigPath(env));
|
|
62
65
|
if (unsupported.length > 0)
|
|
63
66
|
return fail(`${unsupported.map((f) => f.message).join("\n")}\n`);
|
|
64
67
|
if (report.workflow.terminal === "herdr") {
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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 reports the supported
|
|
9
|
+
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 roles to the user config (`~/.config/gdt/config.toml`, or
|
|
15
|
+
`$XDG_CONFIG_HOME/gdt/config.toml`) and the project settings to
|
|
16
|
+
`.gdt/config.toml`, then runs `gdt doctor` and installs this skill.
|
|
17
|
+
- When the user config already defines all three roles, `gdt init` without role
|
|
18
|
+
options reuses them and only writes `.gdt/config.toml`.
|
|
19
|
+
|
|
6
20
|
## Start
|
|
7
21
|
|
|
8
22
|
- `gdt start <issue>` returns immediately; the supervisor keeps running on its own.
|