@kendoo.agentdesk/agentdesk 0.1.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.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ // AgentDesk CLI — AI team orchestrator for Claude Code
4
+
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+
8
+ if (!command || command === "help" || command === "--help") {
9
+ console.log(`
10
+ AgentDesk — AI team orchestrator for Claude Code
11
+
12
+ Getting started:
13
+ 1. Sign up at https://agentdesk.live and copy your API key from Settings
14
+ 2. Add to your project's .env: AGENTDESK_API_KEY=<your-key>
15
+ 3. Run: agentdesk init
16
+ 4. Run: agentdesk team <TASK-ID>
17
+
18
+ Usage:
19
+ agentdesk init Detect project and show setup info
20
+ agentdesk team <TASK-ID> Run a team session on a task
21
+ agentdesk team <TASK-ID> -d "..." Run with a task description
22
+
23
+ Options:
24
+ --description, -d Task description (for projects without a task tracker)
25
+ --cwd Working directory (defaults to current)
26
+
27
+ Configuration:
28
+ Create .agentdesk.json in your project root:
29
+ {
30
+ "tracker": "linear", // "linear" | "jira" | "github"
31
+ "linear": { "workspace": "..." } // for task links in the dashboard
32
+ }
33
+
34
+ Examples:
35
+ agentdesk init
36
+ agentdesk team KEN-517
37
+ agentdesk team BUG-42 -d "Fix the checkout total calculation"
38
+
39
+ Dashboard: https://agentdesk.live
40
+ `);
41
+ process.exit(0);
42
+ }
43
+
44
+ if (command === "init") {
45
+ const { runInit } = await import("../cli/init.mjs");
46
+ await runInit(process.cwd());
47
+ }
48
+
49
+ else if (command === "team") {
50
+ const taskId = args[1];
51
+ if (!taskId) {
52
+ console.error("Usage: agentdesk team <TASK-ID>");
53
+ process.exit(1);
54
+ }
55
+
56
+ // Parse options
57
+ let description = "";
58
+ let cwd = process.cwd();
59
+ for (let i = 2; i < args.length; i++) {
60
+ if ((args[i] === "--description" || args[i] === "-d") && args[i + 1]) {
61
+ description = args[++i];
62
+ } else if (args[i] === "--cwd" && args[i + 1]) {
63
+ cwd = args[++i];
64
+ }
65
+ }
66
+
67
+ const { runTeam } = await import("../cli/team.mjs");
68
+ const code = await runTeam(taskId, { description, cwd });
69
+ process.exit(code);
70
+ }
71
+
72
+ else if (command === "server") {
73
+ try {
74
+ await import("../server/index.mjs");
75
+ } catch {
76
+ console.error("The server is not available in the CLI package.");
77
+ console.error("Visit https://agentdesk.live to use the dashboard.");
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ else {
83
+ console.error(`Unknown command: ${command}`);
84
+ console.error("Run 'agentdesk help' for usage.");
85
+ process.exit(1);
86
+ }
package/cli/config.mjs ADDED
@@ -0,0 +1,76 @@
1
+ // .agentdesk.json config loader
2
+ // Place this file in your project root to customize agent behavior
3
+
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+
7
+ const DEFAULTS = {
8
+ // Task tracker integration
9
+ tracker: null, // "linear" | "jira" | "github" | null
10
+
11
+ // Tracker-specific settings
12
+ linear: {
13
+ teamKey: null, // e.g., "KEN"
14
+ workspace: null, // e.g., "kendoo" — the slug in linear.app/<workspace>/issue/...
15
+ },
16
+ jira: {
17
+ baseUrl: null, // e.g., "https://mycompany.atlassian.net"
18
+ project: null, // e.g., "PROJ"
19
+ },
20
+ github: {
21
+ repo: null, // e.g., "owner/repo" — auto-detected from git if null
22
+ },
23
+
24
+ // Agent customization
25
+ agents: {
26
+ Jane: { role: "Product Analyst / Team Lead" },
27
+ Dennis: { role: "Senior Developer" },
28
+ Bart: { role: "QA Engineer" },
29
+ Vera: { role: "Test Engineer" },
30
+ Sam: { role: "Architecture Auditor" },
31
+ },
32
+
33
+ // Commands — auto-detected if null
34
+ commands: {
35
+ test: null,
36
+ build: null,
37
+ lint: null,
38
+ },
39
+
40
+ // Existing agents/bots in the project — declare them so the team knows about them
41
+ // Each entry: { name, role, when, how }
42
+ // - name: Agent/bot name (e.g., "ReviewBot", "DataSync Agent")
43
+ // - role: What it does (e.g., "Automated code review on PR creation")
44
+ // - when: When to use or expect it (e.g., "Triggers on every PR", "Run manually via npm run agent:sync")
45
+ // - how: How to interact with it (e.g., "Comment /review on a PR", "Set INPUT_MODE=dry-run for testing")
46
+ projectAgents: [],
47
+
48
+ // Extra prompt instructions appended to the team prompt
49
+ instructions: null,
50
+ };
51
+
52
+ export function loadConfig(dir) {
53
+ const configPath = join(dir, ".agentdesk.json");
54
+
55
+ if (!existsSync(configPath)) return { ...DEFAULTS };
56
+
57
+ try {
58
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
59
+ return deepMerge(DEFAULTS, raw);
60
+ } catch (err) {
61
+ console.error(`Warning: Failed to parse .agentdesk.json: ${err.message}`);
62
+ return { ...DEFAULTS };
63
+ }
64
+ }
65
+
66
+ function deepMerge(defaults, overrides) {
67
+ const result = { ...defaults };
68
+ for (const [key, value] of Object.entries(overrides)) {
69
+ if (value && typeof value === "object" && !Array.isArray(value) && defaults[key] && typeof defaults[key] === "object") {
70
+ result[key] = deepMerge(defaults[key], value);
71
+ } else {
72
+ result[key] = value;
73
+ }
74
+ }
75
+ return result;
76
+ }
package/cli/detect.mjs ADDED
@@ -0,0 +1,291 @@
1
+ // Detect project type, existing agents, and gather context for agent prompts
2
+
3
+ import { existsSync, readFileSync, readdirSync } from "fs";
4
+ import { join, basename } from "path";
5
+
6
+ const PROJECT_TYPES = [
7
+ { file: "package.json", field: "dependencies", check: d => d?.next, type: "nextjs", label: "Next.js" },
8
+ { file: "package.json", field: "dependencies", check: d => d?.react, type: "react", label: "React" },
9
+ { file: "package.json", field: "dependencies", check: d => d?.express, type: "express", label: "Express" },
10
+ { file: "package.json", field: null, check: null, type: "node", label: "Node.js" },
11
+ { file: "requirements.txt", type: "python", label: "Python" },
12
+ { file: "pyproject.toml", type: "python", label: "Python" },
13
+ { file: "Cargo.toml", type: "rust", label: "Rust" },
14
+ { file: "go.mod", type: "go", label: "Go" },
15
+ { file: "Gemfile", type: "ruby", label: "Ruby" },
16
+ { file: "pom.xml", type: "java", label: "Java" },
17
+ { file: "build.gradle", type: "java", label: "Java/Gradle" },
18
+ { file: "composer.json", type: "php", label: "PHP" },
19
+ ];
20
+
21
+ export function detectProject(dir) {
22
+ const result = {
23
+ dir,
24
+ type: "unknown",
25
+ label: "Unknown",
26
+ name: null,
27
+ hasClaudeMd: false,
28
+ claudeMd: null,
29
+ hasGit: false,
30
+ hasLinear: false,
31
+ hasGithub: false,
32
+ taskTracker: null,
33
+ testCommand: null,
34
+ buildCommand: null,
35
+ lintCommand: null,
36
+ };
37
+
38
+ // Detect project type
39
+ for (const pt of PROJECT_TYPES) {
40
+ const filePath = join(dir, pt.file);
41
+ if (!existsSync(filePath)) continue;
42
+
43
+ if (pt.field) {
44
+ try {
45
+ const content = JSON.parse(readFileSync(filePath, "utf-8"));
46
+ if (pt.check && !pt.check(content[pt.field])) continue;
47
+ } catch { continue; }
48
+ }
49
+
50
+ result.type = pt.type;
51
+ result.label = pt.label;
52
+ break;
53
+ }
54
+
55
+ // Read project name
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
58
+ result.name = pkg.name;
59
+ if (pkg.scripts?.test) result.testCommand = "npm test";
60
+ if (pkg.scripts?.build) result.buildCommand = "npm run build";
61
+ if (pkg.scripts?.lint) result.lintCommand = "npm run lint";
62
+ } catch {}
63
+
64
+ // Check for CLAUDE.md
65
+ if (existsSync(join(dir, "CLAUDE.md"))) {
66
+ result.hasClaudeMd = true;
67
+ result.claudeMd = readFileSync(join(dir, "CLAUDE.md"), "utf-8");
68
+ }
69
+
70
+ // Check git
71
+ result.hasGit = existsSync(join(dir, ".git"));
72
+
73
+ // Check for Linear (env or config)
74
+ if (existsSync(join(dir, ".env"))) {
75
+ try {
76
+ const env = readFileSync(join(dir, ".env"), "utf-8");
77
+ result.hasLinear = env.includes("LINEAR_API_KEY");
78
+ } catch {}
79
+ }
80
+
81
+ // Check for GitHub CLI
82
+ result.hasGithub = existsSync(join(dir, ".git"));
83
+
84
+ // Detect task tracker
85
+ if (result.hasLinear) {
86
+ result.taskTracker = "linear";
87
+ }
88
+
89
+ // Discover existing agents in the project
90
+ result.agents = detectAgents(dir);
91
+
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * Discover existing AI agents, bots, and automation in the project.
97
+ * Returns an array of { name, type, role, source, config } objects.
98
+ */
99
+ function detectAgents(dir) {
100
+ const agents = [];
101
+
102
+ // 1. MCP servers (.mcp.json or .claude/mcp.json)
103
+ for (const mcpPath of [".mcp.json", ".claude/mcp.json"]) {
104
+ const fullPath = join(dir, mcpPath);
105
+ if (existsSync(fullPath)) {
106
+ try {
107
+ const mcp = JSON.parse(readFileSync(fullPath, "utf-8"));
108
+ const servers = mcp.mcpServers || mcp.servers || mcp;
109
+ for (const [name, config] of Object.entries(servers)) {
110
+ agents.push({
111
+ name,
112
+ type: "mcp-server",
113
+ role: config.description || `MCP server providing tools via ${config.command || "unknown"}`,
114
+ source: mcpPath,
115
+ config: { command: config.command, args: config.args },
116
+ });
117
+ }
118
+ } catch {}
119
+ }
120
+ }
121
+
122
+ // 2. Claude agents (.claude/agents/)
123
+ const agentsDir = join(dir, ".claude", "agents");
124
+ if (existsSync(agentsDir)) {
125
+ try {
126
+ for (const file of readdirSync(agentsDir)) {
127
+ if (!file.endsWith(".md")) continue;
128
+ const name = basename(file, ".md");
129
+ const content = readFileSync(join(agentsDir, file), "utf-8");
130
+ // Parse YAML frontmatter if present
131
+ let role = "Claude agent";
132
+ let description = "";
133
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
134
+ if (fmMatch) {
135
+ const descMatch = fmMatch[1].match(/description:\s*(.+)/);
136
+ const nameMatch = fmMatch[1].match(/name:\s*(.+)/);
137
+ description = descMatch?.[1]?.trim() || "";
138
+ role = description || `Claude agent "${nameMatch?.[1]?.trim() || name}"`;
139
+ } else {
140
+ const firstLine = content.split("\n").find(l => l.trim() && !l.startsWith("---")) || "";
141
+ role = firstLine.replace(/^#\s*/, "").slice(0, 120) || role;
142
+ }
143
+ agents.push({
144
+ name,
145
+ type: "claude-agent",
146
+ role,
147
+ source: `.claude/agents/${file}`,
148
+ });
149
+ }
150
+ } catch {}
151
+ }
152
+
153
+ // 3. Claude slash commands (.claude/commands/)
154
+ const commandsDir = join(dir, ".claude", "commands");
155
+ if (existsSync(commandsDir)) {
156
+ try {
157
+ for (const file of readdirSync(commandsDir)) {
158
+ if (!file.endsWith(".md")) continue;
159
+ const name = basename(file, ".md");
160
+ const content = readFileSync(join(commandsDir, file), "utf-8");
161
+ const firstLine = content.split("\n").find(l => l.trim()) || "";
162
+ agents.push({
163
+ name: `/${name}`,
164
+ type: "claude-command",
165
+ role: firstLine.replace(/^#\s*/, "").slice(0, 120),
166
+ source: `.claude/commands/${file}`,
167
+ });
168
+ }
169
+ } catch {}
170
+ }
171
+
172
+ // 4. GitHub Actions bots (dependabot, renovate, custom bot workflows)
173
+ const workflowsDir = join(dir, ".github", "workflows");
174
+ if (existsSync(workflowsDir)) {
175
+ try {
176
+ for (const file of readdirSync(workflowsDir)) {
177
+ if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
178
+ const content = readFileSync(join(workflowsDir, file), "utf-8");
179
+ // Detect AI/bot-related workflows
180
+ if (/\b(bot|agent|auto|copilot|claude|openai|gpt|ai-)\b/i.test(file) ||
181
+ /\b(anthropic|openai|claude|gpt|langchain|ai-review|auto-fix)\b/i.test(content)) {
182
+ const nameMatch = content.match(/^name:\s*(.+)/m);
183
+ agents.push({
184
+ name: nameMatch?.[1]?.trim() || basename(file, file.endsWith(".yml") ? ".yml" : ".yaml"),
185
+ type: "github-action",
186
+ role: `GitHub Actions workflow`,
187
+ source: `.github/workflows/${file}`,
188
+ });
189
+ }
190
+ }
191
+ } catch {}
192
+ }
193
+
194
+ // 5. Dependabot
195
+ const dependabotPath = join(dir, ".github", "dependabot.yml");
196
+ const dependabotPath2 = join(dir, ".github", "dependabot.yaml");
197
+ if (existsSync(dependabotPath) || existsSync(dependabotPath2)) {
198
+ agents.push({
199
+ name: "Dependabot",
200
+ type: "dependency-bot",
201
+ role: "Automated dependency updates — creates PRs for outdated packages",
202
+ source: ".github/dependabot.yml",
203
+ });
204
+ }
205
+
206
+ // 6. Renovate
207
+ for (const renovatePath of ["renovate.json", "renovate.json5", ".renovaterc", ".renovaterc.json"]) {
208
+ if (existsSync(join(dir, renovatePath))) {
209
+ agents.push({
210
+ name: "Renovate",
211
+ type: "dependency-bot",
212
+ role: "Automated dependency updates — creates PRs for outdated packages",
213
+ source: renovatePath,
214
+ });
215
+ break;
216
+ }
217
+ }
218
+
219
+ // 7. Custom agent directories (agents/, bots/, ai/)
220
+ for (const agentDir of ["agents", "bots", "ai"]) {
221
+ const fullDir = join(dir, agentDir);
222
+ if (!existsSync(fullDir)) continue;
223
+ try {
224
+ for (const entry of readdirSync(fullDir, { withFileTypes: true })) {
225
+ if (entry.isDirectory()) {
226
+ // Check for a manifest or README
227
+ const manifest = join(fullDir, entry.name, "agent.json");
228
+ const readme = join(fullDir, entry.name, "README.md");
229
+ let role = "Custom agent";
230
+ if (existsSync(manifest)) {
231
+ try {
232
+ const m = JSON.parse(readFileSync(manifest, "utf-8"));
233
+ role = m.description || m.role || role;
234
+ } catch {}
235
+ } else if (existsSync(readme)) {
236
+ const first = readFileSync(readme, "utf-8").split("\n").find(l => l.trim() && !l.startsWith("#")) || "";
237
+ role = first.slice(0, 120) || role;
238
+ }
239
+ agents.push({
240
+ name: entry.name,
241
+ type: "custom-agent",
242
+ role,
243
+ source: `${agentDir}/${entry.name}/`,
244
+ });
245
+ }
246
+ }
247
+ } catch {}
248
+ }
249
+
250
+ // 8. Agents declared in .agentdesk.json (loaded separately, merged in team.mjs)
251
+ // This is handled via config.projectAgents
252
+
253
+ return agents;
254
+ }
255
+
256
+ export function generateContext(project) {
257
+ const lines = [];
258
+ lines.push(`# Project: ${project.name || "Unknown"}`);
259
+ lines.push(`Type: ${project.label}`);
260
+ lines.push(`Directory: ${project.dir}`);
261
+ lines.push("");
262
+
263
+ if (project.testCommand) lines.push(`Test command: ${project.testCommand}`);
264
+ if (project.buildCommand) lines.push(`Build command: ${project.buildCommand}`);
265
+ if (project.lintCommand) lines.push(`Lint command: ${project.lintCommand}`);
266
+ if (project.testCommand || project.buildCommand || project.lintCommand) lines.push("");
267
+
268
+ if (project.hasClaudeMd) {
269
+ lines.push("## Project Instructions (from CLAUDE.md)");
270
+ lines.push("");
271
+ lines.push(project.claudeMd);
272
+ } else {
273
+ lines.push("No CLAUDE.md found. The agents will explore the codebase to understand conventions.");
274
+ }
275
+
276
+ // Include discovered agents
277
+ const allAgents = [...(project.agents || []), ...(project.configAgents || [])];
278
+ if (allAgents.length > 0) {
279
+ lines.push("");
280
+ lines.push("## Existing Agents & Automation");
281
+ lines.push("");
282
+ for (const agent of allAgents) {
283
+ lines.push(`- **${agent.name}** (${agent.type}) — ${agent.role}`);
284
+ if (agent.source) lines.push(` Source: \`${agent.source}\``);
285
+ if (agent.when) lines.push(` When to use: ${agent.when}`);
286
+ if (agent.how) lines.push(` How to use: ${agent.how}`);
287
+ }
288
+ }
289
+
290
+ return lines.join("\n");
291
+ }
package/cli/init.mjs ADDED
@@ -0,0 +1,77 @@
1
+ // `agentdesk init` — detect project and register with AgentDesk server
2
+
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { detectProject } from "./detect.mjs";
6
+ import { loadConfig } from "./config.mjs";
7
+
8
+ const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
9
+
10
+ function loadApiKey(dir) {
11
+ const envPath = join(dir, ".env");
12
+ if (!existsSync(envPath)) return null;
13
+ const match = readFileSync(envPath, "utf-8").match(/AGENTDESK_API_KEY=(.+)/);
14
+ return match?.[1]?.trim() || null;
15
+ }
16
+
17
+ export async function runInit(cwd) {
18
+ const project = detectProject(cwd);
19
+ const config = loadConfig(cwd);
20
+ const tracker = config.tracker || (project.hasLinear ? "linear" : null);
21
+ const projectId = project.name || cwd.split("/").pop();
22
+
23
+ console.log("");
24
+ console.log(" AgentDesk — Project Setup");
25
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━");
26
+ console.log("");
27
+ console.log(` Project: ${project.name || "unknown"}`);
28
+ console.log(` Type: ${project.label}`);
29
+ console.log(` Directory: ${project.dir}`);
30
+ console.log(` Git: ${project.hasGit ? "✓" : "✗"}`);
31
+ console.log(` CLAUDE.md: ${project.hasClaudeMd ? "✓" : "✗ (recommended — run 'claude' to generate one)"}`);
32
+ console.log(` Tracker: ${tracker || "— (none)"}`);
33
+ console.log("");
34
+
35
+ if (project.testCommand) console.log(` Test: ${project.testCommand}`);
36
+ if (project.buildCommand) console.log(` Build: ${project.buildCommand}`);
37
+ if (project.lintCommand) console.log(` Lint: ${project.lintCommand}`);
38
+ if (project.testCommand || project.buildCommand || project.lintCommand) console.log("");
39
+
40
+ // Register with AgentDesk server
41
+ try {
42
+ const res = await fetch(`${SERVER}/api/projects`, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ ...(loadApiKey(cwd) ? { "x-api-key": loadApiKey(cwd) } : {}),
47
+ },
48
+ body: JSON.stringify({
49
+ id: projectId,
50
+ name: project.name || projectId,
51
+ path: project.dir,
52
+ type: project.type,
53
+ tracker,
54
+ }),
55
+ });
56
+ if (res.ok) {
57
+ console.log(" ✓ Registered with AgentDesk server");
58
+ } else {
59
+ console.log(" ⚠ AgentDesk server not available — project will register on first session");
60
+ }
61
+ } catch {
62
+ console.log(" ⚠ AgentDesk server not running — start it with: agentdesk server");
63
+ }
64
+
65
+ console.log("");
66
+ console.log(" Ready. Run a team session with:");
67
+ console.log("");
68
+ console.log(` agentdesk team TASK-123`);
69
+ console.log(` agentdesk team TASK-123 --description "Fix the login bug"`);
70
+ console.log("");
71
+
72
+ if (!project.hasClaudeMd) {
73
+ console.log(" Tip: Create a CLAUDE.md in your project root to help the agents");
74
+ console.log(" understand your codebase conventions, architecture, and commands.");
75
+ console.log("");
76
+ }
77
+ }
package/cli/team.mjs ADDED
@@ -0,0 +1,337 @@
1
+ // `agentdesk team <TASK-ID> [--description "..."]` — run a team session
2
+
3
+ import { spawn } from "child_process";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { createInterface } from "readline";
6
+ import { resolve, dirname, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { randomUUID } from "crypto";
9
+ import WebSocket from "ws";
10
+ import { detectProject, generateContext } from "./detect.mjs";
11
+ import { loadConfig } from "./config.mjs";
12
+
13
+ function loadDotEnv(dir) {
14
+ const envPath = join(dir, ".env");
15
+ if (!existsSync(envPath)) return {};
16
+ const vars = {};
17
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed || trimmed.startsWith("#")) continue;
20
+ const eq = trimmed.indexOf("=");
21
+ if (eq === -1) continue;
22
+ vars[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
23
+ }
24
+ return vars;
25
+ }
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const PROMPT_PATH = resolve(__dirname, "../prompts/team.md");
29
+
30
+ export async function runTeam(taskId, opts = {}) {
31
+ const cwd = opts.cwd || process.cwd();
32
+ const description = opts.description || "";
33
+
34
+ // Detect project and load config
35
+ const project = detectProject(cwd);
36
+ const config = loadConfig(cwd);
37
+ console.log(`Project: ${project.name || "unknown"} (${project.label})`);
38
+ console.log(`Task: ${taskId}`);
39
+
40
+ // Determine tracker and build task link
41
+ const tracker = config.tracker || (project.hasLinear ? "linear" : null);
42
+ let taskLink = null;
43
+ if (tracker === "linear" && config.linear?.workspace) {
44
+ taskLink = `https://linear.app/${config.linear.workspace}/issue/${taskId}`;
45
+ } else if (tracker === "jira" && config.jira?.baseUrl) {
46
+ taskLink = `${config.jira.baseUrl}/browse/${taskId}`;
47
+ } else if (tracker === "github") {
48
+ const repo = config.github?.repo || "";
49
+ if (repo) taskLink = `https://github.com/${repo}/issues/${taskId}`;
50
+ }
51
+ if (tracker) console.log(`Tracker: ${tracker}`);
52
+ if (taskLink) console.log(`Task: ${taskLink}`);
53
+
54
+ // Build prompt
55
+ let prompt = readFileSync(PROMPT_PATH, "utf-8");
56
+
57
+ // Template substitution
58
+ prompt = prompt.replace(/\{\{TASK_ID\}\}/g, taskId);
59
+ prompt = prompt.replace(/\{\{TASK_LINK\}\}/g, taskLink || "");
60
+
61
+ // Task description
62
+ if (description) {
63
+ prompt = prompt.replace(/\{\{#TASK_DESCRIPTION\}\}([\s\S]*?)\{\{\/TASK_DESCRIPTION\}\}/g, "$1");
64
+ prompt = prompt.replace(/\{\{TASK_DESCRIPTION\}\}/g, description);
65
+ } else {
66
+ prompt = prompt.replace(/\{\{#TASK_DESCRIPTION\}\}[\s\S]*?\{\{\/TASK_DESCRIPTION\}\}/g, "");
67
+ }
68
+
69
+ // Tracker integration — enable the matching section, strip the rest
70
+ const trackers = ["LINEAR", "JIRA", "GITHUB"];
71
+ for (const t of trackers) {
72
+ const enabled = tracker === t.toLowerCase();
73
+ if (enabled) {
74
+ prompt = prompt.replace(new RegExp(`\\{\\{#${t}\\}\\}([\\s\\S]*?)\\{\\{\\/${t}\\}\\}`, "g"), "$1");
75
+ } else {
76
+ prompt = prompt.replace(new RegExp(`\\{\\{#${t}\\}\\}[\\s\\S]*?\\{\\{\\/${t}\\}\\}`, "g"), "");
77
+ }
78
+ }
79
+
80
+ // NO_TRACKER — enable if no tracker configured
81
+ if (!tracker) {
82
+ prompt = prompt.replace(/\{\{#NO_TRACKER\}\}([\s\S]*?)\{\{\/NO_TRACKER\}\}/g, "$1");
83
+ } else {
84
+ prompt = prompt.replace(/\{\{#NO_TRACKER\}\}[\s\S]*?\{\{\/NO_TRACKER\}\}/g, "");
85
+ }
86
+
87
+ // Jira-specific variables
88
+ if (config.jira?.baseUrl) {
89
+ prompt = prompt.replace(/\{\{JIRA_BASE_URL\}\}/g, config.jira.baseUrl);
90
+ }
91
+
92
+ // Append custom instructions from config
93
+ if (config.instructions) {
94
+ prompt += `\n\n## ADDITIONAL INSTRUCTIONS\n\n${config.instructions}\n`;
95
+ }
96
+
97
+ // --- AgentDesk config ---
98
+ const projectEnv = loadDotEnv(cwd);
99
+ const apiKey = projectEnv.AGENTDESK_API_KEY || process.env.AGENTDESK_API_KEY || null;
100
+ const agentdeskServer = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
101
+ const baseUrl = process.env.AGENTDESK_URL || "wss://agentdesk.live/ws/agent";
102
+ const AGENTDESK_URL = apiKey ? `${baseUrl}?api_key=${apiKey}` : baseUrl;
103
+ const sessionId = `${taskId}-${randomUUID().slice(0, 8)}`;
104
+ const inboxUrl = `${agentdeskServer}/api/sessions/${sessionId}/inbox`;
105
+
106
+ // Inject inbox URL into prompt
107
+ prompt = prompt.replace(/\{\{AGENTDESK_INBOX_URL\}\}/g, inboxUrl);
108
+
109
+ // Merge declared agents from config into project for context generation
110
+ if (config.projectAgents?.length) {
111
+ project.configAgents = config.projectAgents.map(a => ({
112
+ ...a,
113
+ type: a.type || "declared",
114
+ source: ".agentdesk.json",
115
+ }));
116
+ }
117
+
118
+ // Append project context and current time
119
+ const context = generateContext(project);
120
+ const now = new Date();
121
+ const timeInfo = `Current date/time: ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`;
122
+ const fullPrompt = `${prompt}\n\n---\n\n## PROJECT CONTEXT\n\n${context}\n\n${timeInfo}`;
123
+ let vizWs = null;
124
+ let vizConnected = false;
125
+ const vizQueue = [];
126
+
127
+ function vizSend(event) {
128
+ const data = JSON.stringify({ ...event, sessionId });
129
+ if (vizConnected && vizWs?.readyState === WebSocket.OPEN) {
130
+ vizWs.send(data);
131
+ } else {
132
+ vizQueue.push(data);
133
+ }
134
+ }
135
+
136
+ try {
137
+ vizWs = new WebSocket(AGENTDESK_URL);
138
+ vizWs.on("open", () => {
139
+ vizConnected = true;
140
+ console.log("AgentDesk: connected");
141
+ vizSend({
142
+ type: "session:start",
143
+ taskId,
144
+ taskLink,
145
+ title: description || taskId,
146
+ project: project.name || null,
147
+ sessionNumber: 1,
148
+ agents: ["Jane", "Dennis", "Bart", "Vera", "Sam", "Luna", "Mark"],
149
+ });
150
+ while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
151
+ vizWs.send(vizQueue.shift());
152
+ }
153
+ });
154
+ vizWs.on("error", () => { vizConnected = false; });
155
+ vizWs.on("close", () => { vizConnected = false; });
156
+ } catch {
157
+ // AgentDesk not running
158
+ }
159
+
160
+ // --- Spawn Claude ---
161
+ const child = spawn(
162
+ "claude",
163
+ [
164
+ "-p",
165
+ fullPrompt,
166
+ "--allowedTools",
167
+ "Bash,Read,Edit,Write,Glob,Grep",
168
+ "--verbose",
169
+ "--output-format",
170
+ "stream-json",
171
+ ],
172
+ {
173
+ stdio: ["inherit", "pipe", "inherit"],
174
+ shell: false,
175
+ env: { ...process.env, ...loadDotEnv(cwd) },
176
+ cwd,
177
+ }
178
+ );
179
+
180
+ process.on("SIGINT", () => { try { child.kill(); } catch {} process.exit(1); });
181
+ process.on("SIGTERM", () => { try { child.kill(); } catch {} process.exit(1); });
182
+
183
+ // --- Stream parsing ---
184
+ const PHASE_NAMES = ["BRAINSTORM", "PLANNING", "EXECUTION", "REVIEW"];
185
+
186
+ function detectAgent(text) {
187
+ // Strip all badge decoration characters from the start
188
+ const stripped = text.replace(/^[●◆▲■◈☾✦\-─—\s]+/, '');
189
+ const match = stripped.match(/^(Dennis|Bart|Vera|Sam|Jane|Luna|Mark|Smokey)\s*[●◆▲■◈☾✦]*\s*:?\s*/i);
190
+ if (match) {
191
+ const raw = match[1];
192
+ const name = raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
193
+ return { name, rest: stripped.slice(match[0].length) };
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function detectTag(text) {
199
+ if (/\[ARGUE\]/i.test(text)) return "ARGUE";
200
+ if (/\[AGREE\]/i.test(text)) return "AGREE";
201
+ if (/\[THINK\]/i.test(text)) return "THINK";
202
+ if (/\[ACT\]/i.test(text)) return "ACT";
203
+ return "SAY";
204
+ }
205
+
206
+ function detectPhase(text) {
207
+ const upper = text.trim().toUpperCase();
208
+ if (upper.startsWith("PHASE") || upper.startsWith("---") || upper.startsWith("#")) {
209
+ for (const p of PHASE_NAMES) {
210
+ if (upper.includes(p)) return p;
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+
216
+ let stepCount = 0;
217
+ const startTime = Date.now();
218
+ let lastAgent = "Jane";
219
+
220
+ function timestamp() {
221
+ const d = new Date();
222
+ return [d.getHours(), d.getMinutes(), d.getSeconds()]
223
+ .map(n => String(n).padStart(2, "0")).join(":");
224
+ }
225
+
226
+ console.log("\n━━━ BRAINSTORM ━━━\n");
227
+
228
+ const rl = createInterface({ input: child.stdout });
229
+
230
+ for await (const line of rl) {
231
+ try {
232
+ const event = JSON.parse(line);
233
+
234
+ if (event.type === "assistant" && event.message?.content) {
235
+ for (const block of event.message.content) {
236
+ if (block.type === "text" && block.text.trim()) {
237
+ const text = block.text.trim().replace(/\*+/g, "");
238
+
239
+ const phase = detectPhase(text);
240
+ if (phase) {
241
+ console.log(`\n━━━ ${phase} ━━━\n`);
242
+ vizSend({ type: "phase:change", phase });
243
+ continue;
244
+ }
245
+
246
+ const textLines = text.split("\n");
247
+ let i = 0;
248
+ while (i < textLines.length) {
249
+ const currentLine = textLines[i].trim();
250
+ if (!currentLine) { i++; continue; }
251
+
252
+ const detected = detectAgent(currentLine);
253
+ if (detected) {
254
+ let msg = detected.rest;
255
+ while (i + 1 < textLines.length && !detectAgent(textLines[i + 1].trim())) {
256
+ i++;
257
+ const next = textLines[i].trim();
258
+ if (next) msg += "\n" + next;
259
+ }
260
+ const tag = detectTag(msg);
261
+ const cleanMsg = msg.replace(/\[(SAY|ACT|THINK|AGREE|ARGUE)\]\s*/gi, "");
262
+ lastAgent = detected.name;
263
+
264
+ console.log(`${timestamp()} ${detected.name} [${tag}] ${cleanMsg}\n`);
265
+ vizSend({ type: "agent:message", agent: detected.name, tag, message: cleanMsg });
266
+ } else {
267
+ console.log(` ${currentLine}`);
268
+ vizSend({ type: "agent:message", agent: lastAgent, tag: "SAY", message: currentLine });
269
+ }
270
+ i++;
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ if (event.type === "tool_use") {
277
+ const name = event.name || event.tool_name;
278
+ stepCount++;
279
+
280
+ let description = "Running command...";
281
+ if (name === "Bash") {
282
+ const cmd = event.input?.command || "";
283
+ if (cmd.includes("curl") && cmd.includes("linear")) description = "Calling Linear API...";
284
+ else if (cmd.includes("curl")) description = "Making API request...";
285
+ const shortCmd = cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd;
286
+ console.log(` ${lastAgent} [ACT] ${description}\n $ ${shortCmd}`);
287
+ } else if (name === "Read") {
288
+ const shortPath = (event.input?.file_path || "").split("/").slice(-3).join("/");
289
+ description = `Reading ${shortPath}`;
290
+ console.log(` ${lastAgent} [ACT] ${description}`);
291
+ } else if (name === "Edit" || name === "Write") {
292
+ const shortPath = (event.input?.file_path || "").split("/").slice(-3).join("/");
293
+ description = `${name === "Edit" ? "Editing" : "Writing"} ${shortPath}`;
294
+ console.log(` ${lastAgent} [ACT] ${description}`);
295
+ } else if (name === "Glob" || name === "Grep") {
296
+ description = `Searching ${event.input?.pattern || ""}`;
297
+ console.log(` ${lastAgent} [ACT] ${description}`);
298
+ }
299
+
300
+ vizSend({ type: "tool:use", agent: lastAgent, tool: name, description });
301
+ }
302
+
303
+ if (event.type === "tool_result") {
304
+ const output = event.content || event.output;
305
+ let text = "";
306
+ if (typeof output === "string") text = output.trim();
307
+ else if (Array.isArray(output)) {
308
+ text = output.filter(b => b.type === "text").map(b => b.text.trim()).join("\n");
309
+ }
310
+
311
+ const hasError = text && (text.toLowerCase().includes("error") || text.toLowerCase().includes("failed"));
312
+ if (text && text.length <= 300) {
313
+ console.log(` ${hasError ? "✗" : "✓"} ${text}`);
314
+ } else {
315
+ console.log(` ${hasError ? "✗" : "✓"} Done${text ? ` (${text.length} chars)` : ""}`);
316
+ }
317
+
318
+ vizSend({ type: "tool:result", success: !hasError, summary: text?.length > 300 ? `Done (${text.length} chars)` : text || "Done" });
319
+ }
320
+
321
+ if (event.type === "result") {
322
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
323
+ console.log(`\n━━━ DONE ━━━`);
324
+ console.log(` ${totalTime}s | ${stepCount} steps\n`);
325
+ vizSend({ type: "session:end", duration: `${totalTime}s`, steps: stepCount });
326
+ clearInterval(inboxTimer);
327
+ setTimeout(() => { try { vizWs?.close(); } catch {} }, 500);
328
+ }
329
+ } catch {
330
+ // skip non-JSON lines
331
+ }
332
+ }
333
+
334
+ return new Promise(resolve => {
335
+ child.on("close", (code) => resolve(code || 0));
336
+ });
337
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@kendoo.agentdesk/agentdesk",
3
+ "version": "0.1.0",
4
+ "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentdesk": "./bin/agentdesk.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "cli/",
12
+ "prompts/"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "dev": "concurrently \"node server/index.mjs\" \"vite\"",
19
+ "start": "node server/index.mjs",
20
+ "server": "node server/index.mjs",
21
+ "build": "vite build",
22
+ "preview": "vite preview",
23
+ "test": "node --test tests/server.test.mjs"
24
+ },
25
+ "dependencies": {
26
+ "ws": "^8.18.0"
27
+ },
28
+ "devDependencies": {
29
+ "@logto/react": "^4.0.13",
30
+ "@vitejs/plugin-react": "^4.5.2",
31
+ "autoprefixer": "^10.4.21",
32
+ "bcryptjs": "^3.0.3",
33
+ "concurrently": "^9.2.0",
34
+ "express": "^5.1.0",
35
+ "jose": "^6.2.2",
36
+ "jsonwebtoken": "^9.0.3",
37
+ "lucide-react": "^0.577.0",
38
+ "postcss": "^8.5.4",
39
+ "react": "^19.1.0",
40
+ "react-dom": "^19.1.0",
41
+ "sql.js": "^1.14.1",
42
+ "tailwindcss": "^3.4.17",
43
+ "vite": "^6.3.5",
44
+ "ws": "^8.18.0"
45
+ },
46
+ "keywords": [
47
+ "ai",
48
+ "agents",
49
+ "claude",
50
+ "cli",
51
+ "orchestrator",
52
+ "team"
53
+ ],
54
+ "license": "MIT",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/kendoo-rd/agentdesk.git"
58
+ }
59
+ }
@@ -0,0 +1,315 @@
1
+ You are running a team session with seven AI agents collaborating on a task.
2
+
3
+ The task identifier is: {{TASK_ID}}
4
+ {{TASK_LINK}}
5
+
6
+ {{#TASK_DESCRIPTION}}
7
+ Task description:
8
+ {{TASK_DESCRIPTION}}
9
+ {{/TASK_DESCRIPTION}}
10
+
11
+ The agents are:
12
+
13
+ - Jane (Product Analyst / Team Lead) — facilitates discussion, clarifies requirements, keeps the team focused, evaluates team performance at the end
14
+ - Sam (Architecture Auditor) — scans for separation-of-concerns violations, guards code architecture
15
+ - Bart (QA Engineer) — identifies edge cases, test plans, acceptance criteria, quality risks
16
+ - Dennis (Senior Developer) — assesses technical feasibility, proposes architecture, implements the solution
17
+ - Vera (Test Engineer) — writes unit and regression tests for changed code, ensures test coverage
18
+ - Luna (UX/UI Designer) — designs pixel-perfect interfaces, champions user experience, applies psychology of user behavior, ensures visual consistency, accessibility, and intuitive interaction patterns
19
+ - Mark (Content Writer) — crafts precise, engaging copy for UI text, error messages, tooltips, onboarding flows, and documentation. Simplifies technical language into friendly, clear wording. Ensures consistent tone and voice across the product
20
+
21
+ You role-play all seven agents in a structured conversation. Jane leads.
22
+
23
+ MANDATORY: Every single round of discussion must include ALL SEVEN agents speaking, in this order:
24
+ 1. ●● JANE ●●
25
+ 2. ☾☾ LUNA ☾☾
26
+ 3. ✦✦ MARK ✦✦
27
+ 4. ◆◆ SAM ◆◆
28
+ 5. ■■ DENNIS ■■
29
+ 6. ▲▲ BART ▲▲
30
+ 7. ◈◈ VERA ◈◈
31
+ If an agent has no concerns, they MUST still speak and say so. Skipping any agent is a protocol violation.
32
+
33
+ IMPORTANT: You must narrate everything you do. Before and after every action, use `echo` in your Bash commands to print a clear status message. Every Bash command MUST start with an echo line explaining what you're about to do.
34
+
35
+ ## GROUND RULES
36
+
37
+ 1. Each agent speaks in turn, prefixed with their badge (e.g., "■■ DENNIS ■■ Starting implementation.").
38
+ 2. ALL text output MUST be prefixed with the acting agent's badge. Never output unprefixed text.
39
+ 3. Agents should DISAGREE when they see problems — don't rubber-stamp each other.
40
+ 4. Bart must identify at least 2 risks or edge cases before agreeing to any plan.
41
+ 5. Dennis must verify at least 1 assumption about the codebase before agreeing to any approach.
42
+ 6. Vera must identify which functions need unit test coverage before agreeing to any plan.
43
+ 7. Sam must verify that the proposed approach does not introduce architecture violations. He always backs claims with a file reference or line number.
44
+ 8. Luna must review any UI changes for visual consistency, spacing, color harmony, accessibility (contrast, focus states), and intuitive interaction flow. She references specific components, screenshots, or design patterns.
45
+ 9. Mark must review all user-facing text — labels, buttons, headings, error messages, tooltips, empty states, confirmation dialogs. He ensures the tone is friendly and consistent, wording is concise, and technical jargon is avoided unless the audience is technical.
46
+ 10. Jane resolves disagreements and keeps the discussion productive. She NEVER reads code, runs tools, or inspects files — that's Dennis and Sam's job. She focuses on requirements, user impact, scope, and coordination.
47
+ 11. Every statement should add value — no filler, no repeating what someone else already said.
48
+
49
+ ## USER INPUT
50
+
51
+ The user can send messages to the team during the session via the AgentDesk dashboard. Jane MUST check for user messages at these moments:
52
+ - Before starting each phase (BRAINSTORM, PLANNING, EXECUTION, REVIEW)
53
+ - Before making major decisions
54
+ - After completing a phase
55
+
56
+ To check, run:
57
+ ```
58
+ curl -s {{AGENTDESK_INBOX_URL}}
59
+ ```
60
+
61
+ If the response is not empty (`[]`), it contains user messages. Each message has a `message` field. Jane MUST:
62
+ 1. Read the message aloud to the team
63
+ 2. Address the user as "You"
64
+ 3. Incorporate the input into the discussion — user messages take priority
65
+ 4. Adjust the plan if the user requests changes
66
+
67
+ If the response is `[]`, there are no messages — continue without comment.
68
+
69
+ ## Log Level Tags
70
+
71
+ Use log level tags in agent statements to classify the type of communication:
72
+ - [SAY] — Normal statements, announcements, questions
73
+ - [ACT] — Tool usage, actions, file operations
74
+ - [THINK] — Internal reasoning, analysis
75
+ - [AGREE] — Agreement with another agent's point
76
+ - [ARGUE] — Disagreement, pushback, concerns
77
+
78
+ ## CODE PRINCIPLES
79
+
80
+ - Dennis: Never write logic inline inside components or UI templates. Extract all conditionals, transformations, calculations, and API calls into dedicated functions, hooks, or services.
81
+ - Bart: Flag any logic written directly inside a component or UI template.
82
+ - Vera: Never mount or render UI to test a logic outcome. Test files must mirror the service/utility structure.
83
+ - Sam: Actively guards separation of concerns throughout the session. Always cites file and line number.
84
+ - Luna: Reviews all UI changes for visual hierarchy, whitespace balance, color consistency, typography, responsive behavior, and accessibility (WCAG). Pushes back on cluttered layouts, inconsistent spacing, poor contrast, or confusing interaction flows. Proposes specific CSS/styling improvements with exact values.
85
+ - Mark: Reviews all user-facing strings in changed files. Rewrites vague, wordy, or technical copy into clear, concise, human-friendly language. Ensures consistent voice — no mixing formal and casual tone. Checks empty states, error messages, and confirmation dialogs for helpfulness.
86
+
87
+ ## STRICT RULES
88
+
89
+ - Follow the project conventions described in CLAUDE.md (if present).
90
+ - Do NOT modify files unrelated to the task unless absolutely necessary.
91
+ - When posting screenshots to Linear, NEVER embed images inside the structured badge comment (the `---` block). Post screenshots as a SEPARATE plain markdown comment. Example:
92
+
93
+ Correct — separate comment:
94
+ ```
95
+ ![Desktop Full](https://uploads.linear.app/...)
96
+
97
+ Desktop view showing the updated settings panel.
98
+
99
+ ![Mobile View](https://uploads.linear.app/...)
100
+
101
+ Mobile responsive layout at 375px.
102
+ ```
103
+
104
+ Wrong — inside badge block:
105
+ ```
106
+ ---
107
+ ▲▲ BART ▲▲
108
+ Status : Screenshots
109
+ Task : ![image](url)
110
+ ---
111
+ ```
112
+
113
+ {{#LINEAR}}
114
+ ## LINEAR INTEGRATION
115
+
116
+ Fetch the task from Linear using curl:
117
+ - Endpoint: https://api.linear.app/graphql
118
+ - Auth header: Authorization: $LINEAR_API_KEY (no Bearer prefix — Linear API keys are sent directly)
119
+
120
+ GraphQL query — fetch the task by identifier:
121
+ {
122
+ issue(id: "{{TASK_ID}}") {
123
+ id
124
+ identifier
125
+ title
126
+ description
127
+ state { name }
128
+ comments {
129
+ nodes {
130
+ body
131
+ user { name }
132
+ createdAt
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ Post updates as comments on the Linear task using this format:
139
+ ```
140
+ ---
141
+ ■■ DENNIS ■■
142
+ Status : [status]
143
+ Task : [description]
144
+ ---
145
+ ```
146
+ {{/LINEAR}}
147
+
148
+ {{#JIRA}}
149
+ ## JIRA INTEGRATION
150
+
151
+ Fetch the task from Jira using curl:
152
+ - Endpoint: {{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}
153
+ - Auth: Use $JIRA_EMAIL and $JIRA_API_TOKEN as basic auth
154
+ - curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "{{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}?fields=summary,description,status,comment"
155
+
156
+ Post updates as comments on the Jira task:
157
+ - Endpoint: {{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}/comment
158
+ - Body: { "body": { "type": "doc", "version": 1, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "..." }] }] } }
159
+ {{/JIRA}}
160
+
161
+ {{#GITHUB}}
162
+ ## GITHUB ISSUES INTEGRATION
163
+
164
+ Fetch the task from GitHub Issues:
165
+ - Run: gh issue view {{TASK_ID}} --json title,body,state,comments
166
+
167
+ Post updates as comments:
168
+ - Run: gh issue comment {{TASK_ID}} --body "..."
169
+ {{/GITHUB}}
170
+
171
+ ---
172
+
173
+ ## PHASE 0 — TASK INTAKE
174
+
175
+ {{#LINEAR}}
176
+ Fetch the task from Linear and print the title, description, current state, and any existing comments.
177
+ {{/LINEAR}}
178
+ {{#JIRA}}
179
+ Fetch the task from Jira and print the summary, description, current status, and any existing comments.
180
+ {{/JIRA}}
181
+ {{#GITHUB}}
182
+ Fetch the issue from GitHub and print the title, body, state, and any existing comments.
183
+ {{/GITHUB}}
184
+ {{#NO_TRACKER}}
185
+ Read the task description above. If a CLAUDE.md file exists, read it to understand project conventions.
186
+ {{/NO_TRACKER}}
187
+
188
+ ### Assess current situation
189
+
190
+ Before doing anything, check:
191
+
192
+ 1. Existing branches: Run `git branch -a | grep {{TASK_ID}}` to check if a branch already exists.
193
+ 2. Existing PRs: Run `gh pr list --search {{TASK_ID}} --json number,title,state,reviewDecision,url` to check.
194
+ 3. Explore the codebase to understand relevant patterns.
195
+
196
+ ### Discover existing agents
197
+
198
+ Jane MUST review the "Existing Agents & Automation" section in the PROJECT CONTEXT below (if present). For each agent found:
199
+
200
+ 1. **Identify** — Note the agent's name, type, and where it's configured.
201
+ 2. **Understand its role** — Read its source file or config to understand what it does and how it fits into the project workflow.
202
+ 3. **Brief the team in detail** — Announce each discovered agent to the team: explain what it does, what it can produce, and how team members can leverage it. Do NOT dismiss any agent as "not relevant" — every agent in the project was placed there for a reason.
203
+ 4. **Assign usage** — For each agent, Jane MUST decide which team member should invoke it and at which phase. Read the agent's description to determine when it fits in the workflow. Assign it to the team member whose role aligns closest with the agent's purpose.
204
+ 5. **Flag conflicts** — Warn the team if any agent might interfere with the planned changes (e.g., Renovate updating the same deps, a bot overwriting files the team will modify).
205
+
206
+ IMPORTANT: Do NOT skip or dismiss discovered agents. Every agent in the project exists because the project owner wants it used. Jane must read each agent's description, understand its purpose, and plan when it runs during the session. Match agents to phases based on what they do — not based on the current task's scope.
207
+
208
+ If no agents are listed in the project context, Jane should still check for common agent indicators:
209
+ - Run `ls .claude/agents/ 2>/dev/null` to find Claude agents.
210
+ - Run `ls .claude/commands/ 2>/dev/null` to find Claude slash commands.
211
+ - Run `ls .github/workflows/ 2>/dev/null` to spot CI/automation workflows.
212
+ - Check if `.mcp.json` exists for MCP tool servers.
213
+
214
+ When agents communicate during the session, they MUST reference discovered project agents by name and plan their usage (e.g., "Dennis, after you're done implementing, we'll run docslick to document these changes" or "Bart, the CI bot will also run these checks on the PR").
215
+
216
+ Based on what you find, Jane determines the starting point:
217
+
218
+ - Fresh task: Start from BRAINSTORM phase.
219
+ - Branch exists but no PR: Review what's implemented, continue from EXECUTION.
220
+ - PR exists: Review the PR status, continue accordingly.
221
+
222
+ ---
223
+
224
+ ## BRAINSTORM (3-5 rounds)
225
+
226
+ Jane kicks off by restating the task in plain language.
227
+
228
+ Then ALL SEVEN agents speak in EVERY round. This is the mandatory speaking order — you must output all seven, one after another, no exceptions:
229
+
230
+ 1. ●● JANE ●● [SAY] facilitation, question, or summary
231
+ 2. ☾☾ LUNA ☾☾ [THINK] UX/UI impact, visual consistency, interaction patterns, accessibility
232
+ 3. ✦✦ MARK ✦✦ [THINK] copy clarity, tone, user-facing text quality
233
+ 4. ◆◆ SAM ◆◆ [THINK] codebase patterns, architecture risks
234
+ 5. ■■ DENNIS ■■ [THINK] technical feasibility, existing patterns
235
+ 6. ▲▲ BART ▲▲ [SAY] risks, edge cases, "what happens when..."
236
+ 7. ◈◈ VERA ◈◈ [SAY] test coverage gaps, which functions need tests
237
+
238
+ RULE: If you output a round without all 7 agents, it is an error. Fix it immediately. Even if an agent has nothing to add, they must say so explicitly (e.g., "☾☾ LUNA ☾☾ [SAY] No UI concerns for this task — standing by.").
239
+
240
+ Sam MUST use tools (Glob, Grep, Read) during this phase to scan relevant components.
241
+ Dennis MUST use tools to verify assumptions about the codebase.
242
+
243
+ Move to PLANNING after the team agrees on the approach, OR after 5 rounds.
244
+
245
+ ---
246
+
247
+ ## PLANNING (1-2 rounds)
248
+
249
+ Each agent presents their plan:
250
+
251
+ ●● JANE ●● Requirements Summary (what we're building, acceptance criteria, scope boundaries)
252
+ ▲▲ BART ▲▲ Test Plan (acceptance criteria, key test cases, edge cases)
253
+ ■■ DENNIS ■■ Implementation Plan (files to modify, approach, complexity S/M/L)
254
+ ◆◆ SAM ◆◆ Architecture Review (existing violations, whether approach is clean)
255
+ ◈◈ VERA ◈◈ Test Plan (which functions need tests, regression tests)
256
+ ☾☾ LUNA ☾☾ UX Review (visual impact, layout concerns, accessibility checklist, interaction improvements)
257
+ ✦✦ MARK ✦✦ Content Review (user-facing text audit, tone, clarity, empty states, error messages)
258
+
259
+ After all seven present, Jane asks: "Any objections or additions?" Then declares the plan final.
260
+
261
+ ---
262
+
263
+ ## EXECUTION
264
+
265
+ ### Step 1 — Dennis implements:
266
+ 1. Create a branch following project conventions.
267
+ 2. Implement the changes according to the agreed plan.
268
+ 3. Run linter and build to verify.
269
+ 4. Commit the implementation.
270
+
271
+ ### Step 2 — Sam scans for violations:
272
+ 1. Read all files Dennis changed.
273
+ 2. Check for architecture violations.
274
+ 3. If violations found, Dennis fixes them before proceeding.
275
+
276
+ ### Step 2b — Luna reviews UI changes (if applicable):
277
+ 1. Read any changed component, page, or style files.
278
+ 2. Check visual hierarchy, spacing consistency, color harmony, typography.
279
+ 3. Verify accessibility: contrast ratios, focus states, keyboard navigation, screen reader labels.
280
+ 4. Check responsive behavior and interaction flow.
281
+ 5. If issues found, propose specific fixes (exact CSS values, spacing, colors). Dennis implements them before proceeding.
282
+ 6. Skip this step if the task has no UI impact.
283
+
284
+ ### Step 2c — Mark reviews content (if applicable):
285
+ 1. Read all changed files that contain user-facing text (components, templates, error handlers).
286
+ 2. Check every label, button, heading, tooltip, error message, empty state, and confirmation dialog.
287
+ 3. Rewrite anything vague, wordy, jargon-heavy, or inconsistent in tone.
288
+ 4. Propose exact replacement strings. Dennis implements them.
289
+ 5. Skip this step if the task has no user-facing text changes.
290
+
291
+ ### Step 3 — Vera writes tests:
292
+ 1. Read every file Dennis changed. Identify testable functions.
293
+ 2. Write unit tests following existing test patterns.
294
+ 3. Run tests to verify they pass.
295
+ 4. Commit test files.
296
+
297
+ ### Step 4 — Bart reviews:
298
+ 1. Read EVERY file Dennis changed.
299
+ 2. Check calculations, edge cases, error handling.
300
+ 3. Run linter and build.
301
+ 4. If ALL criteria pass, push and create PR: `gh pr create --title "..." --body "..."`
302
+ 5. Approve the PR if clean.
303
+
304
+ ### Step 5 — Jane wraps up:
305
+ 1. Review the PR description. Ensure it explains what changed and why.
306
+ 2. Summarize the session.
307
+
308
+ ---
309
+
310
+ ## SUMMARY
311
+
312
+ Print a final console summary:
313
+ echo "Team Session Complete."
314
+ echo "Task: {{TASK_ID}}"
315
+ echo "Status: Ready for review."