heyio 3.0.2 → 3.0.3
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/api/server.js +1 -1
- package/dist/api/server.js.map +1 -1
- package/dist/logging/logger.d.ts.map +1 -1
- package/dist/logging/logger.js +13 -1
- package/dist/logging/logger.js.map +1 -1
- package/node_modules/@io/shared/package.json +1 -1
- package/package.json +7 -2
- package/public/assets/index-2RY89H3W.js +336 -0
- package/public/assets/index-2RY89H3W.js.map +1 -0
- package/public/assets/index-D3cGfBsj.css +1 -0
- package/public/index.html +14 -0
- package/src/api/middleware/auth.ts +0 -76
- package/src/api/notifications.ts +0 -122
- package/src/api/routes/activity.ts +0 -29
- package/src/api/routes/attachments.ts +0 -93
- package/src/api/routes/config.ts +0 -115
- package/src/api/routes/conversations.ts +0 -87
- package/src/api/routes/health.ts +0 -18
- package/src/api/routes/inbox.ts +0 -98
- package/src/api/routes/schedules.ts +0 -121
- package/src/api/routes/skills.ts +0 -105
- package/src/api/routes/squads.ts +0 -145
- package/src/api/routes/usage.ts +0 -57
- package/src/api/routes/wiki.ts +0 -49
- package/src/api/server.ts +0 -186
- package/src/config.ts +0 -3
- package/src/copilot/client.ts +0 -42
- package/src/copilot/health-monitor.ts +0 -85
- package/src/copilot/orchestrator.ts +0 -222
- package/src/copilot/tools.ts +0 -707
- package/src/index.ts +0 -113
- package/src/logging/logger.ts +0 -26
- package/src/models/index.ts +0 -11
- package/src/models/pricing.ts +0 -121
- package/src/models/registry.ts +0 -131
- package/src/models/token-tracker.ts +0 -151
- package/src/scheduler/engine.ts +0 -146
- package/src/skills/index.ts +0 -13
- package/src/skills/store.ts +0 -188
- package/src/squad/agent.ts +0 -326
- package/src/squad/autonomy.ts +0 -78
- package/src/squad/event-bus.ts +0 -71
- package/src/squad/execution/index.ts +0 -17
- package/src/squad/execution/instance.ts +0 -186
- package/src/squad/execution/meeting.ts +0 -191
- package/src/squad/execution/pr.ts +0 -127
- package/src/squad/execution/runner.ts +0 -97
- package/src/squad/execution/tasks.ts +0 -111
- package/src/squad/execution/worktree.ts +0 -138
- package/src/squad/hiring.ts +0 -222
- package/src/squad/index.ts +0 -17
- package/src/squad/manager.ts +0 -337
- package/src/squad/name-generator.ts +0 -135
- package/src/squad/roles/templates.ts +0 -104
- package/src/squad/skill-parser.ts +0 -120
- package/src/squad/source-resolver.ts +0 -57
- package/src/store/activity.ts +0 -176
- package/src/store/db.ts +0 -237
- package/src/store/inbox.ts +0 -199
- package/src/store/schedules.ts +0 -199
- package/src/wiki/index.ts +0 -12
- package/src/wiki/store.ts +0 -139
- package/tsconfig.json +0 -9
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import type { InstanceStatus, Squad } from '@io/shared';
|
|
2
|
-
import { createChildLogger } from '../../logging/logger.js';
|
|
3
|
-
import { getDatabase } from '../../store/db.js';
|
|
4
|
-
import { getEventBus } from '../event-bus.js';
|
|
5
|
-
import { type SquadRuntime, getSquadMembers, getSquadRuntime } from '../manager.js';
|
|
6
|
-
import { type WorktreeInfo, createWorktree, removeWorktree } from './worktree.js';
|
|
7
|
-
|
|
8
|
-
const logger = () => createChildLogger('instance');
|
|
9
|
-
|
|
10
|
-
export interface InstanceTask {
|
|
11
|
-
id: string;
|
|
12
|
-
description: string;
|
|
13
|
-
assignedTo: string; // agent role
|
|
14
|
-
status: 'pending' | 'in_progress' | 'done' | 'failed';
|
|
15
|
-
result?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface Instance {
|
|
19
|
-
id: string;
|
|
20
|
-
squadId: string;
|
|
21
|
-
issueRef?: string;
|
|
22
|
-
worktree: WorktreeInfo | null;
|
|
23
|
-
branch: string | null;
|
|
24
|
-
status: InstanceStatus;
|
|
25
|
-
tasks: InstanceTask[];
|
|
26
|
-
meetingLog: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const activeInstances = new Map<string, Instance>();
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create a new instance for a squad. Enforces max 3 per squad.
|
|
33
|
-
*/
|
|
34
|
-
export async function createInstance(params: {
|
|
35
|
-
squad: Squad;
|
|
36
|
-
issueRef?: string;
|
|
37
|
-
objective: string;
|
|
38
|
-
}): Promise<Instance> {
|
|
39
|
-
const log = logger();
|
|
40
|
-
const db = getDatabase();
|
|
41
|
-
|
|
42
|
-
// Enforce instance limit
|
|
43
|
-
const existingResult = await db.execute({
|
|
44
|
-
sql: "SELECT COUNT(*) as cnt FROM squad_instances WHERE squad_id = ? AND status NOT IN ('complete', 'failed')",
|
|
45
|
-
args: [params.squad.id],
|
|
46
|
-
});
|
|
47
|
-
const count = (existingResult.rows[0]?.cnt as number) ?? 0;
|
|
48
|
-
if (count >= 3) {
|
|
49
|
-
throw new Error(`Squad '${params.squad.name}' already has ${count} active instances (max 3)`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const id = crypto.randomUUID();
|
|
53
|
-
|
|
54
|
-
// Create worktree if project has git
|
|
55
|
-
let worktree: WorktreeInfo | null = null;
|
|
56
|
-
try {
|
|
57
|
-
worktree = createWorktree({
|
|
58
|
-
repoPath: params.squad.projectPath,
|
|
59
|
-
squadName: params.squad.name,
|
|
60
|
-
instanceId: id,
|
|
61
|
-
});
|
|
62
|
-
} catch (err) {
|
|
63
|
-
log.warn({ err }, 'Could not create worktree, proceeding without isolation');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Persist to DB
|
|
67
|
-
await db.execute({
|
|
68
|
-
sql: `INSERT INTO squad_instances (id, squad_id, issue_ref, worktree_path, branch_name, status)
|
|
69
|
-
VALUES (?, ?, ?, ?, ?, 'planning')`,
|
|
70
|
-
args: [
|
|
71
|
-
id,
|
|
72
|
-
params.squad.id,
|
|
73
|
-
params.issueRef ?? null,
|
|
74
|
-
worktree?.path ?? null,
|
|
75
|
-
worktree?.branch ?? null,
|
|
76
|
-
],
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const instance: Instance = {
|
|
80
|
-
id,
|
|
81
|
-
squadId: params.squad.id,
|
|
82
|
-
issueRef: params.issueRef,
|
|
83
|
-
worktree,
|
|
84
|
-
branch: worktree?.branch ?? null,
|
|
85
|
-
status: 'planning',
|
|
86
|
-
tasks: [],
|
|
87
|
-
meetingLog: [],
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
activeInstances.set(id, instance);
|
|
91
|
-
|
|
92
|
-
await getEventBus().emit({
|
|
93
|
-
id: crypto.randomUUID(),
|
|
94
|
-
timestamp: new Date(),
|
|
95
|
-
type: 'instance:created',
|
|
96
|
-
squadId: params.squad.id,
|
|
97
|
-
instanceId: id,
|
|
98
|
-
data: { issueRef: params.issueRef, objective: params.objective },
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
log.info({ instanceId: id, squadId: params.squad.id }, 'Instance created');
|
|
102
|
-
return instance;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Transition instance to a new status.
|
|
107
|
-
*/
|
|
108
|
-
export async function transitionInstance(
|
|
109
|
-
instanceId: string,
|
|
110
|
-
newStatus: InstanceStatus,
|
|
111
|
-
): Promise<void> {
|
|
112
|
-
const db = getDatabase();
|
|
113
|
-
const instance = activeInstances.get(instanceId);
|
|
114
|
-
if (!instance) throw new Error(`Instance ${instanceId} not found`);
|
|
115
|
-
|
|
116
|
-
const validTransitions: Record<InstanceStatus, InstanceStatus[]> = {
|
|
117
|
-
planning: ['meeting', 'failed'],
|
|
118
|
-
meeting: ['working', 'failed'],
|
|
119
|
-
working: ['reviewing', 'failed'],
|
|
120
|
-
reviewing: ['complete', 'working', 'failed'],
|
|
121
|
-
complete: [],
|
|
122
|
-
failed: [],
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const allowed = validTransitions[instance.status];
|
|
126
|
-
if (!allowed.includes(newStatus)) {
|
|
127
|
-
throw new Error(`Invalid transition: ${instance.status} → ${newStatus}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
instance.status = newStatus;
|
|
131
|
-
const completedAt =
|
|
132
|
-
newStatus === 'complete' || newStatus === 'failed' ? new Date().toISOString() : null;
|
|
133
|
-
|
|
134
|
-
await db.execute({
|
|
135
|
-
sql: 'UPDATE squad_instances SET status = ?, completed_at = COALESCE(?, completed_at) WHERE id = ?',
|
|
136
|
-
args: [newStatus, completedAt, instanceId],
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const eventType =
|
|
140
|
-
newStatus === 'meeting'
|
|
141
|
-
? 'instance:meeting_started'
|
|
142
|
-
: newStatus === 'working'
|
|
143
|
-
? 'instance:work_started'
|
|
144
|
-
: newStatus === 'complete'
|
|
145
|
-
? 'instance:complete'
|
|
146
|
-
: newStatus === 'failed'
|
|
147
|
-
? 'instance:failed'
|
|
148
|
-
: ('instance:created' as const);
|
|
149
|
-
|
|
150
|
-
await getEventBus().emit({
|
|
151
|
-
id: crypto.randomUUID(),
|
|
152
|
-
timestamp: new Date(),
|
|
153
|
-
type: eventType,
|
|
154
|
-
squadId: instance.squadId,
|
|
155
|
-
instanceId,
|
|
156
|
-
data: { from: instance.status, to: newStatus },
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Clean up a completed/failed instance (remove worktree).
|
|
162
|
-
*/
|
|
163
|
-
export async function cleanupInstance(instanceId: string, squad: Squad): Promise<void> {
|
|
164
|
-
const instance = activeInstances.get(instanceId);
|
|
165
|
-
if (!instance) return;
|
|
166
|
-
|
|
167
|
-
if (instance.worktree && instance.branch) {
|
|
168
|
-
removeWorktree({
|
|
169
|
-
repoPath: squad.projectPath,
|
|
170
|
-
worktreePath: instance.worktree.path,
|
|
171
|
-
branch: instance.branch,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
activeInstances.delete(instanceId);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Get an active instance */
|
|
179
|
-
export function getInstance(instanceId: string): Instance | undefined {
|
|
180
|
-
return activeInstances.get(instanceId);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Get all active instances for a squad */
|
|
184
|
-
export function getSquadInstances(squadId: string): Instance[] {
|
|
185
|
-
return [...activeInstances.values()].filter((i) => i.squadId === squadId);
|
|
186
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { createChildLogger } from '../../logging/logger.js';
|
|
2
|
-
import type { Agent } from '../agent.js';
|
|
3
|
-
import { getEventBus } from '../event-bus.js';
|
|
4
|
-
import type { SquadRuntime } from '../manager.js';
|
|
5
|
-
import type { Instance, InstanceTask } from './instance.js';
|
|
6
|
-
import { transitionInstance } from './instance.js';
|
|
7
|
-
|
|
8
|
-
const logger = () => createChildLogger('meeting');
|
|
9
|
-
|
|
10
|
-
export interface MeetingResult {
|
|
11
|
-
consensus: boolean;
|
|
12
|
-
tasks: InstanceTask[];
|
|
13
|
-
log: string[];
|
|
14
|
-
vetoReason?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const MAX_ROUNDS = 5;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Run a round-table meeting for an instance.
|
|
21
|
-
* Protocol:
|
|
22
|
-
* 1. Team Lead presents context
|
|
23
|
-
* 2. Each agent speaks in round-robin order
|
|
24
|
-
* 3. Agents respond to previous speakers
|
|
25
|
-
* 4. Team Lead calls for consensus
|
|
26
|
-
* 5. Veto members can block
|
|
27
|
-
* 6. If blocked, discussion continues (up to MAX_ROUNDS)
|
|
28
|
-
* 7. On consensus, Team Lead formalizes task list
|
|
29
|
-
*/
|
|
30
|
-
export async function runMeeting(params: {
|
|
31
|
-
instance: Instance;
|
|
32
|
-
runtime: SquadRuntime;
|
|
33
|
-
objective: string;
|
|
34
|
-
}): Promise<MeetingResult> {
|
|
35
|
-
const log = logger();
|
|
36
|
-
const { instance, runtime, objective } = params;
|
|
37
|
-
const meetingLog: string[] = [];
|
|
38
|
-
|
|
39
|
-
await transitionInstance(instance.id, 'meeting');
|
|
40
|
-
|
|
41
|
-
await getEventBus().emit({
|
|
42
|
-
id: crypto.randomUUID(),
|
|
43
|
-
timestamp: new Date(),
|
|
44
|
-
type: 'instance:meeting_started',
|
|
45
|
-
squadId: instance.squadId,
|
|
46
|
-
instanceId: instance.id,
|
|
47
|
-
data: { objective },
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const teamLead = runtime.members.get('team-lead');
|
|
51
|
-
if (!teamLead) throw new Error('No team lead available for meeting');
|
|
52
|
-
|
|
53
|
-
// Get non-lead agents for discussion
|
|
54
|
-
const participants = [...runtime.members.entries()].filter(([role]) => role !== 'team-lead');
|
|
55
|
-
|
|
56
|
-
// Step 1: Team lead presents the objective
|
|
57
|
-
const presentation = await teamLead.send(
|
|
58
|
-
`You are starting a round-table meeting. Present the following objective to your team and ask for input:\n\nObjective: ${objective}\n\nProvide a brief summary of the work needed and what expertise is required. Then ask each team member for their perspective.`,
|
|
59
|
-
);
|
|
60
|
-
meetingLog.push(`[team-lead] ${presentation}`);
|
|
61
|
-
|
|
62
|
-
await emitContribution(instance, 'team-lead', presentation);
|
|
63
|
-
|
|
64
|
-
// Step 2-3: Round-robin discussion
|
|
65
|
-
let round = 0;
|
|
66
|
-
let consensusReached = false;
|
|
67
|
-
let vetoReason: string | undefined;
|
|
68
|
-
|
|
69
|
-
while (round < MAX_ROUNDS && !consensusReached) {
|
|
70
|
-
round++;
|
|
71
|
-
log.info({ instanceId: instance.id, round }, 'Meeting round');
|
|
72
|
-
|
|
73
|
-
// Each participant speaks
|
|
74
|
-
for (const [role, agent] of participants) {
|
|
75
|
-
const context = meetingLog.slice(-5).join('\n\n');
|
|
76
|
-
const response = await agent.send(
|
|
77
|
-
`You are in a team meeting (round ${round}/${MAX_ROUNDS}). Here's the recent discussion:\n\n${context}\n\nProvide your professional input on the objective. Focus on your area of expertise. If you're QA, focus on testing concerns. If you have concerns, state them clearly.`,
|
|
78
|
-
);
|
|
79
|
-
meetingLog.push(`[${role}] ${response}`);
|
|
80
|
-
await emitContribution(instance, role, response);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Step 4: Team lead calls for consensus
|
|
84
|
-
const consensusCheck = await teamLead.send(
|
|
85
|
-
`The discussion so far:\n\n${meetingLog.slice(-participants.length - 1).join('\n\n')}\n\nBased on this discussion, do we have consensus to proceed? Consider all concerns raised. Reply with either:\n- "CONSENSUS: <brief summary of agreed plan>"\n- "NEED_DISCUSSION: <what needs to be resolved>"`,
|
|
86
|
-
);
|
|
87
|
-
meetingLog.push(`[team-lead] ${consensusCheck}`);
|
|
88
|
-
|
|
89
|
-
if (consensusCheck.toUpperCase().includes('CONSENSUS:')) {
|
|
90
|
-
// Step 5: Check veto members
|
|
91
|
-
const vetoMembers = [...runtime.members.entries()].filter(([role]) => {
|
|
92
|
-
const skill = runtime.skills.get(role);
|
|
93
|
-
return skill?.veto;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
let vetoed = false;
|
|
97
|
-
for (const [role, agent] of vetoMembers) {
|
|
98
|
-
if (role === 'team-lead') continue; // team lead already agreed
|
|
99
|
-
const vetoCheck = await agent.send(
|
|
100
|
-
`The team has reached consensus on the following plan:\n\n${consensusCheck}\n\nAs a veto-holding member, do you approve this plan? Reply with:\n- "APPROVE" if you agree\n- "VETO: <reason>" if you have critical concerns that must be addressed`,
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
if (vetoCheck.toUpperCase().includes('VETO:')) {
|
|
104
|
-
vetoed = true;
|
|
105
|
-
vetoReason = vetoCheck;
|
|
106
|
-
meetingLog.push(`[${role}] VETO: ${vetoCheck}`);
|
|
107
|
-
await emitVeto(instance, role, vetoCheck);
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
meetingLog.push(`[${role}] Approved`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!vetoed) {
|
|
114
|
-
consensusReached = true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Step 7: Formalize task list
|
|
120
|
-
let tasks: InstanceTask[] = [];
|
|
121
|
-
if (consensusReached) {
|
|
122
|
-
const taskPlan = await teamLead.send(
|
|
123
|
-
`Consensus reached. Now formalize the work into specific tasks. For each task, specify:\n1. A brief description\n2. Which team member role should do it\n\nFormat each task as: "TASK: <description> | ASSIGN: <role>"\n\nList all tasks needed to complete the objective.`,
|
|
124
|
-
);
|
|
125
|
-
meetingLog.push(`[team-lead] Task plan: ${taskPlan}`);
|
|
126
|
-
tasks = parseTaskList(taskPlan, instance.id);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
await getEventBus().emit({
|
|
130
|
-
id: crypto.randomUUID(),
|
|
131
|
-
timestamp: new Date(),
|
|
132
|
-
type: 'instance:meeting_complete',
|
|
133
|
-
squadId: instance.squadId,
|
|
134
|
-
instanceId: instance.id,
|
|
135
|
-
data: { consensus: consensusReached, taskCount: tasks.length, rounds: round },
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
instance.meetingLog = meetingLog;
|
|
139
|
-
instance.tasks = tasks;
|
|
140
|
-
|
|
141
|
-
log.info(
|
|
142
|
-
{ instanceId: instance.id, consensus: consensusReached, tasks: tasks.length },
|
|
143
|
-
'Meeting complete',
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
return { consensus: consensusReached, tasks, log: meetingLog, vetoReason };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Parse the team lead's task list into structured tasks */
|
|
150
|
-
function parseTaskList(taskPlan: string, instanceId: string): InstanceTask[] {
|
|
151
|
-
const tasks: InstanceTask[] = [];
|
|
152
|
-
const lines = taskPlan.split('\n');
|
|
153
|
-
|
|
154
|
-
for (const line of lines) {
|
|
155
|
-
const match = line.match(/TASK:\s*(.+?)\s*\|\s*ASSIGN:\s*(.+)/i);
|
|
156
|
-
if (match) {
|
|
157
|
-
tasks.push({
|
|
158
|
-
id: crypto.randomUUID(),
|
|
159
|
-
description: match[1].trim(),
|
|
160
|
-
assignedTo: match[2].trim().toLowerCase().replace(/\s+/g, '-'),
|
|
161
|
-
status: 'pending',
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return tasks;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function emitContribution(instance: Instance, role: string, content: string) {
|
|
170
|
-
await getEventBus().emit({
|
|
171
|
-
id: crypto.randomUUID(),
|
|
172
|
-
timestamp: new Date(),
|
|
173
|
-
type: 'meeting:contribution',
|
|
174
|
-
squadId: instance.squadId,
|
|
175
|
-
instanceId: instance.id,
|
|
176
|
-
agentRole: role,
|
|
177
|
-
content: content.slice(0, 500),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function emitVeto(instance: Instance, role: string, reason: string) {
|
|
182
|
-
await getEventBus().emit({
|
|
183
|
-
id: crypto.randomUUID(),
|
|
184
|
-
timestamp: new Date(),
|
|
185
|
-
type: 'meeting:veto',
|
|
186
|
-
squadId: instance.squadId,
|
|
187
|
-
instanceId: instance.id,
|
|
188
|
-
agentRole: role,
|
|
189
|
-
content: reason,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { createChildLogger } from '../../logging/logger.js';
|
|
3
|
-
import type { Instance } from './instance.js';
|
|
4
|
-
import { transitionInstance } from './instance.js';
|
|
5
|
-
|
|
6
|
-
const logger = () => createChildLogger('pr');
|
|
7
|
-
|
|
8
|
-
export interface PrResult {
|
|
9
|
-
url: string;
|
|
10
|
-
number: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Create a pull request from the instance's worktree branch.
|
|
15
|
-
* Commits any uncommitted changes, pushes the branch, then creates a PR.
|
|
16
|
-
*/
|
|
17
|
-
export async function createPullRequest(params: {
|
|
18
|
-
instance: Instance;
|
|
19
|
-
title: string;
|
|
20
|
-
squadName: string;
|
|
21
|
-
}): Promise<PrResult | null> {
|
|
22
|
-
const log = logger();
|
|
23
|
-
const { instance, title, squadName } = params;
|
|
24
|
-
|
|
25
|
-
if (!instance.worktree || !instance.branch) {
|
|
26
|
-
log.warn({ instanceId: instance.id }, 'No worktree/branch, skipping PR creation');
|
|
27
|
-
await transitionInstance(instance.id, 'complete');
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const cwd = instance.worktree.path;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// Check if there are changes to commit
|
|
35
|
-
const status = execSync('git status --porcelain', { cwd, encoding: 'utf-8' }).trim();
|
|
36
|
-
if (!status) {
|
|
37
|
-
log.info({ instanceId: instance.id }, 'No changes to commit');
|
|
38
|
-
await transitionInstance(instance.id, 'complete');
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Stage and commit all changes
|
|
43
|
-
execSync('git add -A', { cwd, stdio: 'pipe' });
|
|
44
|
-
|
|
45
|
-
const commitMessage = buildCommitMessage(title, instance, squadName);
|
|
46
|
-
execSync(`git commit -m "${escapeShell(commitMessage)}"`, { cwd, stdio: 'pipe' });
|
|
47
|
-
|
|
48
|
-
// Push the branch
|
|
49
|
-
execSync(`git push -u origin "${instance.branch}"`, { cwd, stdio: 'pipe' });
|
|
50
|
-
|
|
51
|
-
// Create the PR using gh CLI
|
|
52
|
-
const prBody = buildPrBody(instance, squadName);
|
|
53
|
-
const prOutput = execSync(
|
|
54
|
-
`gh pr create --title "${escapeShell(title)}" --body "${escapeShell(prBody)}" --head "${instance.branch}"`,
|
|
55
|
-
{ cwd, encoding: 'utf-8', stdio: 'pipe' },
|
|
56
|
-
).trim();
|
|
57
|
-
|
|
58
|
-
// Parse the PR URL to get the number
|
|
59
|
-
const prUrl = prOutput;
|
|
60
|
-
const prNumber = Number.parseInt(prUrl.split('/').pop() ?? '0', 10);
|
|
61
|
-
|
|
62
|
-
await transitionInstance(instance.id, 'complete');
|
|
63
|
-
log.info({ instanceId: instance.id, prUrl, prNumber }, 'PR created');
|
|
64
|
-
|
|
65
|
-
return { url: prUrl, number: prNumber };
|
|
66
|
-
} catch (err) {
|
|
67
|
-
log.error({ err, instanceId: instance.id }, 'Failed to create PR');
|
|
68
|
-
await transitionInstance(instance.id, 'failed');
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function buildCommitMessage(title: string, instance: Instance, squadName: string): string {
|
|
74
|
-
const lines = [`feat: ${title}`, '', `Squad: ${squadName}`, `Instance: ${instance.id}`];
|
|
75
|
-
|
|
76
|
-
if (instance.issueRef) {
|
|
77
|
-
lines.push(`Closes ${instance.issueRef}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const completedTasks = instance.tasks.filter((t) => t.status === 'done');
|
|
81
|
-
if (completedTasks.length > 0) {
|
|
82
|
-
lines.push('', 'Tasks completed:');
|
|
83
|
-
for (const task of completedTasks) {
|
|
84
|
-
lines.push(`- ${task.description} (${task.assignedTo})`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return lines.join('\n');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function buildPrBody(instance: Instance, squadName: string): string {
|
|
92
|
-
const sections: string[] = [];
|
|
93
|
-
|
|
94
|
-
sections.push(`## 🤖 Generated by IO Squad: ${squadName}`);
|
|
95
|
-
sections.push('');
|
|
96
|
-
|
|
97
|
-
if (instance.issueRef) {
|
|
98
|
-
sections.push(`Closes ${instance.issueRef}`);
|
|
99
|
-
sections.push('');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Tasks section
|
|
103
|
-
sections.push('### Tasks Completed');
|
|
104
|
-
for (const task of instance.tasks) {
|
|
105
|
-
const icon = task.status === 'done' ? '✅' : task.status === 'failed' ? '❌' : '⏳';
|
|
106
|
-
sections.push(`${icon} ${task.description} — *${task.assignedTo}*`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Meeting highlights (last few entries)
|
|
110
|
-
if (instance.meetingLog.length > 0) {
|
|
111
|
-
sections.push('');
|
|
112
|
-
sections.push('<details><summary>Meeting Discussion</summary>');
|
|
113
|
-
sections.push('');
|
|
114
|
-
const recentLog = instance.meetingLog.slice(-10);
|
|
115
|
-
for (const entry of recentLog) {
|
|
116
|
-
sections.push(`> ${entry.slice(0, 200)}`);
|
|
117
|
-
sections.push('');
|
|
118
|
-
}
|
|
119
|
-
sections.push('</details>');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return sections.join('\n');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function escapeShell(str: string): string {
|
|
126
|
-
return str.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
127
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { Squad } from '@io/shared';
|
|
2
|
-
import { createChildLogger } from '../../logging/logger.js';
|
|
3
|
-
import { type SquadRuntime, bootSquad, getSquadRuntime } from '../manager.js';
|
|
4
|
-
import {
|
|
5
|
-
type Instance,
|
|
6
|
-
type PrResult,
|
|
7
|
-
cleanupInstance,
|
|
8
|
-
createInstance,
|
|
9
|
-
createPullRequest,
|
|
10
|
-
executeTasks,
|
|
11
|
-
runMeeting,
|
|
12
|
-
} from './index.js';
|
|
13
|
-
|
|
14
|
-
const logger = () => createChildLogger('runner');
|
|
15
|
-
|
|
16
|
-
export interface RunResult {
|
|
17
|
-
instanceId: string;
|
|
18
|
-
success: boolean;
|
|
19
|
-
pr?: PrResult | null;
|
|
20
|
-
error?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Run a full instance lifecycle:
|
|
25
|
-
* 1. Create instance (with worktree)
|
|
26
|
-
* 2. Hold round-table meeting
|
|
27
|
-
* 3. Execute tasks
|
|
28
|
-
* 4. Create PR
|
|
29
|
-
* 5. Clean up
|
|
30
|
-
*/
|
|
31
|
-
export async function runInstance(params: {
|
|
32
|
-
squad: Squad;
|
|
33
|
-
objective: string;
|
|
34
|
-
issueRef?: string;
|
|
35
|
-
}): Promise<RunResult> {
|
|
36
|
-
const log = logger();
|
|
37
|
-
const { squad, objective, issueRef } = params;
|
|
38
|
-
|
|
39
|
-
// Ensure squad is booted
|
|
40
|
-
let runtime: SquadRuntime | undefined = getSquadRuntime(squad.id);
|
|
41
|
-
if (!runtime) {
|
|
42
|
-
runtime = await bootSquad(squad);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 1. Create instance
|
|
46
|
-
const instance = await createInstance({ squad, issueRef, objective });
|
|
47
|
-
log.info({ instanceId: instance.id }, 'Starting instance run');
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
// 2. Run meeting
|
|
51
|
-
const meetingResult = await runMeeting({ instance, runtime, objective });
|
|
52
|
-
|
|
53
|
-
if (!meetingResult.consensus) {
|
|
54
|
-
log.warn({ instanceId: instance.id }, 'Meeting did not reach consensus');
|
|
55
|
-
return {
|
|
56
|
-
instanceId: instance.id,
|
|
57
|
-
success: false,
|
|
58
|
-
error: `Meeting failed to reach consensus${meetingResult.vetoReason ? `: ${meetingResult.vetoReason}` : ''}`,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (instance.tasks.length === 0) {
|
|
63
|
-
log.warn({ instanceId: instance.id }, 'No tasks generated from meeting');
|
|
64
|
-
return {
|
|
65
|
-
instanceId: instance.id,
|
|
66
|
-
success: false,
|
|
67
|
-
error: 'Meeting produced no actionable tasks',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 3. Execute tasks
|
|
72
|
-
await executeTasks({ instance, runtime });
|
|
73
|
-
|
|
74
|
-
// 4. Create PR
|
|
75
|
-
const prTitle = objective.slice(0, 72);
|
|
76
|
-
const pr = await createPullRequest({ instance, title: prTitle, squadName: squad.name });
|
|
77
|
-
|
|
78
|
-
// 5. Cleanup (if no PR was created — otherwise keep the branch for review)
|
|
79
|
-
if (!pr) {
|
|
80
|
-
await cleanupInstance(instance.id, squad);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
instanceId: instance.id,
|
|
85
|
-
success: true,
|
|
86
|
-
pr,
|
|
87
|
-
};
|
|
88
|
-
} catch (err) {
|
|
89
|
-
log.error({ err, instanceId: instance.id }, 'Instance run failed');
|
|
90
|
-
await cleanupInstance(instance.id, squad);
|
|
91
|
-
return {
|
|
92
|
-
instanceId: instance.id,
|
|
93
|
-
success: false,
|
|
94
|
-
error: err instanceof Error ? err.message : String(err),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|