@gevezex/gdt 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +375 -0
  3. package/dist/agents/adapter.js +7 -0
  4. package/dist/agents/claude.js +13 -0
  5. package/dist/agents/codex.js +13 -0
  6. package/dist/agents/index.js +15 -0
  7. package/dist/agents/mcode.js +14 -0
  8. package/dist/agents/omp.js +14 -0
  9. package/dist/agents/opencode.js +15 -0
  10. package/dist/agents/pi.js +14 -0
  11. package/dist/backends/backend.js +1 -0
  12. package/dist/backends/headless.js +55 -0
  13. package/dist/backends/herdr.js +286 -0
  14. package/dist/backends/index.js +20 -0
  15. package/dist/cli.js +466 -0
  16. package/dist/config.js +210 -0
  17. package/dist/contract.js +133 -0
  18. package/dist/decision.js +175 -0
  19. package/dist/doctor.js +231 -0
  20. package/dist/finding.js +3 -0
  21. package/dist/git.js +25 -0
  22. package/dist/github.js +95 -0
  23. package/dist/locale.js +44 -0
  24. package/dist/notify.js +29 -0
  25. package/dist/prompts.js +80 -0
  26. package/dist/protocol.js +114 -0
  27. package/dist/state.js +113 -0
  28. package/dist/steering.js +165 -0
  29. package/dist/supervisor.js +369 -0
  30. package/dist/worker.js +232 -0
  31. package/dist/workflow.js +333 -0
  32. package/locales/en.toml +28 -0
  33. package/locales/nl.toml +28 -0
  34. package/package.json +61 -0
  35. package/roles/developer.md +66 -0
  36. package/roles/issue-writer.md +40 -0
  37. package/roles/reviewer.md +65 -0
  38. package/roles/tester.md +58 -0
  39. package/schemas/gdt-answer.v1.schema.json +32 -0
  40. package/schemas/gdt-directive.v1.schema.json +36 -0
  41. package/schemas/gdt-handoff.v1.schema.json +117 -0
  42. package/schemas/gdt-question.v1.schema.json +82 -0
  43. package/schemas/gdt-review.v1.schema.json +131 -0
  44. package/schemas/gdt-round.v1.schema.json +28 -0
  45. package/schemas/gdt-test.v1.schema.json +131 -0
  46. package/skill/SKILL.md +46 -0
@@ -0,0 +1,333 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { relative } from "node:path";
4
+ import { headless } from "./backends/headless.js";
5
+ import { backendFor } from "./backends/index.js";
6
+ import { loadConfig, ROLES } from "./config.js";
7
+ import { validateContract } from "./contract.js";
8
+ import { findRepository, herdrPreflight, unsupportedAgentFindings } from "./doctor.js";
9
+ import { changedFiles } from "./git.js";
10
+ import { issueBody, repository } from "./github.js";
11
+ import { loadLocale } from "./locale.js";
12
+ import { alive, lockHolder, paths, readOverrides, readState, writeState } from "./state.js";
13
+ import { cliPath } from "./supervisor.js";
14
+ export const ok = (stdout) => ({ code: 0, stdout, stderr: "" });
15
+ export const fail = (stderr) => ({ code: 1, stdout: "", stderr });
16
+ /** Statuses that need a person or end the workflow: `gdt wait` returns when it reaches one. */
17
+ const ACTION_STATUSES = [
18
+ "awaiting_human",
19
+ "blocked",
20
+ "failed",
21
+ "ready_to_merge",
22
+ "contract_changed",
23
+ "stopped",
24
+ ];
25
+ /** How often `gdt wait` re-reads the local state; well under the 2 seconds the contract allows. */
26
+ const WAIT_POLL_MS = 200;
27
+ function sleepSync(ms) {
28
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
29
+ }
30
+ function newState(issue, repo) {
31
+ return {
32
+ version: 1,
33
+ issue,
34
+ workflow_id: randomBytes(3).toString("hex"),
35
+ status: "starting",
36
+ reason: "",
37
+ role: null,
38
+ round: null,
39
+ exit_code: null,
40
+ repository: repo,
41
+ pr_number: null,
42
+ head: null,
43
+ head_transition_at: null,
44
+ contract: null,
45
+ dispatched: [],
46
+ inflight: null,
47
+ notified_status: null,
48
+ open_findings: [],
49
+ pids: { supervisor: null, workers: {} },
50
+ updated_at: "",
51
+ };
52
+ }
53
+ /** `gdt start <n>`: preflight, then starts a detached supervisor and returns. */
54
+ export function start(issue, cwd, env) {
55
+ const root = findRepository(cwd);
56
+ if (root === null)
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
+ return fail('.gdt/config.toml is invalid. Run "gdt doctor" for details.\n');
61
+ const unsupported = unsupportedAgentFindings(report.roles);
62
+ if (unsupported.length > 0)
63
+ return fail(`${unsupported.map((f) => f.message).join("\n")}\n`);
64
+ if (report.workflow.terminal === "herdr") {
65
+ const problem = herdrPreflight(env);
66
+ if (problem !== null)
67
+ return fail(`${problem}\n`);
68
+ }
69
+ const p = paths(root, issue, env);
70
+ const holder = lockHolder(p);
71
+ if (holder !== null)
72
+ return fail(`Supervisor for #${issue} is already running (pid ${holder})\n`);
73
+ const existing = readState(p);
74
+ if (existing?.status === "failed")
75
+ return fail(`Workflow for #${issue} failed: ${existing.reason}. Next: gdt retry ${issue}\n`);
76
+ if (existing === null || existing.dispatched.length === 0) {
77
+ const changed = changedFiles(root, env, true);
78
+ if (changed.length > 0)
79
+ return fail(`Working tree not clean: ${changed.join(", ")}. Commit or stash before starting.\n`);
80
+ }
81
+ const fetched = issueBody(issue, root, env);
82
+ if ("error" in fetched)
83
+ return fail(`${fetched.error}\n`);
84
+ let locale;
85
+ try {
86
+ locale = loadLocale(report.language);
87
+ }
88
+ catch (err) {
89
+ return fail(`${err instanceof Error ? err.message : String(err)}\n`);
90
+ }
91
+ const contract = validateContract(fetched.body, locale, { maxAcceptanceCriteria: report.contract.max_acceptance_criteria });
92
+ if (!contract.valid) {
93
+ const lines = contract.errors.map((error) => ` - ${error}`).join("\n");
94
+ return fail(`Issue #${issue}: contract invalid (${contract.errors.length} error(s))\n${lines}\nFix the issue body, then run "gdt start ${issue}" again.\n`);
95
+ }
96
+ let state;
97
+ try {
98
+ state = existing ?? newState(issue, repository(root, env));
99
+ }
100
+ catch (err) {
101
+ return fail(`Could not read the repository with gh: ${err instanceof Error ? err.message : String(err)}. Run "gdt doctor".\n`);
102
+ }
103
+ Object.assign(state, { status: "starting", reason: existing === null ? "" : "resuming", pids: { supervisor: null, workers: {} } });
104
+ writeState(p, state);
105
+ let backend;
106
+ let pid;
107
+ try {
108
+ backend = backendFor(report, root, issue, env, p);
109
+ backend.ensureWorkspace();
110
+ pid = backend.spawnPane("supervisor", [process.execPath, cliPath(), "_supervise", String(issue)]);
111
+ }
112
+ catch (err) {
113
+ return fail(`Could not start the supervisor for #${issue}: ${err instanceof Error ? err.message : String(err)}. Run "gdt doctor".\n`);
114
+ }
115
+ const running = () => lockHolder(p) === pid && readState(p)?.pids.supervisor === pid;
116
+ for (let waited = 0; waited < 4000 && !running() && alive(pid); waited += 50)
117
+ sleepSync(50);
118
+ const logs = relative(root, p.logs) || p.logs;
119
+ if (!running())
120
+ return fail(`Supervisor for #${issue} exited during startup; see ${logs}/supervisor.log\n`);
121
+ const attach = backend.attach();
122
+ const attachLine = attach === null ? "" : `Attach: ${attach}\n`;
123
+ return ok(`Supervisor started for #${issue}; logs: ${logs}\n${attachLine}workflow ${state.workflow_id}. Next: gdt status ${issue}\n`);
124
+ }
125
+ /** The workflow's terminal backend; falls back to the headless one when the config is unreadable. */
126
+ function loadBackend(p, issue, env) {
127
+ const { report } = loadConfig(p.root, env);
128
+ return report.valid ? backendFor(report, p.root, issue, env, p) : headless(p.logs, p.root, env);
129
+ }
130
+ /**
131
+ * Stops the supervisor, its workers and any running agent. It does not release the lock: the caller
132
+ * releases it after writing the final state, so `gdt wait` never sees the lock vanish while the state
133
+ * is still a waiting status (AC-4 would otherwise fire for a deliberate stop).
134
+ */
135
+ export function stopProcesses(p, state, env) {
136
+ // The stop window starts here; `gdt wait` keeps waiting while this process lives (see stopInProgress).
137
+ mkdirSync(p.dir, { recursive: true });
138
+ writeFileSync(p.stopping, `${process.pid}\n`);
139
+ const backend = loadBackend(p, state.issue, env);
140
+ const pids = [lockHolder(p), state.pids.supervisor, ...ROLES.map((role) => state.pids.workers[role])];
141
+ for (const pid of new Set(pids))
142
+ if (pid !== null && pid !== undefined && alive(pid))
143
+ backend.close(pid);
144
+ }
145
+ /** Ends the stop window: releases the lock after the final state is written, then the stop marker. */
146
+ function releaseAfterStop(p) {
147
+ rmSync(p.lock, { force: true });
148
+ rmSync(p.stopping, { force: true });
149
+ }
150
+ /** True while a live `gdt stop` or `gdt retry` is between ending the supervisor and writing the final state. */
151
+ function stopInProgress(p) {
152
+ if (!existsSync(p.stopping))
153
+ return false;
154
+ const pid = Number(readFileSync(p.stopping, "utf8").trim());
155
+ return Number.isInteger(pid) && alive(pid);
156
+ }
157
+ /** AC-6: after stopping, every herdr pane shows STOPPED; the gdt processes printed the last line. */
158
+ function markStopped(p, issue, env) {
159
+ const { report } = loadConfig(p.root, env);
160
+ if (!report.valid || report.workflow.terminal !== "herdr")
161
+ return;
162
+ try {
163
+ const backend = backendFor(report, p.root, issue, env, p);
164
+ backend.setTitle("supervisor", "supervisor · stopped");
165
+ for (const role of ROLES)
166
+ backend.setTitle(role, `${role} · ${report.roles[role].agent} · STOPPED`);
167
+ // AC-5: `stopped` reports `idle` for the supervisor pane.
168
+ backend.reportState("supervisor", "idle");
169
+ }
170
+ catch {
171
+ // The processes are already stopped; a missing herdr must not fail `gdt stop`.
172
+ }
173
+ }
174
+ /** `gdt stop <n>`: stops the supervisor, workers and running agents; `gdt start` resumes. */
175
+ export function stop(issue, cwd, env) {
176
+ const root = findRepository(cwd) ?? cwd;
177
+ const p = paths(root, issue, env);
178
+ const before = readState(p);
179
+ if (before === null)
180
+ return fail(`No workflow for #${issue}. Next: gdt start ${issue}\n`);
181
+ stopProcesses(p, before, env);
182
+ // Re-read: the supervisor may have written state until it was stopped.
183
+ const state = readState(p) ?? before;
184
+ // A failed turn keeps its status and reason; only the recovery step (gdt retry) clears it.
185
+ if (state.status === "failed")
186
+ Object.assign(state, { pids: { supervisor: null, workers: {} } });
187
+ else
188
+ Object.assign(state, { status: "stopped", reason: "", pids: { supervisor: null, workers: {} } });
189
+ writeState(p, state);
190
+ // Release the lock only now: `gdt wait` must return on the final status, never on the vanished lock.
191
+ releaseAfterStop(p);
192
+ markStopped(p, issue, env);
193
+ // The printed next step is exactly what `gdt status` reports after this command.
194
+ return ok(`Stopped #${issue}. Next: ${describe(state, false).next}\n`);
195
+ }
196
+ /** States from which `gdt retry` may clear the interrupted turn: a failed turn, or a blocked one that never produced a usable record. */
197
+ function retryable(state) {
198
+ if (state.status === "failed")
199
+ return true;
200
+ return state.status === "blocked" && (state.reason.includes("without a visible handoff") || state.reason.includes("already ran"));
201
+ }
202
+ /** `gdt retry <n>`: stops the workflow and clears the interrupted turn so `gdt start` runs it again. */
203
+ export function retry(issue, cwd, env) {
204
+ const root = findRepository(cwd) ?? cwd;
205
+ const p = paths(root, issue, env);
206
+ const state = readState(p);
207
+ if (state === null)
208
+ return fail(`No workflow for #${issue}. Next: gdt start ${issue}\n`);
209
+ if (!retryable(state)) {
210
+ return fail(`Workflow for #${issue} is not in a retryable state (status ${state.status}). Next: ${describe(state, lockHolder(p) !== null).next}\n`);
211
+ }
212
+ stopProcesses(p, state, env);
213
+ const key = state.inflight?.key ?? state.blocked_key ?? undefined;
214
+ if (key !== undefined) {
215
+ rmSync(p.started(key), { force: true });
216
+ rmSync(p.result(key), { force: true });
217
+ state.dispatched = state.dispatched.filter((known) => known !== key);
218
+ }
219
+ Object.assign(state, {
220
+ status: "stopped",
221
+ reason: "",
222
+ exit_code: null,
223
+ inflight: null,
224
+ blocked_key: null,
225
+ pids: { supervisor: null, workers: {} },
226
+ });
227
+ writeState(p, state);
228
+ // Release the lock only now: `gdt wait` must return on the final status, never on the vanished lock.
229
+ releaseAfterStop(p);
230
+ return ok(`Retry prepared for #${issue}. Next: ${describe(state, false).next}\n`);
231
+ }
232
+ function blockedHint(issue, reason) {
233
+ if (reason.startsWith("round budget exhausted"))
234
+ return `gdt allow-round ${issue}`;
235
+ if (reason.includes("without a visible handoff") || reason.includes("already ran"))
236
+ return `gdt retry ${issue}`;
237
+ if (reason.includes("without a Changelog update"))
238
+ return `add a Changelog entry to issue #${issue}, or revert the body change`;
239
+ return "resolve the cause; the supervisor checks again on every poll";
240
+ }
241
+ /** The one-line status and the next step. */
242
+ export function describe(state, supervisorAlive) {
243
+ const n = state.issue;
244
+ if (state.status === "paused")
245
+ return { line: "paused", next: `gdt resume ${n}` };
246
+ // In herdr mode the supervisor exits after `ready_to_merge`; merging needs no live supervisor.
247
+ const active = !["stopped", "failed", "ready_to_merge"].includes(state.status);
248
+ if (active && !supervisorAlive)
249
+ return { line: `supervisor not running (last status: ${state.status})`, next: `gdt start ${n}` };
250
+ const withReason = state.reason === "" ? state.status : `${state.status}: ${state.reason}`;
251
+ switch (state.status) {
252
+ case "failed":
253
+ return { line: state.reason, next: `gdt retry ${n}` };
254
+ case "stopped":
255
+ return { line: "stopped", next: `gdt start ${n}` };
256
+ case "running":
257
+ return { line: `running: ${state.role ?? "?"} turn, round ${state.round ?? 0}`, next: "wait" };
258
+ case "blocked":
259
+ return { line: withReason, next: blockedHint(n, state.reason) };
260
+ case "awaiting_human":
261
+ return { line: withReason, next: `gdt answer ${n} <question-id> "<answer>"` };
262
+ case "ready_to_merge":
263
+ return { line: withReason, next: `review and merge pull request #${state.pr_number ?? "?"}` };
264
+ default:
265
+ return { line: withReason, next: "wait" };
266
+ }
267
+ }
268
+ /** The state as status and wait see it: the pause file reports `paused` until the workflow ends. */
269
+ function effectiveState(p, state) {
270
+ const paused = existsSync(p.pause) && state.status !== "stopped" && state.status !== "failed";
271
+ return paused ? { ...state, status: "paused", reason: "" } : state;
272
+ }
273
+ /** `gdt status <n>`. The pause file reports `paused` even before the supervisor notices it. */
274
+ export function status(issue, cwd, env, json) {
275
+ const root = findRepository(cwd) ?? cwd;
276
+ const p = paths(root, issue, env);
277
+ const state = readState(p);
278
+ if (state === null) {
279
+ return json
280
+ ? { code: 1, stdout: `${JSON.stringify({ issue, status: null, next_step: `gdt start ${issue}` }, null, 2)}\n`, stderr: "" }
281
+ : fail(`No workflow for #${issue}. Next: gdt start ${issue}\n`);
282
+ }
283
+ const effective = effectiveState(p, state);
284
+ const { line, next } = describe(effective, lockHolder(p) !== null);
285
+ if (!json)
286
+ return ok(`${line}. Next: ${next}\n`);
287
+ let maxRounds = null;
288
+ const { report } = loadConfig(root, env);
289
+ if (report.valid)
290
+ maxRounds = report.workflow.max_correction_rounds;
291
+ const out = {
292
+ issue,
293
+ workflow_id: state.workflow_id,
294
+ status: effective.status,
295
+ reason: effective.reason,
296
+ role: state.role,
297
+ round: state.round,
298
+ max_rounds: maxRounds,
299
+ exit_code: state.exit_code,
300
+ pr_number: state.pr_number,
301
+ open_findings: state.open_findings ?? [],
302
+ overrides: readOverrides(p),
303
+ next_step: next,
304
+ };
305
+ return ok(`${JSON.stringify(out, null, 2)}\n`);
306
+ }
307
+ /**
308
+ * `gdt wait <n>`: blocks on the local workflow state only (no gh, no model tokens) and returns with
309
+ * the `gdt status` output as soon as the workflow reaches an action status. It also returns when the
310
+ * supervisor dies in a waiting status, because that is an event the operator must relay (AC-4).
311
+ */
312
+ export function wait(issue, cwd, env, options) {
313
+ const root = findRepository(cwd) ?? cwd;
314
+ const p = paths(root, issue, env);
315
+ const deadline = options.timeoutSeconds === null ? null : Date.now() + options.timeoutSeconds * 1000;
316
+ for (;;) {
317
+ const state = readState(p);
318
+ // No workflow is status's error path (AC-6); an action status returns at once (AC-1, AC-2).
319
+ if (state === null)
320
+ return status(issue, cwd, env, options.json);
321
+ const effective = effectiveState(p, state);
322
+ if (ACTION_STATUSES.includes(effective.status))
323
+ return status(issue, cwd, env, options.json);
324
+ // A dead supervisor ends the wait, except while the operator paused it deliberately (AC-4).
325
+ // A deliberate stop is not a dead supervisor: its final state follows once the stop window ends.
326
+ if (effective.status !== "paused" && lockHolder(p) === null && !stopInProgress(p))
327
+ return status(issue, cwd, env, options.json);
328
+ if (deadline !== null && Date.now() >= deadline) {
329
+ return fail(`still ${effective.status} after ${options.timeoutSeconds} s. Next: gdt wait ${issue}\n`);
330
+ }
331
+ sleepSync(WAIT_POLL_MS);
332
+ }
333
+ }
@@ -0,0 +1,28 @@
1
+ # Everything the issue-contract validator must recognise in English issue bodies.
2
+
3
+ # Top-level keys: they must come before the first [table].
4
+ name = "English"
5
+ vague_phrases = ["etc.", "and so on", "where needed", "as needed", "properly", "as usual",
6
+ "in the right way", "robust", "user-friendly", "and the like"]
7
+
8
+ [sections]
9
+ plain_language = "Plain language"
10
+ goal = "Goal"
11
+ context = "Context"
12
+ definitions = "Definitions"
13
+ acceptance_criteria = "Acceptance criteria"
14
+ non_functional = "Non-functional"
15
+ out_of_scope = "Out of scope"
16
+ assumptions = "Assumptions"
17
+ open_questions = "Open questions"
18
+ changelog = "Changelog"
19
+ readiness = "Readiness"
20
+
21
+ [ac_fields]
22
+ given = "Given"
23
+ when = "When"
24
+ then = "Then"
25
+ example = "Example"
26
+
27
+ [markers]
28
+ none = "None."
@@ -0,0 +1,28 @@
1
+ # Everything the issue-contract validator must recognise in Dutch issue bodies.
2
+
3
+ # Top-level keys: they must come before the first [table].
4
+ name = "Dutch"
5
+ vague_phrases = ["etc.", "enzovoort", "waar nodig", "netjes", "zoals gebruikelijk",
6
+ "op de juiste manier", "robuust", "gebruiksvriendelijk", "en dergelijke"]
7
+
8
+ [sections]
9
+ plain_language = "In gewone taal"
10
+ goal = "Doel"
11
+ context = "Context"
12
+ definitions = "Definities"
13
+ acceptance_criteria = "Acceptatiecriteria"
14
+ non_functional = "Niet-functioneel"
15
+ out_of_scope = "Buiten scope"
16
+ assumptions = "Aannames"
17
+ open_questions = "Open vragen"
18
+ changelog = "Changelog"
19
+ readiness = "Readiness"
20
+
21
+ [ac_fields]
22
+ given = "Gegeven"
23
+ when = "Wanneer"
24
+ then = "Dan"
25
+ example = "Voorbeeld"
26
+
27
+ [markers]
28
+ none = "Geen."
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@gevezex/gdt",
3
+ "version": "0.2.0",
4
+ "description": "GitHub issues to merge-ready pull requests, with a developer, tester and reviewer agent.",
5
+ "keywords": [
6
+ "github",
7
+ "pull-request",
8
+ "ai-agents",
9
+ "coding-agent",
10
+ "claude-code",
11
+ "codex",
12
+ "opencode",
13
+ "cli"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Ayhan Cicek",
17
+ "homepage": "https://github.com/gevezex/gdt#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/gevezex/gdt/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/gevezex/gdt.git"
24
+ },
25
+ "type": "module",
26
+ "bin": {
27
+ "gdt": "dist/cli.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "locales",
32
+ "roles",
33
+ "schemas",
34
+ "skill"
35
+ ],
36
+ "engines": {
37
+ "node": ">=24"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json && node scripts/export-schemas.js",
41
+ "lint": "eslint . && tsc --noEmit",
42
+ "pretest": "npm run build",
43
+ "test": "vitest run",
44
+ "prepublishOnly": "npm run lint && npm test"
45
+ },
46
+ "dependencies": {
47
+ "smol-toml": "^1.9.0",
48
+ "zod": "^4.6.5"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^10.0.1",
52
+ "@types/node": "^24.19.0",
53
+ "eslint": "^10.11.0",
54
+ "typescript": "~6.0.3",
55
+ "typescript-eslint": "^8.70.1",
56
+ "vitest": "^5.0.2"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }
@@ -0,0 +1,66 @@
1
+ # Role: developer
2
+
3
+ You are the developer in a gdt workflow. You turn one GitHub issue into one draft pull request that
4
+ meets every acceptance criterion (AC). A tester and a reviewer, each a separate agent, check your work
5
+ after you hand off. A deterministic supervisor decides whose turn it is, from the records you post.
6
+
7
+ ## Your job this turn
8
+
9
+ 1. Read the issue body with `gh issue view <issue>`. The current body is the only specification.
10
+ Comments are discussion and evidence; they never change the contract.
11
+ 2. Round 0: create a feature branch from the up-to-date default branch and implement the ACs.
12
+ Round 1 or later: read the latest `[gdt-test:v1]` and `[gdt-review:v1]` records on the pull request
13
+ and fix every finding in one set of commits. That set is one correction round.
14
+ 3. Implement only what the ACs require. Anything under "Out of scope", or not covered by an AC, stays
15
+ out. Record choices the issue leaves open under `assumptions`, and any departure from an AC under
16
+ `deviations`.
17
+ 4. Add or update tests so that each AC is covered, and run the project's build, lint and tests.
18
+ 5. Commit, push, and open the pull request as a draft if it does not exist yet. Its body must contain
19
+ `Closes #<issue>`: that is how the supervisor finds the workflow pull request. There is exactly one
20
+ workflow pull request; never open a second one.
21
+ 6. Post your handoff record as a comment on the pull request (see below), then stop.
22
+
23
+ ## Boundaries
24
+
25
+ - Never merge, deploy, close the issue or mark the pull request ready for review. Those are human
26
+ actions.
27
+ - Never write `[gdt-test:v1]`, `[gdt-review:v1]`, `[gdt-answer:v1]`, `[gdt-directive:v1]` or
28
+ `[gdt-round:v1]` records.
29
+ - Human directives (listed in this prompt when there are any) are guidance, not contract. If a
30
+ directive would change product behaviour that the ACs do not settle, do not follow it: ask a
31
+ question instead, and ask for the issue body to be updated.
32
+
33
+ ## The handoff record
34
+
35
+ Post one comment on the pull request containing the marker, one JSON object and the closing marker:
36
+
37
+ ```
38
+ [gdt-handoff:v1]
39
+ { ...JSON matching the schema in this prompt... }
40
+ [/gdt-handoff:v1]
41
+ ```
42
+
43
+ Use `gh pr comment <pr> --body-file <file>`; if no pull request exists (a `blocked` or
44
+ `awaiting_human` handoff before the first push), post it on the issue with `gh issue comment` instead.
45
+ Copy `repository`, `issue`, `round`,
46
+ `issue_body_sha256` and `acceptance_criteria` exactly from the dispatch facts. Set `pr_number` to the
47
+ pull request number. For each AC, `ac_traceability` lists the files and tests that implement it.
48
+ Prose for humans may follow the closing marker, in the language this prompt names. Marker, keys and
49
+ status values always stay in English.
50
+
51
+ Allowed `status` values:
52
+
53
+ - `ready`: the work is pushed and ready for the tester.
54
+ - `awaiting_human`: you cannot continue without a human decision. Post a `[gdt-question:v1]` record
55
+ too (below).
56
+ - `blocked`: you cannot continue for a reason a human must fix, such as missing access or a broken
57
+ environment. Explain it in prose below the record.
58
+
59
+ ## Questions
60
+
61
+ When a product decision is missing, do not guess. Post a `[gdt-question:v1]` record: on the pull
62
+ request if it exists, otherwise on the issue with `gh issue comment`. Use `role` `developer`,
63
+ `resume_role` `developer`, a new `question_id` (`Q1`, `Q2`, ...), and the question text.
64
+ Copy `repository`, `issue`, `round`, `pr_number`, `issue_body_sha256` and `acceptance_criteria`
65
+ from the dispatch facts, as for your record. Then stop. The supervisor resumes you after a human
66
+ answers.
@@ -0,0 +1,40 @@
1
+ # Role: issue writer
2
+
3
+ You help a user turn an idea into a GitHub issue that gdt can work on. The issue body is the contract:
4
+ the developer, tester and reviewer agents work only from it, so it must be complete and unambiguous.
5
+
6
+ ## How to write the body
7
+
8
+ 1. Talk with the user until the goal, the behaviour and the boundaries are clear. Ask; do not invent
9
+ product decisions. Every open point must be settled before the body is final.
10
+ 2. Write the body to a local file, for example `body.md`, in the user's configured language. Use
11
+ exactly the section headings and acceptance-criterion labels of that language's locale file
12
+ (`locales/<language>.toml` in gdt); never translate headings yourself.
13
+ 3. Give every acceptance criterion a number (`AC-1`, `AC-2`, ...) and the four fields Given, When, Then
14
+ and Example, each with concrete values. An error path with different behaviour gets its own
15
+ criterion. Stay within the configured maximum number of criteria.
16
+ 4. List desired behaviour that is not covered under the out-of-scope section. The open-questions
17
+ section must contain exactly the locale's none marker. Avoid the vague phrases the locale lists.
18
+ 5. Check the file:
19
+
20
+ ```sh
21
+ gdt check-issue --body-file body.md
22
+ ```
23
+
24
+ Fix every reported error and run the check again until it reports the contract as valid.
25
+
26
+ ## Creating the issue
27
+
28
+ Create the issue only when the user asked you to create it, and only after the check passes:
29
+
30
+ ```sh
31
+ gh issue create --title "<title>" --body-file body.md
32
+ ```
33
+
34
+ Otherwise, give the user the body file and the check result, and stop.
35
+
36
+ ## Boundaries
37
+
38
+ - You write issue bodies only. You do not start workflows, write code or post workflow records.
39
+ - Keep a changelog entry in the body; when you later change a created issue's body, add a new
40
+ changelog entry describing the change.
@@ -0,0 +1,65 @@
1
+ # Role: reviewer
2
+
3
+ You are the reviewer in a gdt workflow. After the tester approved, you review the pull request at its
4
+ current head: does the change meet every acceptance criterion (AC), stay within scope, and have the
5
+ quality to be merged?
6
+
7
+ ## Your job this turn
8
+
9
+ 1. Read the issue body with `gh issue view <issue>`. The current body is the only specification.
10
+ 2. Read the pull request and its diff with `gh pr view <pr>` and `gh pr diff <pr>`. All roles share one
11
+ checkout, and it is already at the pull request head: confirm that `git rev-parse HEAD` equals the
12
+ head in this prompt (otherwise post `blocked`), read the code in context, and run the build and
13
+ tests if that helps. Never check out another commit.
14
+ 3. Check, in this order:
15
+ - every AC is implemented and covered by a test;
16
+ - nothing outside the ACs was added (compare with "Out of scope");
17
+ - choices the issue leaves open are named in the pull request body;
18
+ - code quality: correctness, edge cases, error handling, security, readability, and consistency
19
+ with the surrounding code.
20
+ 4. Post your review record as a comment on the pull request (see below), then stop.
21
+
22
+ ## Boundaries
23
+
24
+ - You are read-only. Never commit, push, switch branches, reset, or edit tracked files. The supervisor
25
+ checks the working tree after your turn and blocks the workflow if a tracked file, the branch or
26
+ HEAD changed. Scratch files that git does not track are fine; remove them when you are done.
27
+ - Never fix the code yourself; report findings instead.
28
+ - Never merge, deploy, close the issue or mark the pull request ready for review.
29
+ - Never write `[gdt-handoff:v1]`, `[gdt-test:v1]`, `[gdt-answer:v1]`, `[gdt-directive:v1]` or
30
+ `[gdt-round:v1]` records.
31
+ - Human directives (listed in this prompt when there are any) guide your review. They never count as
32
+ evidence and never replace an AC.
33
+
34
+ ## The review record
35
+
36
+ Post one comment on the pull request containing the marker, one JSON object and the closing marker:
37
+
38
+ ```
39
+ [gdt-review:v1]
40
+ { ...JSON matching the schema in this prompt... }
41
+ [/gdt-review:v1]
42
+ ```
43
+
44
+ Use `gh pr comment <pr> --body-file <file>`. Copy `repository`, `issue`, `round`, `pr_number`,
45
+ `issue_body_sha256` and `acceptance_criteria` exactly from the dispatch facts. Set `head` to the full
46
+ 40-character SHA you reviewed. Give every AC one `ac_results` entry with `passed`, `failed` or
47
+ `not_verified`, and concrete `evidence` (file and line, or test name). Each problem is a finding with
48
+ id `R-1`, `R-2`, ...; set `blocking` to true for an unmet AC, scope creep or a real defect, and false
49
+ for suggestions. Prose for humans may follow the closing marker, in the language this prompt names.
50
+ Marker, keys and status values stay in English.
51
+
52
+ Allowed `status` values:
53
+
54
+ - `approved`: every AC is `passed` and there is no blocking finding.
55
+ - `changes_requested`: at least one AC is not `passed`, or a blocking finding is open.
56
+ - `awaiting_human`: you need a human decision. Post a `[gdt-question:v1]` record too.
57
+ - `blocked`: you cannot review for a reason a human must fix.
58
+
59
+ ## Questions
60
+
61
+ When the issue does not settle something the review depends on, post a `[gdt-question:v1]` record on
62
+ the pull request with `role` `reviewer`, `resume_role` `reviewer`, a new `question_id` and the
63
+ question, then stop.
64
+ Copy `repository`, `issue`, `round`, `pr_number`, `issue_body_sha256` and `acceptance_criteria`
65
+ from the dispatch facts, as for your record.