heyio 0.2.0 → 0.3.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,286 @@
1
+ // Tiered activity log normalizer.
2
+ //
3
+ // Converts raw TaskStreamEvent records (forwarded from the Copilot SDK
4
+ // session) into "activity entries" that present a clean human-readable
5
+ // summary. Each entry preserves the underlying raw payload for drill-down so
6
+ // no information is lost.
7
+ //
8
+ // Used by:
9
+ // - GET /tasks/:taskId/activity (full collapsed list)
10
+ // - the SSE per-event payload (single-event summary attached alongside raw)
11
+ // - the TUI /activity command
12
+ const TOOL_VERBS = {
13
+ bash: "Ran shell command",
14
+ shell: "Ran shell command",
15
+ read_file: "Read file",
16
+ view: "Read file",
17
+ create: "Created file",
18
+ edit: "Edited file",
19
+ str_replace_editor: "Edited file",
20
+ grep: "Searched code",
21
+ glob: "Searched filenames",
22
+ web_fetch: "Fetched URL",
23
+ github: "Called GitHub API",
24
+ delegate_to_teammate: "Delegated to teammate",
25
+ squad_delegate: "Delegated to squad",
26
+ squad_status: "Listed squads",
27
+ squad_agents: "Listed squad agents",
28
+ squad_set_lead: "Set squad lead",
29
+ squad_set_qa: "Set squad QA",
30
+ squad_task_status: "Checked task status",
31
+ squad_task_reviews: "Read task reviews",
32
+ squad_log_decision: "Logged squad decision",
33
+ squad_schedule_create: "Created squad schedule",
34
+ squad_schedule_list: "Listed squad schedules",
35
+ squad_schedule_run_now: "Fired squad schedule",
36
+ wiki_read: "Read wiki page",
37
+ wiki_write: "Wrote wiki page",
38
+ wiki_search: "Searched wiki",
39
+ wiki_list: "Listed wiki pages",
40
+ skill_list: "Listed skills",
41
+ skill_install: "Installed skill",
42
+ skill_remove: "Removed skill",
43
+ skill_search: "Searched skills registry",
44
+ config_update: "Updated config",
45
+ check_update: "Checked for IO update",
46
+ file_ops: "File operation",
47
+ };
48
+ function trunc(s, n) {
49
+ if (!s)
50
+ return "";
51
+ const flat = s.replace(/\s+/g, " ").trim();
52
+ return flat.length > n ? flat.slice(0, n - 1) + "…" : flat;
53
+ }
54
+ function pickArgSummary(toolName, args) {
55
+ if (!args)
56
+ return "";
57
+ const a = args;
58
+ switch (toolName) {
59
+ case "bash":
60
+ case "shell": {
61
+ const cmd = typeof a.command === "string" ? a.command : "";
62
+ return trunc(cmd, 80);
63
+ }
64
+ case "read_file":
65
+ case "view":
66
+ case "create":
67
+ case "edit":
68
+ case "str_replace_editor": {
69
+ const p = typeof a.path === "string" ? a.path : (typeof a.file_path === "string" ? a.file_path : "");
70
+ return p;
71
+ }
72
+ case "grep": {
73
+ const pat = typeof a.pattern === "string" ? `"${trunc(a.pattern, 40)}"` : "";
74
+ const paths = Array.isArray(a.paths) ? a.paths.join(", ") : (typeof a.paths === "string" ? a.paths : "");
75
+ return [pat, paths].filter(Boolean).join(" in ");
76
+ }
77
+ case "glob": {
78
+ return typeof a.pattern === "string" ? a.pattern : "";
79
+ }
80
+ case "web_fetch": {
81
+ return typeof a.url === "string" ? a.url : "";
82
+ }
83
+ case "delegate_to_teammate": {
84
+ const teammate = typeof a.teammate === "string" ? a.teammate : "";
85
+ const task = typeof a.task === "string" ? trunc(a.task, 60) : "";
86
+ return [teammate, task].filter(Boolean).join(": ");
87
+ }
88
+ case "squad_delegate": {
89
+ const slug = typeof a.slug === "string" ? a.slug : "";
90
+ const agent = typeof a.agent === "string" ? ` → ${a.agent}` : "";
91
+ const task = typeof a.task === "string" ? trunc(a.task, 50) : "";
92
+ return `${slug}${agent}${task ? `: ${task}` : ""}`;
93
+ }
94
+ case "wiki_read":
95
+ case "wiki_write":
96
+ case "wiki_delete": {
97
+ return typeof a.path === "string" ? a.path : "";
98
+ }
99
+ case "wiki_search":
100
+ case "skill_search": {
101
+ return typeof a.query === "string" ? `"${trunc(a.query, 40)}"` : "";
102
+ }
103
+ case "github": {
104
+ const ep = typeof a.endpoint === "string" ? a.endpoint : "";
105
+ const method = typeof a.method === "string" ? a.method : "";
106
+ return [method, ep].filter(Boolean).join(" ");
107
+ }
108
+ case "file_ops": {
109
+ const op = typeof a.operation === "string" ? a.operation : "";
110
+ const p = typeof a.path === "string" ? a.path : "";
111
+ return [op, p].filter(Boolean).join(" ");
112
+ }
113
+ default: {
114
+ // Best-effort: stringify the first arg value.
115
+ const k = Object.keys(a)[0];
116
+ if (!k)
117
+ return "";
118
+ const v = a[k];
119
+ const s = typeof v === "string" ? v : JSON.stringify(v);
120
+ return `${k}=${trunc(s ?? "", 50)}`;
121
+ }
122
+ }
123
+ }
124
+ /** Summarize a single TaskStreamEvent. */
125
+ export function summarizeEvent(ev) {
126
+ const data = (ev.data ?? {});
127
+ const base = { ts: ev.ts, rawType: ev.type, raw: ev.data };
128
+ switch (ev.type) {
129
+ case "assistant.intent": {
130
+ const intent = typeof data.intent === "string" ? data.intent : "";
131
+ return { ...base, kind: "reasoning", icon: "🧠", summary: intent || "(intent)" };
132
+ }
133
+ case "assistant.reasoning": {
134
+ const content = typeof data.content === "string" ? data.content : "";
135
+ return {
136
+ ...base,
137
+ kind: "reasoning",
138
+ icon: "🧠",
139
+ summary: trunc(content, 140) || "(reasoning)",
140
+ detail: content,
141
+ };
142
+ }
143
+ case "assistant.message": {
144
+ const content = typeof data.content === "string" ? data.content : "";
145
+ return {
146
+ ...base,
147
+ kind: "message",
148
+ icon: "💬",
149
+ summary: trunc(content, 200) || "(empty message)",
150
+ detail: content,
151
+ };
152
+ }
153
+ case "assistant.turn_start":
154
+ return { ...base, kind: "system", icon: "▶️", summary: "Turn started" };
155
+ case "assistant.turn_end":
156
+ return { ...base, kind: "system", icon: "⏹️", summary: "Turn ended" };
157
+ case "tool.execution_start": {
158
+ const name = typeof data.toolName === "string" ? data.toolName : "tool";
159
+ const verb = TOOL_VERBS[name] ?? `Used ${name}`;
160
+ const args = pickArgSummary(name, data.arguments);
161
+ return {
162
+ ...base,
163
+ kind: "tool",
164
+ icon: "🔧",
165
+ summary: args ? `${verb} — ${args}` : verb,
166
+ toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : undefined,
167
+ status: "pending",
168
+ };
169
+ }
170
+ case "tool.execution_complete": {
171
+ const success = data.success === true;
172
+ const result = (data.result ?? {});
173
+ const content = typeof result.content === "string" ? result.content : "";
174
+ return {
175
+ ...base,
176
+ kind: "tool",
177
+ icon: success ? "✅" : "❌",
178
+ summary: success ? "Tool completed" : "Tool failed",
179
+ detail: trunc(content, 300),
180
+ toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : undefined,
181
+ status: success ? "success" : "error",
182
+ };
183
+ }
184
+ case "tool.execution_progress": {
185
+ const message = typeof data.message === "string" ? data.message : "";
186
+ return {
187
+ ...base,
188
+ kind: "tool",
189
+ icon: "⏳",
190
+ summary: trunc(message, 140) || "Tool progress",
191
+ toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : undefined,
192
+ status: "pending",
193
+ };
194
+ }
195
+ case "session.error": {
196
+ const msg = typeof data.message === "string" ? data.message : (typeof data.error === "string" ? data.error : "");
197
+ return { ...base, kind: "outcome", icon: "❌", summary: `Error: ${trunc(msg, 160) || "session error"}` };
198
+ }
199
+ case "session.warning": {
200
+ const msg = typeof data.message === "string" ? data.message : "";
201
+ return { ...base, kind: "outcome", icon: "⚠️", summary: `Warning: ${trunc(msg, 160) || "session warning"}` };
202
+ }
203
+ case "task.done": {
204
+ const r = typeof data.result === "string" ? data.result : "";
205
+ return { ...base, kind: "outcome", icon: "✅", summary: "Task completed", detail: trunc(r, 300) };
206
+ }
207
+ case "task.failed": {
208
+ const e = typeof data.error === "string" ? data.error : "";
209
+ return { ...base, kind: "outcome", icon: "❌", summary: `Task failed: ${trunc(e, 160)}` };
210
+ }
211
+ case "task.cancelled":
212
+ return { ...base, kind: "outcome", icon: "🛑", summary: "Task cancelled" };
213
+ case "task.review":
214
+ return {
215
+ ...base,
216
+ kind: "outcome",
217
+ icon: data.approved ? "👍" : "👎",
218
+ summary: `Review by ${data.reviewer ?? "?"}: ${data.approved ? "APPROVED" : "REJECTED"}${data.is_qa ? " (QA)" : ""}`,
219
+ detail: typeof data.comments === "string" ? data.comments : undefined,
220
+ };
221
+ case "task.review_complete":
222
+ return {
223
+ ...base,
224
+ kind: "outcome",
225
+ icon: data.promoted ? "🚀" : "ℹ️",
226
+ summary: data.promoted
227
+ ? `Promoted PR: ${data.prUrl ?? ""}`
228
+ : `Review complete: ${typeof data.reason === "string" ? data.reason : ""}`,
229
+ };
230
+ case "task.review_advisory":
231
+ return {
232
+ ...base,
233
+ kind: "outcome",
234
+ icon: "ℹ️",
235
+ summary: typeof data.reason === "string" ? data.reason : "Advisory review note",
236
+ };
237
+ case "task.review_error": {
238
+ const e = typeof data.error === "string" ? data.error : "";
239
+ return { ...base, kind: "outcome", icon: "❌", summary: `Review error: ${trunc(e, 160)}` };
240
+ }
241
+ default:
242
+ return { ...base, kind: "system", icon: "•", summary: ev.type };
243
+ }
244
+ }
245
+ /**
246
+ * Collapse a stream of events into the activity log. Tool start + complete
247
+ * pairs are merged into one entry (success/error and detail attached).
248
+ * Streaming deltas (assistant.message_delta, assistant.reasoning_delta,
249
+ * tool.execution_partial_result) are dropped — the "complete" sibling event
250
+ * is what we summarize.
251
+ */
252
+ export function summarize(events) {
253
+ const out = [];
254
+ const toolIndex = new Map(); // toolCallId → index into out
255
+ for (const ev of events) {
256
+ if (ev.type === "assistant.message_delta" ||
257
+ ev.type === "assistant.reasoning_delta" ||
258
+ ev.type === "tool.execution_partial_result") {
259
+ continue;
260
+ }
261
+ const entry = summarizeEvent(ev);
262
+ if (entry.kind === "tool" && entry.toolCallId) {
263
+ const existing = toolIndex.get(entry.toolCallId);
264
+ if (existing != null && entry.status && entry.status !== "pending") {
265
+ // Merge: keep the verb from the start event, add status/detail/icon from completion.
266
+ const prior = out[existing];
267
+ out[existing] = {
268
+ ...prior,
269
+ icon: entry.icon,
270
+ status: entry.status,
271
+ detail: entry.detail ?? prior.detail,
272
+ // Append a brief outcome marker if the start summary is the only descriptor.
273
+ summary: prior.summary,
274
+ // Keep the completion event raw available for drill-down by stashing on detail when no detail present
275
+ raw: { start: prior.raw, complete: entry.raw },
276
+ rawType: `${prior.rawType}+${entry.rawType}`,
277
+ };
278
+ continue;
279
+ }
280
+ toolIndex.set(entry.toolCallId, out.length);
281
+ }
282
+ out.push(entry);
283
+ }
284
+ return out;
285
+ }
286
+ //# sourceMappingURL=event-summary.js.map
@@ -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,24 @@ 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
+ squad_slug: r.squad_slug,
93
+ })),
78
94
  listSkills,
79
95
  installSkill,
80
96
  removeSkill,
@@ -92,6 +108,30 @@ function toolFingerprint(tools) {
92
108
  const names = (tools ?? []).map((t) => t.name).sort().join(",");
93
109
  return crypto.createHash("sha256").update(`${IO_VERSION}:${names}`).digest("hex").slice(0, 16);
94
110
  }
111
+ function buildSquadRoster() {
112
+ const squads = listSquads();
113
+ if (squads.length === 0)
114
+ return "";
115
+ return squads
116
+ .map((s) => {
117
+ const agents = listSquadAgents(s.slug);
118
+ const lead = agents.find((a) => a.is_lead === 1);
119
+ const agentList = agents
120
+ .map((a) => {
121
+ const badges = [
122
+ a.is_lead === 1 ? "⭐ LEAD" : "",
123
+ a.is_qa === 1 ? "🛡️ QA" : "",
124
+ ]
125
+ .filter(Boolean)
126
+ .join(", ");
127
+ return ` - ${a.character_name} (${a.role_title})${badges ? ` [${badges}]` : ""}`;
128
+ })
129
+ .join("\n");
130
+ const leadLine = lead ? `\nTeam Lead: ${lead.character_name}` : "";
131
+ return `**${s.name}** (\`${s.slug}\`) — ${s.status}\n📁 ${s.project_path}${leadLine}\n${agentList || " _(no agents yet)_"}`;
132
+ })
133
+ .join("\n\n");
134
+ }
95
135
  function buildFullSessionConfig() {
96
136
  const { tools, skillDirectories } = getSessionConfig();
97
137
  return {
@@ -102,6 +142,7 @@ function buildFullSessionConfig() {
102
142
  content: getOrchestratorSystemMessage({
103
143
  selfEditEnabled: config.selfEditEnabled,
104
144
  memorySummary: getWikiSummary() || undefined,
145
+ squadRoster: buildSquadRoster() || undefined,
105
146
  }),
106
147
  },
107
148
  tools,
@@ -0,0 +1,155 @@
1
+ // Squad scheduler — fires recurring "stand-up" meetings on a cron schedule.
2
+ //
3
+ // Design:
4
+ // - Runs as a single setInterval on the daemon (TICK_MS).
5
+ // - Each tick: fetch schedules whose next_run_at is in the past.
6
+ // - For each due schedule: compose a stand-up prompt and delegate it to the
7
+ // squad lead via the existing delegateToAgent() pipeline. The lead then
8
+ // orchestrates the agenda by delegating subtasks to teammates internally.
9
+ // - After firing, advance next_run_at to the next cron occurrence.
10
+ //
11
+ // Schedules survive daemon restarts because next_run_at is persisted. On
12
+ // startup we backfill any next_run_at fields that became stale (or are NULL).
13
+ import { listSchedules, listDueSchedules, recordScheduleRun, updateNextRun } from "../store/schedules.js";
14
+ import { getSquad } from "../store/squads.js";
15
+ import { delegateToAgent } from "./agents.js";
16
+ import { nextRun } from "./cron.js";
17
+ const TICK_MS = 30_000;
18
+ const AGENDA_BLOCKS = {
19
+ triage: `**Triage**
20
+ - Use the GitHub CLI (\`gh issue list\`) to pull open issues with the \`needs-triage\` label.
21
+ - For each issue: read the body and decide on appropriate labels (priority, area, type, etc).
22
+ - Apply labels with \`gh issue edit <num> --add-label "..."\` and remove \`needs-triage\` once labelled.
23
+ - If the issue lacks information, post a clarifying comment with \`gh issue comment\` instead of labelling, and leave \`needs-triage\` on it.`,
24
+ prioritize: `**Prioritize**
25
+ - Identify open issues that are ready to be worked on: properly labelled, not blocked, no \`needs-triage\` or \`needs-review\` label, no open PR already addressing them.
26
+ - Rank by priority labels and surface the top candidate.
27
+ - After the stand-up, the team lead should immediately begin work on the highest-priority ready issue by delegating it to the right teammate.`,
28
+ ideation: `**Ideation**
29
+ - Brainstorm 1–3 concrete improvements or new features for the project.
30
+ - Discuss as a team (use \`delegate_to_teammate\` to gather input from members whose expertise fits the idea).
31
+ - For each idea the team agrees on, create a GitHub issue with \`gh issue create\` tagged with the \`needs-review\` label so the human can approve it before work begins.`,
32
+ };
33
+ function buildStandupPrompt(squad, schedule) {
34
+ const blocks = schedule.agenda
35
+ .map((item) => AGENDA_BLOCKS[item] ?? `**${item}** _(no built-in template — improvise)_`)
36
+ .join("\n\n");
37
+ const notes = schedule.notes ? `\n\n**Operator notes:** ${schedule.notes}` : "";
38
+ return `# Scheduled stand-up: ${schedule.name}
39
+
40
+ You are the team lead for the **${squad.name}** squad (\`${squad.slug}\`). Run a stand-up meeting now.
41
+
42
+ **Project path:** \`${squad.project_path}\` — \`cd\` here before invoking the GitHub CLI so it picks up the right repo.
43
+
44
+ **Agenda** (work through these in order; use \`delegate_to_teammate\` to pull in the right specialist for each item):
45
+
46
+ ${blocks}
47
+
48
+ When you finish the agenda, summarise what was triaged, what was prioritised (and what work you've kicked off), and what new issues were filed during ideation.${notes}`;
49
+ }
50
+ let timer;
51
+ const inFlight = new Set();
52
+ async function fireSchedule(schedule) {
53
+ if (inFlight.has(schedule.id))
54
+ return;
55
+ const squad = getSquad(schedule.squad_slug);
56
+ if (!squad) {
57
+ console.error(`[io] scheduler: squad "${schedule.squad_slug}" missing for schedule ${schedule.id}; disabling next_run_at`);
58
+ updateNextRun(schedule.id, null);
59
+ return;
60
+ }
61
+ inFlight.add(schedule.id);
62
+ const ranAt = new Date();
63
+ let nextIso = null;
64
+ try {
65
+ nextIso = nextRun(schedule.cron_expr, ranAt).toISOString();
66
+ }
67
+ catch (err) {
68
+ console.error(`[io] scheduler: cron parse error for schedule ${schedule.id}:`, err instanceof Error ? err.message : err);
69
+ }
70
+ recordScheduleRun(schedule.id, ranAt, nextIso);
71
+ const prompt = buildStandupPrompt({ name: squad.name, slug: squad.slug, project_path: squad.project_path }, schedule);
72
+ console.log(`[io] scheduler: firing schedule "${schedule.name}" for squad "${squad.slug}" (next run: ${nextIso ?? "never"})`);
73
+ try {
74
+ await delegateToAgent(squad.slug, prompt, () => {
75
+ // No-op: result is recorded on the agent task; the standup is fire-and-forget.
76
+ });
77
+ }
78
+ catch (err) {
79
+ console.error(`[io] scheduler: failed to delegate stand-up for schedule ${schedule.id}:`, err instanceof Error ? err.message : err);
80
+ }
81
+ finally {
82
+ inFlight.delete(schedule.id);
83
+ }
84
+ }
85
+ async function tick() {
86
+ let due;
87
+ try {
88
+ due = listDueSchedules(new Date());
89
+ }
90
+ catch (err) {
91
+ console.error("[io] scheduler tick failed:", err instanceof Error ? err.message : err);
92
+ return;
93
+ }
94
+ for (const s of due) {
95
+ // Sequential — avoid stampeding multiple stand-ups simultaneously.
96
+ await fireSchedule(s);
97
+ }
98
+ }
99
+ /**
100
+ * Backfill next_run_at for any schedules where it's NULL or already in the past
101
+ * but should not have fired (e.g. daemon was offline). We deliberately advance
102
+ * to the next future occurrence rather than replaying missed runs.
103
+ */
104
+ export function reconcileSchedules(now = new Date()) {
105
+ for (const s of listSchedules()) {
106
+ if (!s.enabled)
107
+ continue;
108
+ let needsUpdate = false;
109
+ if (!s.next_run_at) {
110
+ needsUpdate = true;
111
+ }
112
+ else {
113
+ const next = new Date(s.next_run_at);
114
+ if (Number.isNaN(next.getTime()) || next <= now)
115
+ needsUpdate = true;
116
+ }
117
+ if (!needsUpdate)
118
+ continue;
119
+ try {
120
+ const next = nextRun(s.cron_expr, now);
121
+ updateNextRun(s.id, next.toISOString());
122
+ }
123
+ catch (err) {
124
+ console.error(`[io] scheduler: invalid cron "${s.cron_expr}" on schedule ${s.id}; disabling next_run_at:`, err instanceof Error ? err.message : err);
125
+ updateNextRun(s.id, null);
126
+ }
127
+ }
128
+ }
129
+ export function startScheduler() {
130
+ if (timer)
131
+ return;
132
+ reconcileSchedules();
133
+ timer = setInterval(() => {
134
+ void tick();
135
+ }, TICK_MS);
136
+ if (typeof timer.unref === "function")
137
+ timer.unref();
138
+ console.log(`[io] Scheduler started (tick every ${TICK_MS / 1000}s)`);
139
+ }
140
+ export function stopScheduler() {
141
+ if (!timer)
142
+ return;
143
+ clearInterval(timer);
144
+ timer = undefined;
145
+ }
146
+ /** Manually fire a schedule. Used by squad_schedule_run_now. */
147
+ export async function runScheduleNow(scheduleId) {
148
+ const all = listSchedules();
149
+ const s = all.find((x) => x.id === scheduleId);
150
+ if (!s)
151
+ return { ok: false, error: `Schedule ${scheduleId} not found` };
152
+ await fireSchedule(s);
153
+ return { ok: true };
154
+ }
155
+ //# sourceMappingURL=scheduler.js.map
@@ -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### Active Squads\n${opts.squadRoster}\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
- - **Direct answer**: For simple questions, general knowledge, status checks — answer directly.
53
- - **Use tools**: For tasks requiring shell access, file operations, web lookupsuse your tools.
54
- - **Create/delegate to squad**: For coding projects that need persistent context — create a squad with specialized agents.
55
- - **Use a skill**: If you have a skill for the task, use it.
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,41 @@ 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
- After planning tasks with the user, **use \`squad_delegate\` to send each task to the right agent**:
74
- 1. Plan the work with the user (break into concrete tasks).
75
- 2. Call \`squad_delegate\` with the squad slug and task. Optionally specify an \`agent\` (character name) to target a specific specialist. If omitted, the system picks the best available agent.
76
- 3. The agent works autonomously in the background with their specialized system prompt.
77
- 4. Use \`squad_task_status\` to check progress and retrieve results.
78
- 5. Report results back to the user, mentioning the character name.
79
-
80
- You can delegate multiple tasks to different agents in parallel.
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
+ - **Required**: every squad must have at least one agent designated as QA via \`squad_set_qa\`, AND at least one agent whose role title implies a testing/quality focus (e.g. role contains "test", "qa", or "quality"). Both can be the same agent.
97
+ - \`squad_status\`, \`squad_agents\`, and \`squad_delegate\` will surface a ⚠️ warning when either is missing. Delegation is not blocked, but you should fix the gap before promoting work.
98
+ - **QA agents and the team lead have veto power**: if any QA reviewer or the team lead rejects, the PR stays as a draft. The lead's veto is automatic — no need to also designate them as QA.
99
+ - Non-QA rejections are advisory — they're recorded but don't block promotion.
100
+ - 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\`.
101
+ - Use \`squad_task_reviews\` to inspect the reviews on any completed task.
102
+
103
+ ### Squad Build Checklist
104
+ After \`squad_create\`, before delegating real work:
105
+ 1. Add agents with \`squad_add_agent\` (use roles tailored to the project's stack).
106
+ 2. Include at least one **test/quality engineer** role (e.g. "Integration Test Engineer", "QA Specialist", "Quality Reviewer").
107
+ 3. Designate a team lead with \`squad_set_lead\`.
108
+ 4. Designate at least one QA reviewer with \`squad_set_qa\` (often the same agent as the test engineer).
109
+
110
+ ### Scheduled Stand-ups
111
+ Squads can be put on a recurring cron-style schedule. At the scheduled time IO wakes the team lead, who runs the agenda by delegating to teammates. This runs in the background even when no human is in the TUI/Telegram.
112
+
113
+ - \`squad_schedule_create\` — create a recurring stand-up. Cron uses standard 5-field syntax: "minute hour day-of-month month day-of-week". Examples: \`0 5 * * *\` (daily 5AM), \`0 9 * * 1-5\` (9AM weekdays), \`30 14 * * 1\` (Mondays 14:30).
114
+ - Built-in agenda items: \`triage\` (process \`needs-triage\` issues), \`prioritize\` (pick highest-priority ready work and start on it), \`ideation\` (brainstorm and open \`needs-review\` issues for the human). Custom items are passed verbatim to the lead.
115
+ - \`squad_schedule_list\`, \`squad_schedule_pause\`, \`squad_schedule_resume\`, \`squad_schedule_delete\`, \`squad_schedule_run_now\` round out the lifecycle.
116
+
117
+ When a user asks something like "have the IO squad meet every weekday at 5AM to triage and prioritize", call \`squad_schedule_create\` with \`cron: "0 5 * * 1-5"\` and \`agenda: ["triage", "prioritize"]\`.
81
118
 
82
119
  ### Agent Roles Are Dynamic
83
120
  **Do NOT use generic roles** like "developer" or "tester". Analyze the project first and create roles that match its actual technology stack. Examples: