@automagik/genie 4.260331.3 → 4.260331.5
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/dist/genie.js +179 -170
- 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/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/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/send.ts +16 -2
- package/src/term-commands/agents.ts +22 -3
- package/src/term-commands/dispatch.ts +52 -3
- package/src/term-commands/msg.ts +46 -14
- package/src/term-commands/state.ts +35 -5
- package/src/term-commands/team.ts +15 -8
|
@@ -24,6 +24,7 @@ import { tmpdir } from 'node:os';
|
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import type { Command } from 'commander';
|
|
26
26
|
import * as protocolRouter from '../lib/protocol-router.js';
|
|
27
|
+
import { parseWishRef, resolveWish } from '../lib/wish-resolve.js';
|
|
27
28
|
import type { GroupDefinition } from '../lib/wish-state.js';
|
|
28
29
|
import * as wishState from '../lib/wish-state.js';
|
|
29
30
|
import { handleWorkerSpawn } from './agents.js';
|
|
@@ -291,6 +292,28 @@ function buildFallbackWaves(content: string): Wave[] {
|
|
|
291
292
|
];
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Leader Resolution
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolve the leader name for --to in dispatch prompts.
|
|
301
|
+
* Uses GENIE_TEAM to look up the team config's leader field.
|
|
302
|
+
* Falls back to 'team-lead' for legacy teams.
|
|
303
|
+
*/
|
|
304
|
+
async function resolveLeaderTarget(): Promise<string> {
|
|
305
|
+
const teamName = process.env.GENIE_TEAM;
|
|
306
|
+
if (!teamName) return 'team-lead';
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const teamManager = await import('../lib/team-manager.js');
|
|
310
|
+
const config = await teamManager.getTeam(teamName);
|
|
311
|
+
return config?.leader || 'team-lead';
|
|
312
|
+
} catch {
|
|
313
|
+
return 'team-lead';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
294
317
|
// ============================================================================
|
|
295
318
|
// Auto-Orchestration (fire-and-forget)
|
|
296
319
|
// ============================================================================
|
|
@@ -333,7 +356,28 @@ export function detectWorkMode(
|
|
|
333
356
|
* notifying the team-lead.
|
|
334
357
|
*/
|
|
335
358
|
async function autoOrchestrateCommand(slug: string): Promise<void> {
|
|
336
|
-
|
|
359
|
+
let wishPath: string;
|
|
360
|
+
let actualSlug = slug;
|
|
361
|
+
|
|
362
|
+
// Check for namespace/slug format — resolve and auto-create team
|
|
363
|
+
const parsed = parseWishRef(slug);
|
|
364
|
+
if (parsed.namespace) {
|
|
365
|
+
const resolved = await resolveWish(slug);
|
|
366
|
+
wishPath = resolved.wishPath;
|
|
367
|
+
actualSlug = resolved.slug;
|
|
368
|
+
|
|
369
|
+
// Auto-create team using the resolved repo and session
|
|
370
|
+
const { handleTeamCreate } = await import('./team.js');
|
|
371
|
+
await handleTeamCreate(actualSlug, {
|
|
372
|
+
repo: resolved.repo,
|
|
373
|
+
branch: 'dev',
|
|
374
|
+
wish: actualSlug,
|
|
375
|
+
tmuxSession: resolved.session,
|
|
376
|
+
});
|
|
377
|
+
return; // handleTeamCreate spawns the leader, which runs the full lifecycle
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
wishPath = join(process.cwd(), '.genie', 'wishes', slug, 'WISH.md');
|
|
337
381
|
|
|
338
382
|
if (!existsSync(wishPath)) {
|
|
339
383
|
console.error(`❌ Wish not found: ${wishPath}`);
|
|
@@ -341,6 +385,9 @@ async function autoOrchestrateCommand(slug: string): Promise<void> {
|
|
|
341
385
|
process.exit(1);
|
|
342
386
|
}
|
|
343
387
|
|
|
388
|
+
// Best-effort: sync wish to PG index (non-blocking)
|
|
389
|
+
import('../lib/wish-sync.js').then((ws) => ws.syncWishes(process.cwd())).catch(() => {});
|
|
390
|
+
|
|
344
391
|
const content = await readFile(wishPath, 'utf-8');
|
|
345
392
|
const groups = parseWishGroups(content);
|
|
346
393
|
const waves = parseExecutionStrategy(content);
|
|
@@ -545,7 +592,8 @@ async function workDispatchCommand(agentName: string, ref: string): Promise<void
|
|
|
545
592
|
console.log(` Group: ${group}`);
|
|
546
593
|
|
|
547
594
|
const effectiveRole = `${agentName}-${group}`;
|
|
548
|
-
const
|
|
595
|
+
const leaderTarget = await resolveLeaderTarget();
|
|
596
|
+
const workPrompt = `Execute Group ${group} of wish "${slug}". Your full context is in the system prompt. Read the wish at ${wishPath} if needed. Implement all deliverables, run validation, and report completion.\n\nWhen done:\n1. Run: genie done ${slug}#${group}\n2. Run: genie send 'Group ${group} complete. <summary>' --to ${leaderTarget}`;
|
|
549
597
|
await handleWorkerSpawn(agentName, {
|
|
550
598
|
provider: 'claude',
|
|
551
599
|
team: process.env.GENIE_TEAM ?? 'genie',
|
|
@@ -620,7 +668,8 @@ async function reviewCommand(agentName: string, ref: string): Promise<void> {
|
|
|
620
668
|
console.log(` Group: ${group}`);
|
|
621
669
|
if (diff) console.log(` Diff: ${diff.split('\n').length} lines`);
|
|
622
670
|
|
|
623
|
-
const
|
|
671
|
+
const reviewLeaderTarget = await resolveLeaderTarget();
|
|
672
|
+
const reviewPrompt = `Review "${ref}". Your context and diff are in the system prompt. Evaluate against acceptance criteria and return SHIP, FIX-FIRST, or BLOCKED with severity-tagged findings.\n\nWhen done, report your verdict:\nRun: genie send '<SHIP|FIX-FIRST|BLOCKED> — <summary>' --to ${reviewLeaderTarget}`;
|
|
624
673
|
await handleWorkerSpawn(agentName, {
|
|
625
674
|
provider: 'claude',
|
|
626
675
|
team: process.env.GENIE_TEAM ?? 'genie',
|
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
|
}
|
|
@@ -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
|
|
|
@@ -245,7 +245,7 @@ export function registerTeamNamespace(program: Command): void {
|
|
|
245
245
|
// Team Create Handler (extracted for cognitive complexity)
|
|
246
246
|
// ============================================================================
|
|
247
247
|
|
|
248
|
-
async function handleTeamCreate(
|
|
248
|
+
export async function handleTeamCreate(
|
|
249
249
|
name: string,
|
|
250
250
|
options: { repo: string; branch: string; wish?: string; tmuxSession?: string; spawn?: boolean },
|
|
251
251
|
): Promise<void> {
|
|
@@ -328,6 +328,11 @@ async function spawnLeaderWithWish(
|
|
|
328
328
|
config.tmuxSessionName = tmuxSession;
|
|
329
329
|
await teamManager.updateTeamConfig(config.name, config);
|
|
330
330
|
|
|
331
|
+
// Set leader name = wish slug, spawner = caller identity
|
|
332
|
+
config.leader = slug;
|
|
333
|
+
config.spawner = process.env.GENIE_AGENT_NAME || 'cli';
|
|
334
|
+
await teamManager.updateTeamConfig(config.name, config);
|
|
335
|
+
|
|
331
336
|
// Locate WISH.md in source repo
|
|
332
337
|
const sourceWishPath = join(resolvedRepo, '.genie', 'wishes', slug, 'WISH.md');
|
|
333
338
|
if (!existsSync(sourceWishPath)) {
|
|
@@ -342,17 +347,19 @@ async function spawnLeaderWithWish(
|
|
|
342
347
|
await copyFile(sourceWishPath, destWishPath);
|
|
343
348
|
console.log(` Wish: copied ${slug}/WISH.md into worktree`);
|
|
344
349
|
|
|
345
|
-
// Hire the standard team:
|
|
346
|
-
const
|
|
350
|
+
// Hire the standard team: leader + engineer + reviewer + qa + fix
|
|
351
|
+
const leaderName = config.leader || 'team-lead';
|
|
352
|
+
const standardTeam = [leaderName, 'engineer', 'reviewer', 'qa', 'fix'];
|
|
347
353
|
for (const role of standardTeam) {
|
|
348
354
|
await teamManager.hireAgent(config.name, role);
|
|
349
355
|
}
|
|
350
356
|
console.log(` Team: hired ${standardTeam.join(', ')}`);
|
|
351
357
|
|
|
352
358
|
// Spawn leader — AGENTS.md comes from the built-in resolver, prompt delivered as initialPrompt
|
|
353
|
-
const members = standardTeam.filter((r) => r !==
|
|
354
|
-
const
|
|
355
|
-
|
|
359
|
+
const members = standardTeam.filter((r) => r !== leaderName).join(', ');
|
|
360
|
+
const spawner = config.spawner || 'cli';
|
|
361
|
+
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.`;
|
|
362
|
+
await handleWorkerSpawn(leaderName, {
|
|
356
363
|
provider: 'claude',
|
|
357
364
|
team: config.name,
|
|
358
365
|
cwd: config.worktreePath,
|
|
@@ -362,9 +369,9 @@ async function spawnLeaderWithWish(
|
|
|
362
369
|
|
|
363
370
|
// Deliver kickoff prompt via mailbox as backup (durable, queued to disk)
|
|
364
371
|
const protocolRouter = await import('../lib/protocol-router.js');
|
|
365
|
-
const result = await protocolRouter.sendMessage(config.worktreePath, 'cli',
|
|
372
|
+
const result = await protocolRouter.sendMessage(config.worktreePath, 'cli', leaderName, kickoffPrompt);
|
|
366
373
|
if (!result.delivered) {
|
|
367
|
-
console.warn(`⚠ Backup delivery to
|
|
374
|
+
console.warn(`⚠ Backup delivery to ${leaderName} failed: ${result.reason ?? 'unknown'}`);
|
|
368
375
|
}
|
|
369
376
|
console.log(' Leader: spawned and working');
|
|
370
377
|
}
|