@automagik/genie 4.260331.4 → 4.260331.6
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/.claude-plugin/marketplace.json +1 -1
- package/CLAUDE.md +31 -5
- package/dist/genie.js +652 -615
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/skills/genie/SKILL.md +71 -233
- package/skills/genie/reference/lifecycle.md +65 -0
- package/src/db/migrations/016_team_spawner.sql +5 -0
- package/src/db/migrations/017_wishes_table.sql +18 -0
- package/src/genie-commands/session.ts +19 -7
- package/src/genie.ts +26 -3
- package/src/hooks/handlers/auto-spawn.ts +14 -0
- package/src/lib/agent-registry.ts +25 -5
- package/src/lib/claude-native-teams.ts +69 -45
- package/src/lib/protocol-router-spawn.ts +10 -1
- package/src/lib/qa-runner.ts +1 -1
- package/src/lib/team-auto-spawn.ts +7 -2
- package/src/lib/team-manager.test.ts +45 -0
- package/src/lib/team-manager.ts +20 -3
- package/src/lib/wish-resolve.test.ts +108 -0
- package/src/lib/wish-resolve.ts +124 -0
- package/src/lib/wish-sync.test.ts +141 -0
- package/src/lib/wish-sync.ts +182 -0
- package/src/term-commands/agent/index.ts +6 -0
- package/src/term-commands/agent/send.ts +16 -2
- package/src/term-commands/agents.ts +22 -3
- package/src/term-commands/dir.ts +0 -92
- package/src/term-commands/dispatch.ts +52 -3
- package/src/term-commands/msg.ts +54 -14
- package/src/term-commands/state.ts +35 -5
- package/src/term-commands/team.ts +23 -8
package/src/term-commands/msg.ts
CHANGED
|
@@ -99,6 +99,29 @@ async function findMemberByPane(teamName: string, paneId: string): Promise<strin
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Leader Alias Resolution
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the 'team-lead' alias to the actual leader name for a given team context.
|
|
108
|
+
* Falls back to 'team-lead' for legacy teams without a leader set.
|
|
109
|
+
*/
|
|
110
|
+
async function resolveLeaderAlias(recipient: string, teamContext?: string): Promise<string> {
|
|
111
|
+
if (recipient !== 'team-lead') return recipient;
|
|
112
|
+
|
|
113
|
+
const teamManager = await getTeamManager();
|
|
114
|
+
|
|
115
|
+
// Try explicit team context first
|
|
116
|
+
const teamName = teamContext ?? process.env.GENIE_TEAM;
|
|
117
|
+
if (teamName) {
|
|
118
|
+
const config = await teamManager.getTeam(teamName);
|
|
119
|
+
if (config?.leader) return config.leader;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return 'team-lead';
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
// ============================================================================
|
|
103
126
|
// Scope Checking
|
|
104
127
|
// ============================================================================
|
|
@@ -124,11 +147,12 @@ export async function checkSendScope(_repoPath: string, sender: string, recipien
|
|
|
124
147
|
return `Scope violation: "${recipient}" is not in sender's team(s): ${teamNames}`;
|
|
125
148
|
}
|
|
126
149
|
|
|
127
|
-
/** Build the list of teams the sender belongs to, including env-based
|
|
150
|
+
/** Build the list of teams the sender belongs to, including env-based leader membership. */
|
|
128
151
|
function resolveSenderTeams(teams: teamManagerTypes.TeamConfig[], sender: string): teamManagerTypes.TeamConfig[] {
|
|
129
152
|
let senderTeams = teams.filter((t) => t.members.includes(sender));
|
|
130
153
|
|
|
131
|
-
|
|
154
|
+
// If sender is the leader (by name or 'team-lead' alias), include the leader's team
|
|
155
|
+
if (sender === 'team-lead' || teams.some((t) => t.leader === sender)) {
|
|
132
156
|
const envTeam = process.env.GENIE_TEAM;
|
|
133
157
|
if (envTeam) {
|
|
134
158
|
const leaderTeam = teams.find((t) => t.name === envTeam);
|
|
@@ -141,9 +165,10 @@ function resolveSenderTeams(teams: teamManagerTypes.TeamConfig[], sender: string
|
|
|
141
165
|
return senderTeams;
|
|
142
166
|
}
|
|
143
167
|
|
|
144
|
-
/** Check whether a recipient is reachable within a given team (direct member,
|
|
168
|
+
/** Check whether a recipient is reachable within a given team (direct member, leader, or prefixed name). */
|
|
145
169
|
function isRecipientInTeam(team: teamManagerTypes.TeamConfig, recipient: string): boolean {
|
|
146
|
-
|
|
170
|
+
// Direct member, legacy team-lead alias, or actual leader name
|
|
171
|
+
if (team.members.includes(recipient) || recipient === 'team-lead' || recipient === team.leader) return true;
|
|
147
172
|
if (recipient.startsWith(`${team.name}-`)) {
|
|
148
173
|
const roleOnly = recipient.slice(team.name.length + 1);
|
|
149
174
|
if (team.members.includes(roleOnly)) return true;
|
|
@@ -161,9 +186,13 @@ async function findAgentTeam(_repoPath: string, agentName: string): Promise<team
|
|
|
161
186
|
const memberTeam = teams.find((t) => t.members.includes(agentName));
|
|
162
187
|
if (memberTeam) return memberTeam;
|
|
163
188
|
|
|
164
|
-
|
|
189
|
+
// Match by leader name or legacy 'team-lead' alias
|
|
190
|
+
if (agentName === 'team-lead' || teams.some((t) => t.leader === agentName)) {
|
|
165
191
|
const envTeam = process.env.GENIE_TEAM;
|
|
166
192
|
if (envTeam) return teams.find((t) => t.name === envTeam) ?? null;
|
|
193
|
+
// Also find by leader field directly
|
|
194
|
+
const leaderTeam = teams.find((t) => t.leader === agentName);
|
|
195
|
+
if (leaderTeam) return leaderTeam;
|
|
167
196
|
}
|
|
168
197
|
|
|
169
198
|
return null;
|
|
@@ -430,14 +459,17 @@ async function handleSend(body: string, options: { to: string; from?: string; te
|
|
|
430
459
|
const repoPath = process.cwd();
|
|
431
460
|
const from = options.from ?? (await detectSenderIdentity(options.team));
|
|
432
461
|
|
|
433
|
-
|
|
462
|
+
// Resolve 'team-lead' alias to actual leader name
|
|
463
|
+
const to = await resolveLeaderAlias(options.to, options.team);
|
|
464
|
+
|
|
465
|
+
const scopeError = await checkSendScope(repoPath, from, to);
|
|
434
466
|
if (scopeError) {
|
|
435
467
|
console.error(`Error: ${scopeError}`);
|
|
436
468
|
process.exit(1);
|
|
437
469
|
}
|
|
438
470
|
|
|
439
471
|
const senderActor = localActor(from);
|
|
440
|
-
const recipientActor = localActor(
|
|
472
|
+
const recipientActor = localActor(to);
|
|
441
473
|
|
|
442
474
|
const conv = await ts.findOrCreateConversation({
|
|
443
475
|
type: 'dm',
|
|
@@ -448,19 +480,19 @@ async function handleSend(body: string, options: { to: string; from?: string; te
|
|
|
448
480
|
await ts.addMember(conv.id, senderActor);
|
|
449
481
|
await ts.addMember(conv.id, recipientActor);
|
|
450
482
|
|
|
451
|
-
const mailboxMessage = await mailbox.send(repoPath, from,
|
|
483
|
+
const mailboxMessage = await mailbox.send(repoPath, from, to, body);
|
|
452
484
|
const msg = await ts.sendMessage(conv.id, senderActor, body);
|
|
453
485
|
|
|
454
486
|
// Emit runtime event for real-time observability (fire-and-forget)
|
|
455
487
|
try {
|
|
456
488
|
const { publishSubjectEvent } = await import('../lib/runtime-events.js');
|
|
457
|
-
await publishSubjectEvent(repoPath, `genie.msg.${
|
|
489
|
+
await publishSubjectEvent(repoPath, `genie.msg.${to}`, {
|
|
458
490
|
kind: 'message',
|
|
459
491
|
agent: from,
|
|
460
492
|
direction: 'out',
|
|
461
|
-
peer:
|
|
493
|
+
peer: to,
|
|
462
494
|
text: body,
|
|
463
|
-
data: { messageId: msg.id, conversationId: conv.id, from, to
|
|
495
|
+
data: { messageId: msg.id, conversationId: conv.id, from, to },
|
|
464
496
|
source: 'mailbox',
|
|
465
497
|
});
|
|
466
498
|
} catch {
|
|
@@ -468,16 +500,16 @@ async function handleSend(body: string, options: { to: string; from?: string; te
|
|
|
468
500
|
}
|
|
469
501
|
|
|
470
502
|
// Best-effort native inbox bridge
|
|
471
|
-
const bridged = await bridgeToNativeInbox(from,
|
|
503
|
+
const bridged = await bridgeToNativeInbox(from, to, body, options.team).catch((err) => {
|
|
472
504
|
const reason = err instanceof Error ? err.message : String(err);
|
|
473
505
|
console.warn(`[genie send] Native inbox bridge failed: ${reason}`);
|
|
474
506
|
return false;
|
|
475
507
|
});
|
|
476
508
|
if (bridged) {
|
|
477
|
-
await mailbox.markDelivered(repoPath,
|
|
509
|
+
await mailbox.markDelivered(repoPath, to, mailboxMessage.id).catch(() => {});
|
|
478
510
|
}
|
|
479
511
|
|
|
480
|
-
console.log(`Message sent to "${
|
|
512
|
+
console.log(`Message sent to "${to}".`);
|
|
481
513
|
console.log(` ID: ${msg.id}`);
|
|
482
514
|
console.log(` Conversation: ${conv.id}`);
|
|
483
515
|
}
|
|
@@ -494,6 +526,14 @@ export function registerSendInboxCommands(program: Command): void {
|
|
|
494
526
|
.option('--to <agent>', 'Recipient agent name (default: team-lead)', 'team-lead')
|
|
495
527
|
.option('--from <sender>', 'Sender ID (auto-detected from context)')
|
|
496
528
|
.option('--team <name>', 'Explicit team context for sender/recipient resolution')
|
|
529
|
+
.addHelpText(
|
|
530
|
+
'after',
|
|
531
|
+
`
|
|
532
|
+
Examples:
|
|
533
|
+
genie send 'start task #3' --to engineer # Message a specific agent
|
|
534
|
+
genie send 'status update' --to team-lead # Report to team lead
|
|
535
|
+
genie send 'deploy ready' --team my-feature # Message within team context`,
|
|
536
|
+
)
|
|
497
537
|
.action(async (body: string, options: { to: string; from?: string; team?: string }) => {
|
|
498
538
|
try {
|
|
499
539
|
await handleSend(body, options);
|
|
@@ -235,7 +235,27 @@ function autoKillPane(): void {
|
|
|
235
235
|
// ============================================================================
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
|
-
*
|
|
238
|
+
* Resolve the leader name and spawner for the current team context.
|
|
239
|
+
* Falls back to 'team-lead' for legacy teams.
|
|
240
|
+
*/
|
|
241
|
+
async function resolveNotificationTargets(): Promise<{ leader: string; spawner?: string }> {
|
|
242
|
+
const teamName = process.env.GENIE_TEAM;
|
|
243
|
+
if (!teamName) return { leader: 'team-lead' };
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const teamManager = await import('../lib/team-manager.js');
|
|
247
|
+
const config = await teamManager.getTeam(teamName);
|
|
248
|
+
return {
|
|
249
|
+
leader: config?.leader || 'team-lead',
|
|
250
|
+
spawner: config?.spawner,
|
|
251
|
+
};
|
|
252
|
+
} catch {
|
|
253
|
+
return { leader: 'team-lead' };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Notify leader (and spawner) of wave or wish completion via protocol-router.
|
|
239
259
|
* Best-effort — failures are logged but do not block the done flow.
|
|
240
260
|
*/
|
|
241
261
|
async function notifyWaveCompletion(
|
|
@@ -246,17 +266,27 @@ async function notifyWaveCompletion(
|
|
|
246
266
|
try {
|
|
247
267
|
const protocolRouter = await import('../lib/protocol-router.js');
|
|
248
268
|
const repoPath = process.cwd();
|
|
269
|
+
const { leader, spawner } = await resolveNotificationTargets();
|
|
270
|
+
|
|
249
271
|
const message = wishComplete
|
|
250
272
|
? `WISH COMPLETE — all groups done: [${waveResult.waveGroups.join(', ')}]. Run \`genie team done\` to clean up.`
|
|
251
273
|
: `${waveResult.waveName} complete. All groups done: [${waveResult.waveGroups.join(', ')}]. Run /review or advance to next wave.`;
|
|
252
|
-
|
|
274
|
+
|
|
275
|
+
// Notify leader
|
|
276
|
+
const result = await protocolRouter.sendMessage(repoPath, 'cli', leader, message);
|
|
253
277
|
if (result && typeof result === 'object' && 'delivered' in result && !result.delivered) {
|
|
254
|
-
console.warn(
|
|
278
|
+
console.warn(` ⚠️ Wave-complete notification to ${leader} may not have been delivered.`);
|
|
255
279
|
} else {
|
|
256
|
-
console.log(
|
|
280
|
+
console.log(` Notified ${leader} of wave completion.`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Also notify spawner if different from leader
|
|
284
|
+
if (spawner && spawner !== leader && spawner !== 'cli') {
|
|
285
|
+
await protocolRouter.sendMessage(repoPath, 'cli', spawner, message).catch(() => {});
|
|
286
|
+
console.log(` Notified spawner (${spawner}) of wave completion.`);
|
|
257
287
|
}
|
|
258
288
|
} catch {
|
|
259
|
-
console.warn(' ⚠️ Could not notify
|
|
289
|
+
console.warn(' ⚠️ Could not notify leader (messaging unavailable).');
|
|
260
290
|
}
|
|
261
291
|
}
|
|
262
292
|
|
|
@@ -29,6 +29,14 @@ export function registerTeamNamespace(program: Command): void {
|
|
|
29
29
|
.option('--tmux-session <name>', 'Tmux session to place team window in (default: derived from repo path)')
|
|
30
30
|
.option('--session <name>', 'Alias for --tmux-session (deprecated)')
|
|
31
31
|
.option('--no-spawn', 'Create team and copy wish without spawning the leader (useful for testing)')
|
|
32
|
+
.addHelpText(
|
|
33
|
+
'after',
|
|
34
|
+
`
|
|
35
|
+
Examples:
|
|
36
|
+
genie team create my-feature --repo . # Create team in current repo
|
|
37
|
+
genie team create my-feature --repo . --wish my-feature-slug # Create team with a wish
|
|
38
|
+
genie team create hotfix --repo . --branch main # Create from main branch`,
|
|
39
|
+
)
|
|
32
40
|
.action(
|
|
33
41
|
async (
|
|
34
42
|
name: string,
|
|
@@ -245,7 +253,7 @@ export function registerTeamNamespace(program: Command): void {
|
|
|
245
253
|
// Team Create Handler (extracted for cognitive complexity)
|
|
246
254
|
// ============================================================================
|
|
247
255
|
|
|
248
|
-
async function handleTeamCreate(
|
|
256
|
+
export async function handleTeamCreate(
|
|
249
257
|
name: string,
|
|
250
258
|
options: { repo: string; branch: string; wish?: string; tmuxSession?: string; spawn?: boolean },
|
|
251
259
|
): Promise<void> {
|
|
@@ -328,6 +336,11 @@ async function spawnLeaderWithWish(
|
|
|
328
336
|
config.tmuxSessionName = tmuxSession;
|
|
329
337
|
await teamManager.updateTeamConfig(config.name, config);
|
|
330
338
|
|
|
339
|
+
// Set leader name = wish slug, spawner = caller identity
|
|
340
|
+
config.leader = slug;
|
|
341
|
+
config.spawner = process.env.GENIE_AGENT_NAME || 'cli';
|
|
342
|
+
await teamManager.updateTeamConfig(config.name, config);
|
|
343
|
+
|
|
331
344
|
// Locate WISH.md in source repo
|
|
332
345
|
const sourceWishPath = join(resolvedRepo, '.genie', 'wishes', slug, 'WISH.md');
|
|
333
346
|
if (!existsSync(sourceWishPath)) {
|
|
@@ -342,17 +355,19 @@ async function spawnLeaderWithWish(
|
|
|
342
355
|
await copyFile(sourceWishPath, destWishPath);
|
|
343
356
|
console.log(` Wish: copied ${slug}/WISH.md into worktree`);
|
|
344
357
|
|
|
345
|
-
// Hire the standard team:
|
|
346
|
-
const
|
|
358
|
+
// Hire the standard team: leader + engineer + reviewer + qa + fix
|
|
359
|
+
const leaderName = config.leader || 'team-lead';
|
|
360
|
+
const standardTeam = [leaderName, 'engineer', 'reviewer', 'qa', 'fix'];
|
|
347
361
|
for (const role of standardTeam) {
|
|
348
362
|
await teamManager.hireAgent(config.name, role);
|
|
349
363
|
}
|
|
350
364
|
console.log(` Team: hired ${standardTeam.join(', ')}`);
|
|
351
365
|
|
|
352
366
|
// Spawn leader — AGENTS.md comes from the built-in resolver, prompt delivered as initialPrompt
|
|
353
|
-
const members = standardTeam.filter((r) => r !==
|
|
354
|
-
const
|
|
355
|
-
|
|
367
|
+
const members = standardTeam.filter((r) => r !== leaderName).join(', ');
|
|
368
|
+
const spawner = config.spawner || 'cli';
|
|
369
|
+
const kickoffPrompt = `Your team is "${config.name}". Repo: ${config.repo}. Branch: ${config.name}. Worktree: ${config.worktreePath}. Wish slug: ${slug}. Your team members are: ${members} (already hired — genie work will spawn them automatically). Report completion to: ${spawner} (via genie send --to ${spawner}). Read the wish at .genie/wishes/${slug}/WISH.md and execute the full lifecycle autonomously.`;
|
|
370
|
+
await handleWorkerSpawn(leaderName, {
|
|
356
371
|
provider: 'claude',
|
|
357
372
|
team: config.name,
|
|
358
373
|
cwd: config.worktreePath,
|
|
@@ -362,9 +377,9 @@ async function spawnLeaderWithWish(
|
|
|
362
377
|
|
|
363
378
|
// Deliver kickoff prompt via mailbox as backup (durable, queued to disk)
|
|
364
379
|
const protocolRouter = await import('../lib/protocol-router.js');
|
|
365
|
-
const result = await protocolRouter.sendMessage(config.worktreePath, 'cli',
|
|
380
|
+
const result = await protocolRouter.sendMessage(config.worktreePath, 'cli', leaderName, kickoffPrompt);
|
|
366
381
|
if (!result.delivered) {
|
|
367
|
-
console.warn(`⚠ Backup delivery to
|
|
382
|
+
console.warn(`⚠ Backup delivery to ${leaderName} failed: ${result.reason ?? 'unknown'}`);
|
|
368
383
|
}
|
|
369
384
|
console.log(' Leader: spawned and working');
|
|
370
385
|
}
|