heyio 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/copilot/agents.js +210 -10
- package/dist/copilot/orchestrator.js +42 -2
- package/dist/copilot/system-message.js +35 -15
- package/dist/copilot/tools.js +88 -3
- package/dist/store/db.js +11 -0
- package/dist/store/squads.js +18 -0
- package/dist/store/tasks.js +14 -0
- package/package.json +1 -1
package/dist/copilot/agents.js
CHANGED
|
@@ -8,8 +8,8 @@ import { defineTool, approveAll } from "@github/copilot-sdk";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { getClient } from "./client.js";
|
|
10
10
|
import { getModelForTask, getModelForTier, classifyComplexity } from "./model-router.js";
|
|
11
|
-
import { getSquad, updateSquadSession, updateSquadStatus, getDecisionsSummary, logDecision, listSquadAgents, getSquadAgent, updateAgentSession, updateAgentStatus, } from "../store/squads.js";
|
|
12
|
-
import { createTask, completeTask, failTask, getActiveTasks, getTask, cancelTask, } from "../store/tasks.js";
|
|
11
|
+
import { getSquad, updateSquadSession, updateSquadStatus, getDecisionsSummary, logDecision, listSquadAgents, getSquadAgent, getSquadLead, updateAgentSession, updateAgentStatus, } from "../store/squads.js";
|
|
12
|
+
import { createTask, completeTask, createReview, failTask, getActiveTasks, getTask, cancelTask, } from "../store/tasks.js";
|
|
13
13
|
import { SESSIONS_DIR } from "../paths.js";
|
|
14
14
|
import { getUniverse } from "./universes.js";
|
|
15
15
|
// Key format: "squadSlug:characterName" for per-agent sessions, "squadSlug" for legacy
|
|
@@ -116,10 +116,17 @@ export async function delegateToAgent(squadSlug, task, onComplete, targetAgent)
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
else {
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
// Prefer the designated team lead if one exists; otherwise fall back to
|
|
120
|
+
// the first idle agent (or just the first agent on the roster).
|
|
121
|
+
const lead = getSquadLead(squadSlug);
|
|
122
|
+
if (lead) {
|
|
123
|
+
agent = lead;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const agents = listSquadAgents(squadSlug);
|
|
127
|
+
if (agents.length > 0) {
|
|
128
|
+
agent = agents.find((a) => a.status === "idle") ?? agents[0];
|
|
129
|
+
}
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
const session = agent
|
|
@@ -159,6 +166,19 @@ export async function delegateToAgent(squadSlug, task, onComplete, targetAgent)
|
|
|
159
166
|
if (agent)
|
|
160
167
|
updateAgentStatus(squadSlug, agent.character_name, "idle");
|
|
161
168
|
recordTaskEvent(taskId, { ts: Date.now(), type: "task.done", data: { result } });
|
|
169
|
+
try {
|
|
170
|
+
await runPeerReview(squadSlug, agent?.character_name ?? "", taskId, task, result);
|
|
171
|
+
}
|
|
172
|
+
catch (reviewErr) {
|
|
173
|
+
console.error("[io] Peer review error:", reviewErr instanceof Error ? reviewErr.message : reviewErr);
|
|
174
|
+
recordTaskEvent(taskId, {
|
|
175
|
+
ts: Date.now(),
|
|
176
|
+
type: "task.review_error",
|
|
177
|
+
data: {
|
|
178
|
+
error: reviewErr instanceof Error ? reviewErr.message : String(reviewErr),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
162
182
|
onComplete(taskId, result);
|
|
163
183
|
}
|
|
164
184
|
catch (err) {
|
|
@@ -241,7 +261,34 @@ async function getOrCreateAgentSession(squadSlug, agent, taskDescription) {
|
|
|
241
261
|
const universeName = squad.universe
|
|
242
262
|
? getUniverse(squad.universe)?.name ?? squad.universe
|
|
243
263
|
: "Unknown";
|
|
244
|
-
const
|
|
264
|
+
const isLead = agent.is_lead === 1;
|
|
265
|
+
const agentTools = buildAgentTools(squadSlug, isLead);
|
|
266
|
+
let leadSection = "";
|
|
267
|
+
if (isLead) {
|
|
268
|
+
const teammates = listSquadAgents(squadSlug).filter((a) => a.character_name !== agent.character_name);
|
|
269
|
+
const roster = teammates.length > 0
|
|
270
|
+
? teammates
|
|
271
|
+
.map((t) => {
|
|
272
|
+
const charter = t.charter
|
|
273
|
+
? t.charter.length > 200
|
|
274
|
+
? t.charter.slice(0, 200) + "…"
|
|
275
|
+
: t.charter
|
|
276
|
+
: "(no charter)";
|
|
277
|
+
return `- **${t.character_name}** — ${t.role_title}: ${charter}`;
|
|
278
|
+
})
|
|
279
|
+
.join("\n")
|
|
280
|
+
: "_(no other agents on this squad yet — ask IO to add some)_";
|
|
281
|
+
leadSection = `
|
|
282
|
+
|
|
283
|
+
## Team Lead Role
|
|
284
|
+
You are the team lead for this squad. When you receive a task, your job is to:
|
|
285
|
+
1. Break it down into concrete subtasks
|
|
286
|
+
2. Assign each subtask to the most appropriate teammate using the \`delegate_to_teammate\` tool
|
|
287
|
+
3. Collect results and synthesize a final summary
|
|
288
|
+
|
|
289
|
+
## Your Team
|
|
290
|
+
${roster}`;
|
|
291
|
+
}
|
|
245
292
|
const systemMessage = `You are ${agent.character_name}, a specialist agent on the "${squad.name}" project team (${universeName} universe).
|
|
246
293
|
|
|
247
294
|
## Your Identity
|
|
@@ -256,7 +303,7 @@ ${agent.charter ?? "General-purpose agent. Handle tasks as they come."}
|
|
|
256
303
|
- **Path**: ${squad.project_path}
|
|
257
304
|
|
|
258
305
|
## Past Decisions
|
|
259
|
-
${decisions}
|
|
306
|
+
${decisions}${leadSection}
|
|
260
307
|
|
|
261
308
|
## Instructions
|
|
262
309
|
You are a coding agent. Use the shell tool to run commands and file_ops to read/write files.
|
|
@@ -343,7 +390,7 @@ Log important decisions with squad_log_decision so they persist.`,
|
|
|
343
390
|
agentSessions.set(squadSlug, session);
|
|
344
391
|
return session;
|
|
345
392
|
}
|
|
346
|
-
function buildAgentTools(squadSlug) {
|
|
393
|
+
function buildAgentTools(squadSlug, isLead = false) {
|
|
347
394
|
const shell = defineTool("shell", {
|
|
348
395
|
description: "Run a shell command. Use for git, build tools, file operations, etc.",
|
|
349
396
|
skipPermission: true,
|
|
@@ -461,7 +508,50 @@ function buildAgentTools(squadSlug) {
|
|
|
461
508
|
}
|
|
462
509
|
},
|
|
463
510
|
});
|
|
464
|
-
|
|
511
|
+
const tools = [shell, fileOps, squadLogDecision];
|
|
512
|
+
if (isLead) {
|
|
513
|
+
const delegateToTeammate = defineTool("delegate_to_teammate", {
|
|
514
|
+
description: "Delegate a subtask to a teammate on this squad. The teammate runs the task synchronously and returns its result. Use this to divvy work as the team lead.",
|
|
515
|
+
skipPermission: true,
|
|
516
|
+
parameters: z.object({
|
|
517
|
+
teammate: z
|
|
518
|
+
.string()
|
|
519
|
+
.describe("The teammate's character_name (e.g., 'Optimus Prime')"),
|
|
520
|
+
task: z
|
|
521
|
+
.string()
|
|
522
|
+
.describe("The concrete task or subtask the teammate should perform"),
|
|
523
|
+
}),
|
|
524
|
+
handler: async ({ teammate, task }) => {
|
|
525
|
+
try {
|
|
526
|
+
const teammateAgent = getSquadAgent(squadSlug, teammate);
|
|
527
|
+
if (!teammateAgent) {
|
|
528
|
+
return `Error: teammate "${teammate}" not found in squad "${squadSlug}". Use squad_agents to list the roster.`;
|
|
529
|
+
}
|
|
530
|
+
if (teammateAgent.is_lead === 1) {
|
|
531
|
+
return `Error: "${teammate}" is the team lead. Delegate to a non-lead teammate.`;
|
|
532
|
+
}
|
|
533
|
+
updateAgentStatus(squadSlug, teammateAgent.character_name, "working");
|
|
534
|
+
try {
|
|
535
|
+
const session = await getOrCreateAgentSession(squadSlug, teammateAgent, task);
|
|
536
|
+
const response = await session.sendAndWait({ prompt: task }, 300_000);
|
|
537
|
+
const result = response?.data?.content ?? "(teammate returned no output)";
|
|
538
|
+
updateAgentStatus(squadSlug, teammateAgent.character_name, "idle");
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
updateAgentStatus(squadSlug, teammateAgent.character_name, "error");
|
|
543
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
544
|
+
return `Error from teammate "${teammate}": ${message}`;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
tools.push(delegateToTeammate);
|
|
553
|
+
}
|
|
554
|
+
return tools;
|
|
465
555
|
}
|
|
466
556
|
function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
467
557
|
if (depth >= maxDepth)
|
|
@@ -481,6 +571,116 @@ function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
|
481
571
|
}
|
|
482
572
|
return results;
|
|
483
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Run a peer review phase after a task completes. Every other agent on the
|
|
576
|
+
* squad reviews the work and votes APPROVED / REJECTED. QA agents
|
|
577
|
+
* (is_qa === 1) have veto power: if any QA agent rejects, the PR is left as
|
|
578
|
+
* draft. Otherwise, any GitHub PR URL found in the task result is promoted
|
|
579
|
+
* from draft to ready via `gh pr ready`.
|
|
580
|
+
*/
|
|
581
|
+
async function runPeerReview(squadSlug, originalAgentCharacter, taskId, taskDescription, taskResult) {
|
|
582
|
+
const reviewers = listSquadAgents(squadSlug).filter((a) => a.character_name !== originalAgentCharacter);
|
|
583
|
+
if (reviewers.length === 0) {
|
|
584
|
+
recordTaskEvent(taskId, {
|
|
585
|
+
ts: Date.now(),
|
|
586
|
+
type: "task.review_complete",
|
|
587
|
+
data: { promoted: false, reason: "No other agents to review" },
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const reviewPrompt = `You are reviewing the following completed task:
|
|
592
|
+
|
|
593
|
+
## Task
|
|
594
|
+
${taskDescription}
|
|
595
|
+
|
|
596
|
+
## Work Done
|
|
597
|
+
${taskResult}
|
|
598
|
+
|
|
599
|
+
Review the work. Respond with:
|
|
600
|
+
- First line: APPROVED or REJECTED
|
|
601
|
+
- Remaining lines: your review comments`;
|
|
602
|
+
const reviews = [];
|
|
603
|
+
for (const reviewer of reviewers) {
|
|
604
|
+
try {
|
|
605
|
+
const session = await getOrCreateAgentSession(squadSlug, reviewer, `Peer review of task ${taskId}`);
|
|
606
|
+
const response = await session.sendAndWait({ prompt: reviewPrompt }, 300_000);
|
|
607
|
+
const content = response?.data?.content ?? "";
|
|
608
|
+
const lines = content.split(/\r?\n/);
|
|
609
|
+
const firstLine = (lines[0] ?? "").trim().toUpperCase();
|
|
610
|
+
const approved = firstLine.includes("APPROVED") && !firstLine.includes("REJECTED");
|
|
611
|
+
const comments = lines.slice(1).join("\n").trim() || null;
|
|
612
|
+
createReview(taskId, squadSlug, reviewer.character_name, approved, comments ?? undefined);
|
|
613
|
+
recordTaskEvent(taskId, {
|
|
614
|
+
ts: Date.now(),
|
|
615
|
+
type: "task.review",
|
|
616
|
+
data: {
|
|
617
|
+
reviewer: reviewer.character_name,
|
|
618
|
+
is_qa: reviewer.is_qa === 1,
|
|
619
|
+
approved,
|
|
620
|
+
comments,
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
reviews.push({
|
|
624
|
+
reviewer: reviewer.character_name,
|
|
625
|
+
is_qa: reviewer.is_qa === 1,
|
|
626
|
+
approved,
|
|
627
|
+
comments: comments ?? "",
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
632
|
+
console.error(`[io] Reviewer ${reviewer.character_name} failed:`, message);
|
|
633
|
+
recordTaskEvent(taskId, {
|
|
634
|
+
ts: Date.now(),
|
|
635
|
+
type: "task.review_error",
|
|
636
|
+
data: { reviewer: reviewer.character_name, error: message },
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const qaRejection = reviews.find((r) => r.is_qa && !r.approved);
|
|
641
|
+
const prMatch = taskResult.match(/https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/);
|
|
642
|
+
if (qaRejection) {
|
|
643
|
+
recordTaskEvent(taskId, {
|
|
644
|
+
ts: Date.now(),
|
|
645
|
+
type: "task.review_complete",
|
|
646
|
+
data: {
|
|
647
|
+
promoted: false,
|
|
648
|
+
reason: `QA veto from ${qaRejection.reviewer}`,
|
|
649
|
+
prUrl: prMatch ? prMatch[0] : null,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (!prMatch) {
|
|
655
|
+
recordTaskEvent(taskId, {
|
|
656
|
+
ts: Date.now(),
|
|
657
|
+
type: "task.review_complete",
|
|
658
|
+
data: { promoted: false, reason: "No PR URL found in task result" },
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const [prUrl, owner, repo, prNumber] = prMatch;
|
|
663
|
+
try {
|
|
664
|
+
execSync(`gh pr ready ${prNumber} --repo ${owner}/${repo}`, {
|
|
665
|
+
encoding: "utf-8",
|
|
666
|
+
timeout: 30_000,
|
|
667
|
+
env: { ...process.env, HOME: process.env.HOME || homedir() },
|
|
668
|
+
});
|
|
669
|
+
recordTaskEvent(taskId, {
|
|
670
|
+
ts: Date.now(),
|
|
671
|
+
type: "task.review_complete",
|
|
672
|
+
data: { promoted: true, prUrl },
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
677
|
+
recordTaskEvent(taskId, {
|
|
678
|
+
ts: Date.now(),
|
|
679
|
+
type: "task.review_complete",
|
|
680
|
+
data: { promoted: false, reason: `gh pr ready failed: ${message}`, prUrl },
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
484
684
|
/**
|
|
485
685
|
* Cancel a running agent task by aborting its session and marking the task
|
|
486
686
|
* cancelled. Returns true if the task existed and was running.
|
|
@@ -3,8 +3,8 @@ import { approveAll, } from "@github/copilot-sdk";
|
|
|
3
3
|
import { config } from "../config.js";
|
|
4
4
|
import { SESSIONS_DIR, IO_VERSION } from "../paths.js";
|
|
5
5
|
import { getState, setState, deleteState, logConversation } from "../store/db.js";
|
|
6
|
-
import { clearStaleTasks, getTask } from "../store/tasks.js";
|
|
7
|
-
import { getSquad, listSquads, createSquad, deleteSquad, logDecision, getDecisionsSummary, updateSquadStatus, addSquadAgent, listSquadAgents, removeSquadAgent, } from "../store/squads.js";
|
|
6
|
+
import { clearStaleTasks, getTask, getTaskReviews } from "../store/tasks.js";
|
|
7
|
+
import { getSquad, listSquads, createSquad, deleteSquad, logDecision, getDecisionsSummary, updateSquadStatus, addSquadAgent, listSquadAgents, removeSquadAgent, setSquadLead, getSquadLead, setSquadQA, } from "../store/squads.js";
|
|
8
8
|
import { readPage, writePage, assertPagePath, deletePage, listPages } from "../wiki/fs.js";
|
|
9
9
|
import { resolveModelTiers } from "./model-router.js";
|
|
10
10
|
import { searchWiki, getWikiSummary } from "../wiki/search.js";
|
|
@@ -73,8 +73,23 @@ function getToolDeps() {
|
|
|
73
73
|
model_tier: a.model_tier,
|
|
74
74
|
personality: a.personality,
|
|
75
75
|
status: a.status,
|
|
76
|
+
is_lead: a.is_lead,
|
|
77
|
+
is_qa: a.is_qa,
|
|
76
78
|
})),
|
|
77
79
|
removeSquadAgent,
|
|
80
|
+
setSquadLead,
|
|
81
|
+
getSquadLead: (slug) => {
|
|
82
|
+
const lead = getSquadLead(slug);
|
|
83
|
+
return lead
|
|
84
|
+
? { character_name: lead.character_name, role_title: lead.role_title }
|
|
85
|
+
: undefined;
|
|
86
|
+
},
|
|
87
|
+
setSquadQA,
|
|
88
|
+
getTaskReviews: (taskId) => getTaskReviews(taskId).map((r) => ({
|
|
89
|
+
reviewer_character: r.reviewer_character,
|
|
90
|
+
approved: r.approved,
|
|
91
|
+
comments: r.comments,
|
|
92
|
+
})),
|
|
78
93
|
listSkills,
|
|
79
94
|
installSkill,
|
|
80
95
|
removeSkill,
|
|
@@ -92,6 +107,30 @@ function toolFingerprint(tools) {
|
|
|
92
107
|
const names = (tools ?? []).map((t) => t.name).sort().join(",");
|
|
93
108
|
return crypto.createHash("sha256").update(`${IO_VERSION}:${names}`).digest("hex").slice(0, 16);
|
|
94
109
|
}
|
|
110
|
+
function buildSquadRoster() {
|
|
111
|
+
const squads = listSquads();
|
|
112
|
+
if (squads.length === 0)
|
|
113
|
+
return "";
|
|
114
|
+
return squads
|
|
115
|
+
.map((s) => {
|
|
116
|
+
const agents = listSquadAgents(s.slug);
|
|
117
|
+
const lead = agents.find((a) => a.is_lead === 1);
|
|
118
|
+
const agentList = agents
|
|
119
|
+
.map((a) => {
|
|
120
|
+
const badges = [
|
|
121
|
+
a.is_lead === 1 ? "⭐ LEAD" : "",
|
|
122
|
+
a.is_qa === 1 ? "🛡️ QA" : "",
|
|
123
|
+
]
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join(", ");
|
|
126
|
+
return ` - ${a.character_name} (${a.role_title})${badges ? ` [${badges}]` : ""}`;
|
|
127
|
+
})
|
|
128
|
+
.join("\n");
|
|
129
|
+
const leadLine = lead ? `\nTeam Lead: ${lead.character_name}` : "";
|
|
130
|
+
return `**${s.name}** (\`${s.slug}\`) — ${s.status}\n📁 ${s.project_path}${leadLine}\n${agentList || " _(no agents yet)_"}`;
|
|
131
|
+
})
|
|
132
|
+
.join("\n\n");
|
|
133
|
+
}
|
|
95
134
|
function buildFullSessionConfig() {
|
|
96
135
|
const { tools, skillDirectories } = getSessionConfig();
|
|
97
136
|
return {
|
|
@@ -102,6 +141,7 @@ function buildFullSessionConfig() {
|
|
|
102
141
|
content: getOrchestratorSystemMessage({
|
|
103
142
|
selfEditEnabled: config.selfEditEnabled,
|
|
104
143
|
memorySummary: getWikiSummary() || undefined,
|
|
144
|
+
squadRoster: buildSquadRoster() || undefined,
|
|
105
145
|
}),
|
|
106
146
|
},
|
|
107
147
|
tools,
|
|
@@ -18,8 +18,8 @@ This restriction does NOT apply to:
|
|
|
18
18
|
- Any files outside the IO installation directory
|
|
19
19
|
`;
|
|
20
20
|
const squadBlock = opts?.squadRoster
|
|
21
|
-
? `\n
|
|
22
|
-
:
|
|
21
|
+
? `\n## Active Squads\nThe following squads are available. Route relevant coding requests directly to them.\n\n${opts.squadRoster}\n`
|
|
22
|
+
: `\n## Active Squads\nNo squads created yet. Use \`squad_create\` to set up a project squad.\n`;
|
|
23
23
|
const osName = process.platform === "darwin" ? "macOS"
|
|
24
24
|
: process.platform === "win32" ? "Windows"
|
|
25
25
|
: "Linux";
|
|
@@ -47,12 +47,22 @@ When no source tag is present, assume TUI.
|
|
|
47
47
|
|
|
48
48
|
## Your Role
|
|
49
49
|
|
|
50
|
-
You receive messages and decide how to handle them:
|
|
50
|
+
You receive messages and decide how to handle them based on a strict routing priority:
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
52
|
+
### 1. Squad routing (highest priority)
|
|
53
|
+
If **active squads are listed below** and the request is clearly related to one of those projects (coding tasks, bugs, features, issues, PRs, architecture), **delegate immediately to that squad's team lead** using \`squad_delegate\` — do NOT plan the work yourself, do NOT break it into subtasks. Just pass the full request to the lead and let the team handle it internally.
|
|
54
|
+
|
|
55
|
+
- The team lead will plan and delegate internally to teammates.
|
|
56
|
+
- You do not need to understand the full scope of the work — the lead does.
|
|
57
|
+
- Multiple squads? Pick the one whose \`project_path\` / name best matches the request.
|
|
58
|
+
|
|
59
|
+
### 2. Direct tools (medium priority)
|
|
60
|
+
For tasks that require shell access, file operations, web lookups, or knowledge base updates — and that are NOT squad project work — use your tools directly. Use a skill if one is available for the task.
|
|
61
|
+
|
|
62
|
+
### 3. Direct answer (lowest priority)
|
|
63
|
+
For general questions, conversation, status checks, or anything outside the scope of a squad's project — answer directly.
|
|
64
|
+
|
|
65
|
+
> **Rule**: If a squad exists that covers the topic, always delegate. Never plan or implement squad work yourself.
|
|
56
66
|
${squadBlock}
|
|
57
67
|
## Squad System
|
|
58
68
|
|
|
@@ -70,14 +80,24 @@ Squads are persistent project teams with **named specialist agents**. Each squad
|
|
|
70
80
|
- Check overall status with \`squad_status\`.
|
|
71
81
|
|
|
72
82
|
### Delegating Work
|
|
73
|
-
|
|
74
|
-
1.
|
|
75
|
-
2.
|
|
76
|
-
3.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
**Do not plan squad work yourself.** When a squad-relevant request arrives:
|
|
84
|
+
1. Call \`squad_delegate\` with the squad slug and the full request (as-is from the user). Do NOT specify an agent — let it route to the team lead automatically.
|
|
85
|
+
2. The team lead breaks it down and delegates to teammates internally via \`delegate_to_teammate\`. If no lead is designated, the system falls back to the first idle agent.
|
|
86
|
+
3. Use \`squad_task_status\` to monitor progress and report results back to the user.
|
|
87
|
+
|
|
88
|
+
Only specify an \`agent\` when the user **explicitly asks** to target a specific squad member by name.
|
|
89
|
+
|
|
90
|
+
### Team Leads
|
|
91
|
+
Every squad should have a **team lead**. After building the team with \`squad_add_agent\`, designate one agent as the lead using \`squad_set_lead\`. The lead receives delegated tasks (when no specific agent is targeted), breaks them into subtasks, and assigns work to teammates via the lead-only \`delegate_to_teammate\` tool. This keeps coordination inside the squad rather than forcing IO to micro-manage assignments.
|
|
92
|
+
|
|
93
|
+
### Peer Review & QA Approvals
|
|
94
|
+
When an agent finishes a task, the other squad members automatically review the work and vote APPROVED or REJECTED. Reviews are recorded and emitted as \`task.review\` events.
|
|
95
|
+
|
|
96
|
+
- Designate QA reviewers with \`squad_set_qa\` — typically agents focused on testing, security, or quality.
|
|
97
|
+
- **QA agents have veto power**: if any QA reviewer rejects, the PR stays as a draft.
|
|
98
|
+
- Non-QA rejections are advisory — they're recorded but don't block promotion.
|
|
99
|
+
- When all QA approvals pass (or no QA agents exist) and the task result contains a GitHub PR URL, the PR is automatically promoted from draft to ready via \`gh pr ready\`.
|
|
100
|
+
- Use \`squad_task_reviews\` to inspect the reviews on any completed task.
|
|
81
101
|
|
|
82
102
|
### Agent Roles Are Dynamic
|
|
83
103
|
**Do NOT use generic roles** like "developer" or "tester". Analyze the project first and create roles that match its actual technology stack. Examples:
|
package/dist/copilot/tools.js
CHANGED
|
@@ -111,10 +111,14 @@ export function createTools(deps) {
|
|
|
111
111
|
? UNIVERSES.find((u) => u.id === s.universe)?.name ?? s.universe
|
|
112
112
|
: "none";
|
|
113
113
|
const agents = deps.listSquadAgents(s.slug);
|
|
114
|
+
const lead = deps.getSquadLead(s.slug);
|
|
115
|
+
const leadLine = lead
|
|
116
|
+
? `\n ⭐ Team Lead: ${lead.character_name} (${lead.role_title})`
|
|
117
|
+
: "";
|
|
114
118
|
const agentList = agents.length > 0
|
|
115
119
|
? "\n Agents: " + agents.map((a) => `${a.character_name} (${a.role_title})`).join(", ")
|
|
116
120
|
: "\n Agents: none — use squad_add_agent to build the team";
|
|
117
|
-
return `- **${s.name}** (\`${s.slug}\`) — ${s.status} — 🎬 ${universeName}${agentList}\n 📁 ${s.projectPath}`;
|
|
121
|
+
return `- **${s.name}** (\`${s.slug}\`) — ${s.status} — 🎬 ${universeName}${leadLine}${agentList}\n 📁 ${s.projectPath}`;
|
|
118
122
|
})
|
|
119
123
|
.join("\n");
|
|
120
124
|
},
|
|
@@ -373,7 +377,11 @@ export function createTools(deps) {
|
|
|
373
377
|
const universeName = squad.universe
|
|
374
378
|
? UNIVERSES.find((u) => u.id === squad.universe)?.name ?? squad.universe
|
|
375
379
|
: "none";
|
|
376
|
-
const lines = agents.map((a) =>
|
|
380
|
+
const lines = agents.map((a) => {
|
|
381
|
+
const leadBadge = a.is_lead === 1 ? " ⭐ [LEAD]" : "";
|
|
382
|
+
const qaBadge = a.is_qa === 1 ? " 🛡️ [QA]" : "";
|
|
383
|
+
return `- **${a.character_name}**${leadBadge}${qaBadge} — ${a.role_title} (${a.model_tier}) — ${a.status}${a.personality ? `\n _${a.personality}_` : ""}`;
|
|
384
|
+
});
|
|
377
385
|
return `**${squad.name}** — 🎬 ${universeName}\n\n${lines.join("\n")}`;
|
|
378
386
|
},
|
|
379
387
|
});
|
|
@@ -973,7 +981,84 @@ export function createTools(deps) {
|
|
|
973
981
|
}
|
|
974
982
|
},
|
|
975
983
|
});
|
|
976
|
-
|
|
984
|
+
const squadSetQA = defineTool("squad_set_qa", {
|
|
985
|
+
description: "Mark a squad agent as a QA reviewer with veto power. QA agents must approve before a PR is promoted from draft to ready.",
|
|
986
|
+
skipPermission: true,
|
|
987
|
+
parameters: z.object({
|
|
988
|
+
slug: z.string().describe("Squad slug"),
|
|
989
|
+
character_name: z.string().describe("Character name of the agent"),
|
|
990
|
+
is_qa: z
|
|
991
|
+
.boolean()
|
|
992
|
+
.describe("Whether this agent is a QA reviewer (true) or not (false)"),
|
|
993
|
+
}),
|
|
994
|
+
handler: async ({ slug, character_name, is_qa }) => {
|
|
995
|
+
try {
|
|
996
|
+
const squad = deps.getSquad(slug);
|
|
997
|
+
if (!squad)
|
|
998
|
+
return `Squad not found: ${slug}`;
|
|
999
|
+
const agents = deps.listSquadAgents(slug);
|
|
1000
|
+
const target = agents.find((a) => a.character_name === character_name);
|
|
1001
|
+
if (!target) {
|
|
1002
|
+
return `Agent "${character_name}" not found in squad "${slug}".`;
|
|
1003
|
+
}
|
|
1004
|
+
deps.setSquadQA(slug, character_name, is_qa);
|
|
1005
|
+
return is_qa
|
|
1006
|
+
? `🛡️ ${character_name} (${target.role_title}) is now a QA reviewer for squad "${squad.name}". They have veto power over PR promotion.`
|
|
1007
|
+
: `${character_name} is no longer a QA reviewer for squad "${squad.name}".`;
|
|
1008
|
+
}
|
|
1009
|
+
catch (err) {
|
|
1010
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
const squadTaskReviews = defineTool("squad_task_reviews", {
|
|
1015
|
+
description: "Get the peer reviews left on a completed task by the squad. Shows who approved or rejected and any comments.",
|
|
1016
|
+
skipPermission: true,
|
|
1017
|
+
parameters: z.object({
|
|
1018
|
+
task_id: z.string().describe("The task ID to fetch reviews for"),
|
|
1019
|
+
}),
|
|
1020
|
+
handler: async ({ task_id }) => {
|
|
1021
|
+
const reviews = deps.getTaskReviews(task_id);
|
|
1022
|
+
if (reviews.length === 0) {
|
|
1023
|
+
return `No reviews found for task ${task_id}.`;
|
|
1024
|
+
}
|
|
1025
|
+
return reviews
|
|
1026
|
+
.map((r) => {
|
|
1027
|
+
const verdict = r.approved === 1 ? "✅ APPROVED" : "❌ REJECTED";
|
|
1028
|
+
const comments = r.comments ? `\n ${r.comments.replace(/\n/g, "\n ")}` : "";
|
|
1029
|
+
return `- **${r.reviewer_character}** — ${verdict}${comments}`;
|
|
1030
|
+
})
|
|
1031
|
+
.join("\n");
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
const squadSetLead = defineTool("squad_set_lead", {
|
|
1035
|
+
description: "Designate an agent as the team lead for their squad. The lead receives delegated tasks (when no specific agent is targeted) and orchestrates the team by divvying subtasks to teammates.",
|
|
1036
|
+
skipPermission: true,
|
|
1037
|
+
parameters: z.object({
|
|
1038
|
+
slug: z.string().describe("Squad slug"),
|
|
1039
|
+
character_name: z
|
|
1040
|
+
.string()
|
|
1041
|
+
.describe("Character name of the agent to make team lead"),
|
|
1042
|
+
}),
|
|
1043
|
+
handler: async ({ slug, character_name }) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const squad = deps.getSquad(slug);
|
|
1046
|
+
if (!squad)
|
|
1047
|
+
return `Squad not found: ${slug}`;
|
|
1048
|
+
const agents = deps.listSquadAgents(slug);
|
|
1049
|
+
const target = agents.find((a) => a.character_name === character_name);
|
|
1050
|
+
if (!target) {
|
|
1051
|
+
return `Agent "${character_name}" not found in squad "${slug}". Use squad_agents to list the roster.`;
|
|
1052
|
+
}
|
|
1053
|
+
deps.setSquadLead(slug, character_name);
|
|
1054
|
+
return `⭐ ${character_name} (${target.role_title}) is now the team lead for squad "${squad.name}".`;
|
|
1055
|
+
}
|
|
1056
|
+
catch (err) {
|
|
1057
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
return [wikiRead, wikiWrite, wikiSearch, wikiDelete, wikiList, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, squadDelete, squadAnalyze, squadAddAgent, squadAgents, squadRemoveAgent, squadSetLead, squadSetQA, squadTaskReviews, skillList, skillInstall, skillRemove, skillSearch, configUpdate, checkUpdate, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github];
|
|
977
1062
|
}
|
|
978
1063
|
function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
979
1064
|
if (depth >= maxDepth)
|
package/dist/store/db.js
CHANGED
|
@@ -70,6 +70,17 @@ export function getDb() {
|
|
|
70
70
|
status TEXT NOT NULL DEFAULT 'idle',
|
|
71
71
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
72
72
|
UNIQUE(squad_slug, character_name)
|
|
73
|
+
)`,
|
|
74
|
+
`ALTER TABLE squad_agents ADD COLUMN is_lead INTEGER NOT NULL DEFAULT 0`,
|
|
75
|
+
`ALTER TABLE squad_agents ADD COLUMN is_qa INTEGER NOT NULL DEFAULT 0`,
|
|
76
|
+
`CREATE TABLE IF NOT EXISTS squad_task_reviews (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
task_id TEXT NOT NULL,
|
|
79
|
+
squad_slug TEXT NOT NULL,
|
|
80
|
+
reviewer_character TEXT NOT NULL,
|
|
81
|
+
approved INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
comments TEXT,
|
|
83
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
73
84
|
)`,
|
|
74
85
|
];
|
|
75
86
|
for (const migration of migrations) {
|
package/dist/store/squads.js
CHANGED
|
@@ -115,4 +115,22 @@ export function getDecisionsSummary(squadSlug) {
|
|
|
115
115
|
})
|
|
116
116
|
.join("\n");
|
|
117
117
|
}
|
|
118
|
+
export function setSquadLead(squadSlug, characterName) {
|
|
119
|
+
const db = getDb();
|
|
120
|
+
const tx = db.transaction(() => {
|
|
121
|
+
db.prepare("UPDATE squad_agents SET is_lead = 0 WHERE squad_slug = ?").run(squadSlug);
|
|
122
|
+
db.prepare("UPDATE squad_agents SET is_lead = 1 WHERE squad_slug = ? AND character_name = ?").run(squadSlug, characterName);
|
|
123
|
+
});
|
|
124
|
+
tx();
|
|
125
|
+
}
|
|
126
|
+
export function getSquadLead(squadSlug) {
|
|
127
|
+
return getDb()
|
|
128
|
+
.prepare("SELECT * FROM squad_agents WHERE squad_slug = ? AND is_lead = 1 LIMIT 1")
|
|
129
|
+
.get(squadSlug);
|
|
130
|
+
}
|
|
131
|
+
export function setSquadQA(squadSlug, characterName, isQA) {
|
|
132
|
+
getDb()
|
|
133
|
+
.prepare("UPDATE squad_agents SET is_qa = ? WHERE squad_slug = ? AND character_name = ?")
|
|
134
|
+
.run(isQA ? 1 : 0, squadSlug, characterName);
|
|
135
|
+
}
|
|
118
136
|
//# sourceMappingURL=squads.js.map
|
package/dist/store/tasks.js
CHANGED
|
@@ -39,4 +39,18 @@ export function listRecentTasks(limit = 50) {
|
|
|
39
39
|
.prepare("SELECT * FROM agent_tasks ORDER BY datetime(started_at) DESC, task_id DESC LIMIT ?")
|
|
40
40
|
.all(limit);
|
|
41
41
|
}
|
|
42
|
+
export function createReview(taskId, squadSlug, reviewerCharacter, approved, comments) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const info = db
|
|
45
|
+
.prepare("INSERT INTO squad_task_reviews (task_id, squad_slug, reviewer_character, approved, comments) VALUES (?, ?, ?, ?, ?)")
|
|
46
|
+
.run(taskId, squadSlug, reviewerCharacter, approved ? 1 : 0, comments ?? null);
|
|
47
|
+
return db
|
|
48
|
+
.prepare("SELECT * FROM squad_task_reviews WHERE id = ?")
|
|
49
|
+
.get(info.lastInsertRowid);
|
|
50
|
+
}
|
|
51
|
+
export function getTaskReviews(taskId) {
|
|
52
|
+
return getDb()
|
|
53
|
+
.prepare("SELECT * FROM squad_task_reviews WHERE task_id = ? ORDER BY created_at ASC, id ASC")
|
|
54
|
+
.all(taskId);
|
|
55
|
+
}
|
|
42
56
|
//# sourceMappingURL=tasks.js.map
|