@draht/coding-agent 2026.3.2 → 2026.3.4

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 (194) hide show
  1. package/CHANGELOG.md +194 -13
  2. package/README.md +90 -106
  3. package/agents/architect.md +45 -0
  4. package/agents/debugger.md +57 -0
  5. package/agents/git-committer.md +46 -0
  6. package/agents/implementer.md +25 -0
  7. package/agents/reviewer.md +52 -0
  8. package/agents/security-auditor.md +61 -0
  9. package/agents/verifier.md +44 -0
  10. package/bin/draht-tools.cjs +20 -20
  11. package/dist/agents/architect.md +45 -0
  12. package/dist/agents/debugger.md +57 -0
  13. package/dist/agents/git-committer.md +46 -0
  14. package/dist/agents/implementer.md +25 -0
  15. package/dist/agents/reviewer.md +52 -0
  16. package/dist/agents/security-auditor.md +61 -0
  17. package/dist/agents/verifier.md +44 -0
  18. package/dist/cli/args.d.ts.map +1 -1
  19. package/dist/cli/args.js +1 -0
  20. package/dist/cli/args.js.map +1 -1
  21. package/dist/cli.d.ts.map +1 -1
  22. package/dist/cli.js +5 -0
  23. package/dist/cli.js.map +1 -1
  24. package/dist/config.d.ts +0 -7
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +2 -14
  27. package/dist/config.js.map +1 -1
  28. package/dist/core/agent-session.d.ts +14 -4
  29. package/dist/core/agent-session.d.ts.map +1 -1
  30. package/dist/core/agent-session.js +167 -49
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/auth-storage.d.ts +1 -1
  33. package/dist/core/auth-storage.d.ts.map +1 -1
  34. package/dist/core/auth-storage.js +2 -1
  35. package/dist/core/auth-storage.js.map +1 -1
  36. package/dist/core/builtins/subagent.d.ts +14 -0
  37. package/dist/core/builtins/subagent.d.ts.map +1 -0
  38. package/dist/core/builtins/subagent.js +492 -0
  39. package/dist/core/builtins/subagent.js.map +1 -0
  40. package/dist/core/compaction/compaction.d.ts.map +1 -1
  41. package/dist/core/compaction/compaction.js +4 -1
  42. package/dist/core/compaction/compaction.js.map +1 -1
  43. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  44. package/dist/core/export-html/tool-renderer.js +6 -0
  45. package/dist/core/export-html/tool-renderer.js.map +1 -1
  46. package/dist/core/extensions/loader.d.ts.map +1 -1
  47. package/dist/core/extensions/loader.js +19 -8
  48. package/dist/core/extensions/loader.js.map +1 -1
  49. package/dist/core/extensions/runner.d.ts.map +1 -1
  50. package/dist/core/extensions/runner.js +1 -0
  51. package/dist/core/extensions/runner.js.map +1 -1
  52. package/dist/core/extensions/types.d.ts +8 -2
  53. package/dist/core/extensions/types.d.ts.map +1 -1
  54. package/dist/core/extensions/types.js.map +1 -1
  55. package/dist/core/model-registry.d.ts +1 -0
  56. package/dist/core/model-registry.d.ts.map +1 -1
  57. package/dist/core/model-registry.js +9 -6
  58. package/dist/core/model-registry.js.map +1 -1
  59. package/dist/core/model-resolver.d.ts.map +1 -1
  60. package/dist/core/model-resolver.js +35 -5
  61. package/dist/core/model-resolver.js.map +1 -1
  62. package/dist/core/prompt-templates.js.map +1 -1
  63. package/dist/core/resource-loader.d.ts +2 -0
  64. package/dist/core/resource-loader.d.ts.map +1 -1
  65. package/dist/core/resource-loader.js +5 -1
  66. package/dist/core/resource-loader.js.map +1 -1
  67. package/dist/core/sdk.d.ts +1 -1
  68. package/dist/core/sdk.d.ts.map +1 -1
  69. package/dist/core/sdk.js.map +1 -1
  70. package/dist/core/session-manager.js.map +1 -1
  71. package/dist/core/settings-manager.d.ts +3 -0
  72. package/dist/core/settings-manager.d.ts.map +1 -1
  73. package/dist/core/settings-manager.js +4 -0
  74. package/dist/core/settings-manager.js.map +1 -1
  75. package/dist/core/system-prompt.d.ts +4 -0
  76. package/dist/core/system-prompt.d.ts.map +1 -1
  77. package/dist/core/system-prompt.js +34 -12
  78. package/dist/core/system-prompt.js.map +1 -1
  79. package/dist/core/tools/edit-diff.js.map +1 -1
  80. package/dist/core/tools/path-utils.js.map +1 -1
  81. package/dist/index.d.ts +1 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/main.d.ts.map +1 -1
  86. package/dist/main.js +5 -5
  87. package/dist/main.js.map +1 -1
  88. package/dist/migrations.d.ts +1 -1
  89. package/dist/migrations.d.ts.map +1 -1
  90. package/dist/migrations.js +3 -3
  91. package/dist/migrations.js.map +1 -1
  92. package/dist/modes/interactive/components/armin.js.map +1 -1
  93. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  94. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  95. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  96. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  97. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/extension-editor.js +9 -1
  99. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  100. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  101. package/dist/modes/interactive/components/footer.js.map +1 -1
  102. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/login-dialog.js +1 -1
  104. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  105. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  106. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  108. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  110. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  111. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  112. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/session-selector.js +1 -1
  114. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  115. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  116. package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
  117. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  118. package/dist/modes/interactive/components/tool-execution.js +28 -3
  119. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  120. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  121. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  122. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  123. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/user-message.js +11 -0
  125. package/dist/modes/interactive/components/user-message.js.map +1 -1
  126. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  127. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  128. package/dist/modes/interactive/interactive-mode.js +28 -27
  129. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  130. package/dist/modes/interactive/theme/dark.json +1 -1
  131. package/dist/modes/interactive/theme/light.json +1 -1
  132. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  133. package/dist/modes/interactive/theme/theme.js +5 -0
  134. package/dist/modes/interactive/theme/theme.js.map +1 -1
  135. package/dist/prompts/commands/discuss-phase.md +3 -3
  136. package/dist/prompts/commands/execute-phase.md +9 -9
  137. package/dist/prompts/commands/fix.md +29 -0
  138. package/dist/prompts/commands/init-project.md +49 -0
  139. package/dist/prompts/commands/map-codebase.md +2 -2
  140. package/dist/prompts/commands/new-project.md +9 -9
  141. package/dist/prompts/commands/next-milestone.md +44 -0
  142. package/dist/prompts/commands/pause-work.md +2 -2
  143. package/dist/prompts/commands/plan-phase.md +5 -5
  144. package/dist/prompts/commands/progress.md +1 -1
  145. package/dist/prompts/commands/quick.md +4 -4
  146. package/dist/prompts/commands/resume-work.md +1 -1
  147. package/dist/prompts/commands/review.md +26 -0
  148. package/dist/prompts/commands/verify-work.md +4 -4
  149. package/docs/compaction.md +14 -14
  150. package/docs/custom-provider.md +19 -11
  151. package/docs/development.md +1 -1
  152. package/docs/extensions.md +52 -33
  153. package/docs/json.md +4 -4
  154. package/docs/packages.md +1 -1
  155. package/docs/providers.md +4 -2
  156. package/docs/rpc.md +1 -1
  157. package/docs/sdk.md +24 -24
  158. package/docs/session.md +6 -6
  159. package/docs/settings.md +1 -0
  160. package/docs/termux.md +1 -1
  161. package/docs/themes.md +2 -2
  162. package/docs/tui.md +20 -20
  163. package/examples/extensions/README.md +5 -4
  164. package/examples/extensions/antigravity-image-gen.ts +3 -1
  165. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  166. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  167. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  168. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  169. package/examples/extensions/doom-overlay/README.md +1 -1
  170. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  171. package/examples/extensions/dynamic-tools.ts +74 -0
  172. package/examples/extensions/subagent/README.md +11 -11
  173. package/examples/extensions/with-deps/package-lock.json +2 -2
  174. package/examples/extensions/with-deps/package.json +1 -1
  175. package/examples/sdk/README.md +3 -3
  176. package/package.json +11 -8
  177. package/prompts/commands/discuss-phase.md +3 -3
  178. package/prompts/commands/execute-phase.md +9 -9
  179. package/prompts/commands/fix.md +29 -0
  180. package/prompts/commands/init-project.md +49 -0
  181. package/prompts/commands/map-codebase.md +2 -2
  182. package/prompts/commands/new-project.md +9 -9
  183. package/prompts/commands/next-milestone.md +44 -0
  184. package/prompts/commands/pause-work.md +2 -2
  185. package/prompts/commands/plan-phase.md +5 -5
  186. package/prompts/commands/progress.md +1 -1
  187. package/prompts/commands/quick.md +4 -4
  188. package/prompts/commands/resume-work.md +1 -1
  189. package/prompts/commands/review.md +26 -0
  190. package/prompts/commands/verify-work.md +4 -4
  191. package/dist/extensions/gsd-commands.ts +0 -338
  192. package/dist/extensions/subagent.ts +0 -312
  193. package/extensions/gsd-commands.ts +0 -338
  194. package/extensions/subagent.ts +0 -312
@@ -1,312 +0,0 @@
1
- /**
2
- * Subagent Tool for Draht — adapted from the pi subagent example
3
- *
4
- * Spawns isolated draht processes for delegated tasks.
5
- * Agents are defined in .draht/agents/*.md (project) or ~/.draht/agent/agents/*.md (global).
6
- *
7
- * Modes:
8
- * single — { agent, task }
9
- * parallel — { tasks: [{ agent, task }] } (max 8, concurrency 4)
10
- * chain — { chain: [{ agent, task }] } (sequential, {previous} placeholder)
11
- */
12
-
13
- import { spawn } from "node:child_process";
14
- import * as fs from "node:fs";
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
- import type { Message } from "@draht/ai";
18
- import { StringEnum } from "@draht/ai";
19
- import { type ExtensionAPI, getAgentDir, parseFrontmatter } from "@draht/coding-agent";
20
- import { Type } from "@sinclair/typebox";
21
-
22
- const MAX_PARALLEL = 8;
23
- const MAX_CONCURRENCY = 4;
24
- // Use the same binary that's currently running
25
- const DRAHT_BIN = process.execPath;
26
-
27
- // ─── Agent discovery ────────────────────────────────────────────────────────
28
-
29
- interface AgentConfig {
30
- name: string;
31
- description: string;
32
- tools?: string[];
33
- model?: string;
34
- systemPrompt: string;
35
- source: "user" | "project";
36
- }
37
-
38
- function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
39
- if (!fs.existsSync(dir)) return [];
40
- let entries: fs.Dirent[];
41
- try {
42
- entries = fs.readdirSync(dir, { withFileTypes: true });
43
- } catch {
44
- return [];
45
- }
46
- const agents: AgentConfig[] = [];
47
- for (const entry of entries) {
48
- if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
49
- try {
50
- const content = fs.readFileSync(path.join(dir, entry.name), "utf-8");
51
- const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
52
- if (!frontmatter.name || !frontmatter.description) continue;
53
- const tools = frontmatter.tools?.split(",").map((t: string) => t.trim()).filter(Boolean);
54
- agents.push({
55
- name: frontmatter.name,
56
- description: frontmatter.description,
57
- tools: tools?.length ? tools : undefined,
58
- model: frontmatter.model,
59
- systemPrompt: body,
60
- source,
61
- });
62
- } catch {
63
- continue;
64
- }
65
- }
66
- return agents;
67
- }
68
-
69
- function findProjectAgentsDir(cwd: string): string | null {
70
- let dir = cwd;
71
- while (true) {
72
- const candidate = path.join(dir, ".draht", "agents");
73
- try {
74
- if (fs.statSync(candidate).isDirectory()) return candidate;
75
- } catch {}
76
- const parent = path.dirname(dir);
77
- if (parent === dir) return null;
78
- dir = parent;
79
- }
80
- }
81
-
82
- type AgentScope = "user" | "project" | "both";
83
-
84
- function discoverAgents(cwd: string, scope: AgentScope): AgentConfig[] {
85
- const userDir = path.join(getAgentDir(), "agents");
86
- const projectDir = findProjectAgentsDir(cwd);
87
- const userAgents = scope !== "project" ? loadAgentsFromDir(userDir, "user") : [];
88
- const projectAgents = scope !== "user" && projectDir ? loadAgentsFromDir(projectDir, "project") : [];
89
- const map = new Map<string, AgentConfig>();
90
- for (const a of userAgents) map.set(a.name, a);
91
- for (const a of projectAgents) map.set(a.name, a); // project overrides global
92
- return Array.from(map.values());
93
- }
94
-
95
- // ─── Runner ─────────────────────────────────────────────────────────────────
96
-
97
- interface RunResult {
98
- agent: string;
99
- task: string;
100
- exitCode: number;
101
- output: string;
102
- stderr: string;
103
- step?: number;
104
- }
105
-
106
- function writeTemp(name: string, content: string): { file: string; dir: string } {
107
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "draht-subagent-"));
108
- const file = path.join(dir, `${name.replace(/[^\w.-]/g, "_")}.md`);
109
- fs.writeFileSync(file, content, { encoding: "utf-8", mode: 0o600 });
110
- return { file, dir };
111
- }
112
-
113
- function cleanTemp(file: string, dir: string) {
114
- try { fs.unlinkSync(file); } catch {}
115
- try { fs.rmdirSync(dir); } catch {}
116
- }
117
-
118
- function getFinalText(messages: Message[]): string {
119
- for (let i = messages.length - 1; i >= 0; i--) {
120
- const msg = messages[i];
121
- if (msg.role === "assistant") {
122
- for (const part of msg.content) {
123
- if (part.type === "text") return part.text;
124
- }
125
- }
126
- }
127
- return "";
128
- }
129
-
130
- async function runAgent(
131
- cwd: string,
132
- agent: AgentConfig,
133
- task: string,
134
- signal?: AbortSignal,
135
- step?: number,
136
- ): Promise<RunResult> {
137
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
138
- if (agent.model) args.push("--model", agent.model);
139
- if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
140
-
141
- let tmpFile: string | null = null;
142
- let tmpDir: string | null = null;
143
-
144
- if (agent.systemPrompt.trim()) {
145
- const tmp = writeTemp(agent.name, agent.systemPrompt);
146
- tmpFile = tmp.file;
147
- tmpDir = tmp.dir;
148
- args.push("--append-system-prompt", tmpFile);
149
- }
150
-
151
- args.push(`Task: ${task}`);
152
-
153
- const messages: Message[] = [];
154
- let stderr = "";
155
-
156
- try {
157
- const exitCode = await new Promise<number>((resolve) => {
158
- const proc = spawn(DRAHT_BIN, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
159
- let buf = "";
160
-
161
- const processLine = (line: string) => {
162
- if (!line.trim()) return;
163
- try {
164
- const event = JSON.parse(line);
165
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
166
- messages.push(event.message as Message);
167
- }
168
- } catch {}
169
- };
170
-
171
- proc.stdout.on("data", (d) => {
172
- buf += d.toString();
173
- const lines = buf.split("\n");
174
- buf = lines.pop() || "";
175
- for (const l of lines) processLine(l);
176
- });
177
- proc.stderr.on("data", (d) => { stderr += d.toString(); });
178
- proc.on("close", (code) => {
179
- if (buf.trim()) processLine(buf);
180
- resolve(code ?? 0);
181
- });
182
- proc.on("error", () => resolve(1));
183
-
184
- if (signal) {
185
- const kill = () => { proc.kill("SIGTERM"); setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 5000); };
186
- if (signal.aborted) kill();
187
- else signal.addEventListener("abort", kill, { once: true });
188
- }
189
- });
190
-
191
- return { agent: agent.name, task, exitCode, output: getFinalText(messages), stderr, step };
192
- } finally {
193
- if (tmpFile && tmpDir) cleanTemp(tmpFile, tmpDir);
194
- }
195
- }
196
-
197
- async function runParallel<T>(
198
- items: T[],
199
- concurrency: number,
200
- fn: (item: T, i: number) => Promise<T extends unknown ? RunResult : never>,
201
- ): Promise<RunResult[]> {
202
- const results: RunResult[] = new Array(items.length);
203
- let next = 0;
204
- await Promise.all(
205
- Array.from({ length: Math.min(concurrency, items.length) }, async () => {
206
- while (true) {
207
- const i = next++;
208
- if (i >= items.length) return;
209
- results[i] = await (fn as (item: T, i: number) => Promise<RunResult>)(items[i], i);
210
- }
211
- }),
212
- );
213
- return results;
214
- }
215
-
216
- // ─── Extension ──────────────────────────────────────────────────────────────
217
-
218
- const TaskItem = Type.Object({
219
- agent: Type.String({ description: "Agent name" }),
220
- task: Type.String({ description: "Task description" }),
221
- });
222
-
223
- const ChainItem = Type.Object({
224
- agent: Type.String({ description: "Agent name" }),
225
- task: Type.String({ description: "Task, optionally using {previous} placeholder" }),
226
- });
227
-
228
- const Params = Type.Object({
229
- agent: Type.Optional(Type.String()),
230
- task: Type.Optional(Type.String()),
231
- tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks" })),
232
- chain: Type.Optional(Type.Array(ChainItem, { description: "Chained tasks" })),
233
- agentScope: Type.Optional(
234
- StringEnum(["user", "project", "both"] as const, { default: "both" }),
235
- ),
236
- });
237
-
238
- export default function (pi: ExtensionAPI) {
239
- pi.registerTool({
240
- name: "subagent",
241
- label: "Subagent",
242
- description:
243
- "Delegate to specialized agents. single: {agent,task} | parallel: {tasks:[]} | chain: {chain:[]} with {previous} placeholder. agentScope: 'both' (default) uses project .draht/agents/ + global.",
244
- parameters: Params,
245
-
246
- async execute(_id, params, signal, _onUpdate, ctx) {
247
- const scope: AgentScope = (params.agentScope as AgentScope) ?? "both";
248
- const agents = discoverAgents(ctx.cwd, scope);
249
- const available = agents.map((a) => a.name).join(", ") || "none";
250
-
251
- const find = (name: string) => agents.find((a) => a.name === name);
252
- const notFound = (name: string) => ({
253
- content: [{ type: "text" as const, text: `Unknown agent "${name}". Available: ${available}` }],
254
- isError: true,
255
- });
256
-
257
- // ── Chain mode ──
258
- if (params.chain?.length) {
259
- let previous = "";
260
- const results: RunResult[] = [];
261
- for (let i = 0; i < params.chain.length; i++) {
262
- const step = params.chain[i];
263
- const agent = find(step.agent);
264
- if (!agent) return notFound(step.agent);
265
- const task = step.task.replace(/\{previous\}/g, previous);
266
- const result = await runAgent(ctx.cwd, agent, task, signal, i + 1);
267
- results.push(result);
268
- if (result.exitCode !== 0) {
269
- return {
270
- content: [{ type: "text" as const, text: `Chain failed at step ${i + 1} (${step.agent}):\n${result.output || result.stderr}` }],
271
- isError: true,
272
- };
273
- }
274
- previous = result.output;
275
- }
276
- return { content: [{ type: "text" as const, text: results[results.length - 1].output || "(no output)" }] };
277
- }
278
-
279
- // ── Parallel mode ──
280
- if (params.tasks?.length) {
281
- if (params.tasks.length > MAX_PARALLEL) {
282
- return { content: [{ type: "text" as const, text: `Too many tasks (max ${MAX_PARALLEL})` }], isError: true };
283
- }
284
- for (const t of params.tasks) { if (!find(t.agent)) return notFound(t.agent); }
285
-
286
- const results = await runParallel(params.tasks, MAX_CONCURRENCY, async (t, i) => {
287
- return runAgent(ctx.cwd, find(t.agent)!, t.task, signal);
288
- });
289
-
290
- const ok = results.filter((r) => r.exitCode === 0).length;
291
- const summary = results
292
- .map((r) => `[${r.agent}] ${r.exitCode === 0 ? "✓" : "✗"} ${r.output.slice(0, 200)}`)
293
- .join("\n\n");
294
- return { content: [{ type: "text" as const, text: `Parallel: ${ok}/${results.length} succeeded\n\n${summary}` }] };
295
- }
296
-
297
- // ── Single mode ──
298
- if (params.agent && params.task) {
299
- const agent = find(params.agent);
300
- if (!agent) return notFound(params.agent);
301
- const result = await runAgent(ctx.cwd, agent, params.task, signal);
302
- const isError = result.exitCode !== 0;
303
- return {
304
- content: [{ type: "text" as const, text: result.output || result.stderr || "(no output)" }],
305
- ...(isError ? { isError: true } : {}),
306
- };
307
- }
308
-
309
- return { content: [{ type: "text" as const, text: `Provide exactly one mode. Available agents: ${available}` }], isError: true };
310
- },
311
- });
312
- }
@@ -1,338 +0,0 @@
1
- /**
2
- * GSD — Get Shit Done
3
- *
4
- * Batteries-included phase commands for structured AI-assisted development.
5
- *
6
- * Full workflow:
7
- * /discuss <feature> — clarify requirements before planning
8
- * /plan <feature> — architect produces implementation plan
9
- * /execute <task1, task2...> — parallel implement → review → commit
10
- * /verify — parallel lint/typecheck/tests + security audit
11
- *
12
- * Utilities:
13
- * /review <scope> — ad-hoc code review + security audit
14
- * /fix <issue> — targeted fix plan for a failing task
15
- * /quick <task> — one-shot implement + commit (skip full GSD)
16
- * /resume — pick up interrupted work from CONTINUE-HERE.md
17
- * /status — show .planning/STATE.md overview
18
- * /new-project <name> [path] — create project dir, git init, scaffold .draht/
19
- * /init-project — scaffold .draht/ in existing project
20
- */
21
-
22
- import * as fs from "node:fs";
23
- import * as path from "node:path";
24
- import type { ExtensionAPI } from "@draht/coding-agent";
25
-
26
- function isBusy(ctx: { isIdle: () => boolean }, ui: { notify: (msg: string, level: string) => void }): boolean {
27
- if (!ctx.isIdle()) {
28
- ui.notify("Agent is busy", "warning");
29
- return true;
30
- }
31
- return false;
32
- }
33
-
34
- export default function (pi: ExtensionAPI) {
35
- // ── /discuss ─────────────────────────────────────────────────────────────
36
- pi.registerCommand("discuss", {
37
- description: "Clarify requirements before planning. Architect asks questions and defines scope. Usage: /discuss <feature>",
38
- handler: async (args, ctx) => {
39
- if (!args.trim()) {
40
- ctx.ui.notify("Usage: /discuss <feature description>", "warning");
41
- return;
42
- }
43
- if (isBusy(ctx, ctx.ui)) return;
44
-
45
- pi.sendUserMessage(
46
- `Use the subagent tool to delegate to the architect agent with this task:
47
-
48
- "We are in the DISCUSS phase for: ${args.trim()}
49
-
50
- Your job is NOT to plan yet. First:
51
- 1. Read relevant existing code to understand the current state
52
- 2. Identify ambiguities, unknowns, and risks
53
- 3. List clarifying questions that need answers before planning can begin
54
- 4. Define a clear, bounded scope for what will and won't be built
55
- 5. Output a DISCUSS summary with: scope, assumptions, open questions, risks
56
-
57
- Do not produce file lists or implementation details yet."
58
-
59
- Set agentScope to "project".`,
60
- );
61
- },
62
- });
63
-
64
- // ── /plan ────────────────────────────────────────────────────────────────
65
- pi.registerCommand("plan", {
66
- description: "Plan a feature — architect reads codebase and produces structured implementation plan. Usage: /plan <feature>",
67
- handler: async (args, ctx) => {
68
- if (!args.trim()) {
69
- ctx.ui.notify("Usage: /plan <feature description>", "warning");
70
- return;
71
- }
72
- if (isBusy(ctx, ctx.ui)) return;
73
-
74
- pi.sendUserMessage(
75
- `Use the subagent tool to delegate to the architect agent with this task: "${args.trim()}"\n\nSet agentScope to "project".`,
76
- );
77
- },
78
- });
79
-
80
- // ── /execute ─────────────────────────────────────────────────────────────
81
- pi.registerCommand("execute", {
82
- description: "Execute tasks in parallel, then chain reviewer + git-committer. Usage: /execute task1, task2, task3",
83
- handler: async (args, ctx) => {
84
- if (!args.trim()) {
85
- ctx.ui.notify("Usage: /execute task1, task2, task3", "warning");
86
- return;
87
- }
88
- if (isBusy(ctx, ctx.ui)) return;
89
-
90
- const tasks = args.split(",").map((t) => t.trim()).filter(Boolean);
91
-
92
- if (tasks.length === 1) {
93
- pi.sendUserMessage(
94
- `Use the subagent tool in chain mode with agentScope "project":
95
- 1. agent: implementer — task: "${tasks[0]}"
96
- 2. agent: reviewer — task: "Review the changes just made: {previous}"
97
- 3. agent: git-committer — task: "Commit all changes. Review context: {previous}"`,
98
- );
99
- } else {
100
- const parallelList = tasks.map((t, i) => `${i + 1}. agent: implementer — task: "${t}"`).join("\n");
101
- pi.sendUserMessage(
102
- `Use the subagent tool with agentScope "project":
103
-
104
- Step 1 — PARALLEL mode (run all simultaneously):
105
- ${parallelList}
106
-
107
- Step 2 — CHAIN mode (after all parallel tasks complete):
108
- 1. agent: reviewer — task: "Review all changes just implemented"
109
- 2. agent: git-committer — task: "Commit all changes. Review findings: {previous}"`,
110
- );
111
- }
112
- },
113
- });
114
-
115
- // ── /verify ──────────────────────────────────────────────────────────────
116
- pi.registerCommand("verify", {
117
- description: "Parallel verification: lint, typecheck, tests, and security audit",
118
- handler: async (_args, ctx) => {
119
- if (isBusy(ctx, ctx.ui)) return;
120
-
121
- pi.sendUserMessage(
122
- `Use the subagent tool in PARALLEL mode with agentScope "project":
123
- 1. agent: verifier — task: "Run all lint, typecheck, and test checks"
124
- 2. agent: security-auditor — task: "Audit all recent changes for security vulnerabilities"
125
-
126
- After both complete, merge findings into a single prioritized report. List what must be fixed before this is production-ready.`,
127
- );
128
- },
129
- });
130
-
131
- // ── /review ──────────────────────────────────────────────────────────────
132
- pi.registerCommand("review", {
133
- description: "Ad-hoc code review + security audit. Usage: /review <scope or files>",
134
- handler: async (args, ctx) => {
135
- if (isBusy(ctx, ctx.ui)) return;
136
-
137
- const scope = args.trim() || "all recent changes";
138
- pi.sendUserMessage(
139
- `Use the subagent tool in PARALLEL mode with agentScope "project":
140
- 1. agent: reviewer — task: "Review ${scope} for correctness, type safety, and conventions"
141
- 2. agent: security-auditor — task: "Audit ${scope} for security vulnerabilities"
142
-
143
- After both complete, merge into a single prioritized findings report.`,
144
- );
145
- },
146
- });
147
-
148
- // ── /fix ─────────────────────────────────────────────────────────────────
149
- pi.registerCommand("fix", {
150
- description: "Create a targeted fix plan for a failing task or bug. Usage: /fix <description of what's broken>",
151
- handler: async (args, ctx) => {
152
- if (!args.trim()) {
153
- ctx.ui.notify("Usage: /fix <description of what's broken>", "warning");
154
- return;
155
- }
156
- if (isBusy(ctx, ctx.ui)) return;
157
-
158
- pi.sendUserMessage(
159
- `Use the subagent tool in chain mode with agentScope "project":
160
- 1. agent: architect — task: "Diagnose this issue and produce a minimal fix plan: ${args.trim()}. Read the relevant code first. Output: root cause, exact files to change, fix steps."
161
- 2. agent: implementer — task: "Apply this fix plan exactly: {previous}"
162
- 3. agent: reviewer — task: "Verify the fix is correct and doesn't introduce regressions: {previous}"
163
- 4. agent: git-committer — task: "Commit the fix. Fix summary: {previous}"`,
164
- );
165
- },
166
- });
167
-
168
- // ── /quick ───────────────────────────────────────────────────────────────
169
- pi.registerCommand("quick", {
170
- description: "One-shot task: implement + commit. Skips full GSD workflow. Usage: /quick <task>",
171
- handler: async (args, ctx) => {
172
- if (!args.trim()) {
173
- ctx.ui.notify("Usage: /quick <task description>", "warning");
174
- return;
175
- }
176
- if (isBusy(ctx, ctx.ui)) return;
177
-
178
- pi.sendUserMessage(
179
- `Use the subagent tool in chain mode with agentScope "project":
180
- 1. agent: implementer — task: "${args.trim()}"
181
- 2. agent: git-committer — task: "Commit the changes just made: {previous}"`,
182
- );
183
- },
184
- });
185
-
186
- // ── /resume ──────────────────────────────────────────────────────────────
187
- pi.registerCommand("resume", {
188
- description: "Resume interrupted work — reads CONTINUE-HERE.md and picks up where we left off",
189
- handler: async (_args, ctx) => {
190
- if (isBusy(ctx, ctx.ui)) return;
191
-
192
- const continueFile = path.join(ctx.cwd, ".planning", "CONTINUE-HERE.md");
193
- const stateFile = path.join(ctx.cwd, ".planning", "STATE.md");
194
-
195
- if (!fs.existsSync(continueFile) && !fs.existsSync(stateFile)) {
196
- ctx.ui.notify("No .planning/CONTINUE-HERE.md or STATE.md found. Nothing to resume.", "warning");
197
- return;
198
- }
199
-
200
- let context = "";
201
- if (fs.existsSync(continueFile)) {
202
- context += `\nCONTINUE-HERE.md:\n${fs.readFileSync(continueFile, "utf-8")}`;
203
- }
204
- if (fs.existsSync(stateFile)) {
205
- context += `\nSTATE.md:\n${fs.readFileSync(stateFile, "utf-8")}`;
206
- }
207
-
208
- pi.sendUserMessage(
209
- `Read the following project state and resume work from where it was interrupted. Identify the next incomplete task and proceed with it using the appropriate subagent.${context}`,
210
- );
211
- },
212
- });
213
-
214
- // ── /status ──────────────────────────────────────────────────────────────
215
- pi.registerCommand("status", {
216
- description: "Show current GSD project state from .planning/STATE.md",
217
- handler: async (_args, ctx) => {
218
- const stateFile = path.join(ctx.cwd, ".planning", "STATE.md");
219
- const logFile = path.join(ctx.cwd, ".planning", "execution-log.jsonl");
220
-
221
- if (!fs.existsSync(stateFile)) {
222
- ctx.ui.notify("No .planning/STATE.md found. Run /init-project or /new-project first.", "warning");
223
- return;
224
- }
225
-
226
- let output = fs.readFileSync(stateFile, "utf-8");
227
-
228
- if (fs.existsSync(logFile)) {
229
- const entries = fs.readFileSync(logFile, "utf-8")
230
- .split("\n").filter(Boolean)
231
- .map((l) => { try { return JSON.parse(l); } catch { return null; } })
232
- .filter(Boolean);
233
-
234
- const passed = entries.filter((e) => e.status === "pass").length;
235
- const failed = entries.filter((e) => e.status === "fail").length;
236
- const skipped = entries.filter((e) => e.status === "skip").length;
237
- output += `\n\n---\n**Execution log:** ${passed} passed, ${failed} failed, ${skipped} skipped`;
238
- }
239
-
240
- pi.sendUserMessage(`Here is the current project state:\n\n${output}`);
241
- },
242
- });
243
-
244
- // ── /new-project ─────────────────────────────────────────────────────────
245
- pi.registerCommand("new-project", {
246
- description: "Create a new project: mkdir, git init, scaffold .draht/. Usage: /new-project <name> [/base/path]",
247
- handler: async (args, ctx) => {
248
- const parts = args.trim().split(/\s+/);
249
- const name = parts[0];
250
- if (!name) {
251
- ctx.ui.notify("Usage: /new-project <name> [/optional/base/path]", "warning");
252
- return;
253
- }
254
-
255
- const basePath = parts[1] ?? ctx.cwd;
256
- const projectDir = path.join(basePath, name);
257
-
258
- if (fs.existsSync(projectDir)) {
259
- ctx.ui.notify(`Directory already exists: ${projectDir}`, "warning");
260
- return;
261
- }
262
-
263
- fs.mkdirSync(projectDir, { recursive: true });
264
-
265
- const { execSync } = await import("node:child_process");
266
- execSync("git init", { cwd: projectDir });
267
-
268
- // Scaffold .draht/ from global agents
269
- const agentSrc = path.join(process.env.HOME ?? "~", ".draht", "agent", "agents");
270
- const agentDest = path.join(projectDir, ".draht", "agents");
271
- const extDest = path.join(projectDir, ".draht", "extensions");
272
- fs.mkdirSync(agentDest, { recursive: true });
273
- fs.mkdirSync(extDest, { recursive: true });
274
-
275
- let agentsCopied = 0;
276
- if (fs.existsSync(agentSrc)) {
277
- for (const file of fs.readdirSync(agentSrc)) {
278
- if (!file.endsWith(".md")) continue;
279
- fs.copyFileSync(path.join(agentSrc, file), path.join(agentDest, file));
280
- agentsCopied++;
281
- }
282
- }
283
-
284
- // Copy shipped extensions into the new project
285
- const extSrc = path.dirname(new URL(import.meta.url).pathname);
286
- for (const file of ["subagent.ts", "gsd-commands.ts"]) {
287
- const src = path.join(extSrc, file);
288
- if (fs.existsSync(src)) {
289
- fs.copyFileSync(src, path.join(extDest, file));
290
- }
291
- }
292
-
293
- fs.writeFileSync(path.join(projectDir, ".gitignore"), "node_modules/\n.env\n.env.local\n");
294
-
295
- ctx.ui.notify(
296
- `✓ ${projectDir} created — ${agentsCopied} agents, git initialized. Customize .draht/agents/*.md for your stack.`,
297
- "info",
298
- );
299
-
300
- pi.sendUserMessage(
301
- `New project "${name}" created at ${projectDir} with ${agentsCopied} GSD agents scaffolded. What should we build first?`,
302
- );
303
- },
304
- });
305
-
306
- // ── /init-project ────────────────────────────────────────────────────────
307
- pi.registerCommand("init-project", {
308
- description: "Scaffold .draht/ config into an existing project from global agent defaults",
309
- handler: async (_args, ctx) => {
310
- const targetDir = path.join(ctx.cwd, ".draht");
311
-
312
- if (fs.existsSync(targetDir)) {
313
- ctx.ui.notify(".draht/ already exists in this project", "warning");
314
- return;
315
- }
316
-
317
- const agentSrc = path.join(process.env.HOME ?? "~", ".draht", "agent", "agents");
318
- const agentDest = path.join(targetDir, "agents");
319
- const extDest = path.join(targetDir, "extensions");
320
- fs.mkdirSync(agentDest, { recursive: true });
321
- fs.mkdirSync(extDest, { recursive: true });
322
-
323
- let copied = 0;
324
- if (fs.existsSync(agentSrc)) {
325
- for (const file of fs.readdirSync(agentSrc)) {
326
- if (!file.endsWith(".md")) continue;
327
- fs.copyFileSync(path.join(agentSrc, file), path.join(agentDest, file));
328
- copied++;
329
- }
330
- }
331
-
332
- ctx.ui.notify(
333
- `.draht/ scaffolded with ${copied} agents. Customize .draht/agents/*.md for this project's stack.`,
334
- "info",
335
- );
336
- },
337
- });
338
- }