@automagik/genie 4.260331.6 → 4.260331.8

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.
@@ -12,22 +12,11 @@
12
12
  * Commands:
13
13
  * genie agent register <name> — Register agent locally + auto-register in Omni
14
14
  *
15
- * Storage: Primary source is `app_store` table (item_type='agent').
16
- * Legacy `agents` table kept for backward compat with spawn.
17
- * JSON cache (~/.genie/agent-directory.json) regenerated after every mutation.
15
+ * Storage: agent-directory.json is the source of truth for registered agents.
18
16
  */
19
17
 
20
18
  import { resolve as resolvePath } from 'node:path';
21
19
  import type { Command } from 'commander';
22
- import {
23
- type StoreRow,
24
- listItemsFromStore,
25
- migrateAgentDirectory,
26
- regenerateAgentCache,
27
- registerItemInStore,
28
- removeItemFromStore,
29
- updateItemInStore,
30
- } from '../lib/agent-cache.js';
31
20
  import * as directory from '../lib/agent-directory.js';
32
21
  import { printSyncResult, syncAgentDirectory } from '../lib/agent-sync.js';
33
22
  import { getActor, recordAuditEvent } from '../lib/audit.js';
@@ -65,9 +54,6 @@ export function registerDirNamespace(program: Command): void {
65
54
  .action(async (name: string, options: { global?: boolean }) => {
66
55
  try {
67
56
  const removed = await directory.rm(name, { global: options.global });
68
- // Also remove from app_store
69
- await removeItemFromStore(name).catch(() => {});
70
- await regenerateAgentCache();
71
57
  recordAuditEvent('item', name, 'item_removed', getActor(), { type: 'agent', source: 'dir_rm' }).catch(() => {});
72
58
 
73
59
  if (removed) {
@@ -164,18 +150,6 @@ async function handleDirAdd(name: string, options: DirAddOptions): Promise<void>
164
150
  { global: options.global },
165
151
  );
166
152
 
167
- // Also register in app_store (primary source of truth)
168
- try {
169
- await registerItemInStore({
170
- name,
171
- itemType: 'agent',
172
- installPath: resolvedDir,
173
- manifest: { promptMode, model: options.model, roles: normalizeRoles(options.roles), repo: options.repo },
174
- });
175
- } catch {
176
- // Best-effort — legacy agents table is still the spawn path
177
- }
178
- await regenerateAgentCache();
179
153
  recordAuditEvent('item', name, 'item_registered', getActor(), { type: 'agent', source: 'dir_add' }).catch(() => {});
180
154
 
181
155
  const scope = options.global ? 'global' : 'project';
@@ -207,16 +181,6 @@ async function handleEdit(name: string, options: EditOptions): Promise<void> {
207
181
 
208
182
  const entry = await directory.edit(name, updates, { global: options.global });
209
183
 
210
- // Also update app_store
211
- try {
212
- await updateItemInStore(name, {
213
- installPath: updates.dir,
214
- manifest: { promptMode: updates.promptMode, model: updates.model, roles: updates.roles, repo: updates.repo },
215
- });
216
- } catch {
217
- // Best-effort
218
- }
219
- await regenerateAgentCache();
220
184
  recordAuditEvent('item', name, 'item_updated', getActor(), { type: 'agent', source: 'dir_edit' }).catch(() => {});
221
185
 
222
186
  const scope = options.global ? 'global' : 'project';
@@ -279,37 +243,8 @@ async function showEntry(name: string, json?: boolean): Promise<void> {
279
243
  console.log('');
280
244
  }
281
245
 
282
- async function listEntries(json?: boolean, includeBuiltins?: boolean, includeArchived?: boolean): Promise<void> {
283
- // One-time migration from legacy JSON → DB (idempotent, best-effort)
284
- await migrateAgentDirectory().catch(() => {});
285
-
286
- // Try app_store first (primary), fall back to legacy agents table
287
- let entries: directory.ScopedDirectoryEntry[];
288
- try {
289
- const storeItems = await listItemsFromStore('agent');
290
- entries = storeItems
291
- .filter((item: StoreRow) => {
292
- if (includeArchived) return true;
293
- const manifest = (item.manifest ?? {}) as Record<string, unknown>;
294
- return !manifest.archived;
295
- })
296
- .map((item: StoreRow) => {
297
- const manifest = (item.manifest ?? {}) as Record<string, unknown>;
298
- return {
299
- name: item.name,
300
- dir: (item.install_path as string) ?? '',
301
- repo: (manifest.repo as string) ?? '',
302
- promptMode: ((manifest.promptMode as string) ?? 'append') as directory.PromptMode,
303
- model: manifest.model as string | undefined,
304
- roles: normalizeRoles(manifest.roles as string[] | undefined),
305
- registeredAt: item.installed_at as string,
306
- scope: (manifest.archived ? 'archived' : 'global') as directory.DirectoryScope,
307
- };
308
- });
309
- } catch {
310
- // Fallback to legacy
311
- entries = await directory.ls();
312
- }
246
+ async function listEntries(json?: boolean, includeBuiltins?: boolean, _includeArchived?: boolean): Promise<void> {
247
+ const entries = await directory.ls();
313
248
 
314
249
  if (json) {
315
250
  listEntriesJson(entries, includeBuiltins);
@@ -9,7 +9,6 @@
9
9
  * genie export projects — Projects
10
10
  * genie export schedules [name] — Schedules with run_spec
11
11
  * genie export agents — Agents, templates, checkpoints
12
- * genie export apps — App store (KhalOS, graceful skip)
13
12
  * genie export comms — Conversations, messages, mailbox
14
13
  * genie export config — OS config (KhalOS, graceful skip)
15
14
  */
@@ -374,18 +373,6 @@ export function registerExportCommands(program: Command): void {
374
373
  },
375
374
  );
376
375
 
377
- // genie export apps
378
- sharedOpts(exp.command('apps').description('Export app store (graceful skip if missing)')).action(
379
- async (options: ExportOptions) => {
380
- try {
381
- await runExport(['apps'], 'partial', (sql) => exportGroup(sql, 'apps'), options);
382
- } catch (error) {
383
- console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
384
- process.exit(1);
385
- }
386
- },
387
- );
388
-
389
376
  // genie export comms
390
377
  sharedOpts(exp.command('comms').description('Export conversations, messages, mailbox')).action(
391
378
  async (options: ExportOptions) => {
@@ -336,8 +336,11 @@ async function spawnLeaderWithWish(
336
336
  config.tmuxSessionName = tmuxSession;
337
337
  await teamManager.updateTeamConfig(config.name, config);
338
338
 
339
- // Set leader name = wish slug, spawner = caller identity
340
- config.leader = slug;
339
+ // Resolve leader from project's leader_agent, spawner = caller identity
340
+ const { getProjectByRepoPath } = await import('../lib/task-service.js');
341
+ const project = await getProjectByRepoPath(resolvedRepo);
342
+ const leaderAgent = project?.leaderAgent || slug;
343
+ config.leader = leaderAgent;
341
344
  config.spawner = process.env.GENIE_AGENT_NAME || 'cli';
342
345
  await teamManager.updateTeamConfig(config.name, config);
343
346
 
@@ -355,21 +358,21 @@ async function spawnLeaderWithWish(
355
358
  await copyFile(sourceWishPath, destWishPath);
356
359
  console.log(` Wish: copied ${slug}/WISH.md into worktree`);
357
360
 
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'];
361
+ // Hire the standard team: leader (by agent name) + engineer + reviewer + qa + fix
362
+ const standardTeam = [leaderAgent, 'engineer', 'reviewer', 'qa', 'fix'];
361
363
  for (const role of standardTeam) {
362
364
  await teamManager.hireAgent(config.name, role);
363
365
  }
364
366
  console.log(` Team: hired ${standardTeam.join(', ')}`);
365
367
 
366
- // Spawn leader — AGENTS.md comes from the built-in resolver, prompt delivered as initialPrompt
367
- const members = standardTeam.filter((r) => r !== leaderName).join(', ');
368
+ // Spawn leader — resolve agent definition from leaderAgent, use slug as role identity
369
+ const members = standardTeam.filter((r) => r !== leaderAgent).join(', ');
368
370
  const spawner = config.spawner || 'cli';
369
371
  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, {
372
+ await handleWorkerSpawn(leaderAgent, {
371
373
  provider: 'claude',
372
374
  team: config.name,
375
+ role: slug,
373
376
  cwd: config.worktreePath,
374
377
  session: tmuxSession,
375
378
  initialPrompt: kickoffPrompt,
@@ -377,11 +380,11 @@ async function spawnLeaderWithWish(
377
380
 
378
381
  // Deliver kickoff prompt via mailbox as backup (durable, queued to disk)
379
382
  const protocolRouter = await import('../lib/protocol-router.js');
380
- const result = await protocolRouter.sendMessage(config.worktreePath, 'cli', leaderName, kickoffPrompt);
383
+ const result = await protocolRouter.sendMessage(config.worktreePath, 'cli', leaderAgent, kickoffPrompt);
381
384
  if (!result.delivered) {
382
- console.warn(`⚠ Backup delivery to ${leaderName} failed: ${result.reason ?? 'unknown'}`);
385
+ console.warn(`⚠ Backup delivery to ${leaderAgent} failed: ${result.reason ?? 'unknown'}`);
383
386
  }
384
- console.log(' Leader: spawned and working');
387
+ console.log(` Leader: ${leaderAgent} spawned as ${slug}`);
385
388
  }
386
389
 
387
390
  // ============================================================================
@@ -1,282 +0,0 @@
1
- /**
2
- * Agent Cache — Maintains agent-directory.json as a cache of app_store agents.
3
- *
4
- * Also provides generic CRUD helpers for the `app_store` table (register,
5
- * remove, update, get, list). The cache file is a denormalised snapshot
6
- * written to GENIE_HOME so that non-PG consumers (hooks, shell scripts)
7
- * can read agent metadata without a database connection.
8
- *
9
- * Best-effort: DB failures never block the CLI.
10
- */
11
-
12
- import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
13
- import { homedir } from 'node:os';
14
- import { join } from 'node:path';
15
- import { getActor, recordAuditEvent } from './audit.js';
16
- import { getConnection, isAvailable } from './db.js';
17
-
18
- // ============================================================================
19
- // Constants
20
- // ============================================================================
21
-
22
- const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
23
- const CACHE_FILE = 'agent-directory.json';
24
- const CACHE_BACKUP = 'agent-directory.json.bak';
25
-
26
- // ============================================================================
27
- // Types
28
- // ============================================================================
29
-
30
- /** Denormalised cache entry written to agent-directory.json. */
31
- interface CacheEntry {
32
- name: string;
33
- dir: string;
34
- repo?: string;
35
- promptMode: string;
36
- model?: string;
37
- roles?: string[];
38
- registeredAt: string;
39
- }
40
-
41
- /** Row shape returned by SELECT * FROM app_store. */
42
- export interface StoreRow {
43
- id: string;
44
- name: string;
45
- item_type: string;
46
- version: string;
47
- description: string | null;
48
- author_name: string | null;
49
- author_url: string | null;
50
- git_url: string | null;
51
- install_path: string | null;
52
- manifest: Record<string, unknown>;
53
- approval_status: string;
54
- tags: string[];
55
- category: string | null;
56
- license: string | null;
57
- dependencies: string[];
58
- installed_at: string;
59
- updated_at: string;
60
- }
61
-
62
- /** Insert payload for registerItemInStore. */
63
- interface StoreInsert {
64
- name: string;
65
- itemType: string;
66
- version?: string;
67
- description?: string;
68
- authorName?: string;
69
- authorUrl?: string;
70
- gitUrl?: string;
71
- installPath?: string;
72
- manifest?: Record<string, unknown>;
73
- tags?: string[];
74
- category?: string;
75
- license?: string;
76
- dependencies?: string[];
77
- }
78
-
79
- // ============================================================================
80
- // Cache regeneration
81
- // ============================================================================
82
-
83
- /**
84
- * Rebuild agent-directory.json from all `item_type = 'agent'` rows in app_store.
85
- *
86
- * Silently returns if the database is unavailable — the stale cache (if any)
87
- * remains on disk until the next successful regeneration.
88
- */
89
- export async function regenerateAgentCache(): Promise<void> {
90
- try {
91
- if (!(await isAvailable())) return;
92
-
93
- const sql = await getConnection();
94
- const rows = await sql`
95
- SELECT name, install_path, manifest, installed_at
96
- FROM app_store
97
- WHERE item_type = 'agent'
98
- ORDER BY name
99
- `;
100
-
101
- const entries: CacheEntry[] = rows.map((r: Record<string, unknown>) => {
102
- const manifest = (r.manifest ?? {}) as Record<string, unknown>;
103
- const entry: CacheEntry = {
104
- name: r.name as string,
105
- dir: (r.install_path as string) ?? '',
106
- promptMode: (manifest.promptMode as string) ?? 'append',
107
- registeredAt: r.installed_at ? new Date(r.installed_at as string).toISOString() : new Date().toISOString(),
108
- };
109
- if (manifest.repo) entry.repo = manifest.repo as string;
110
- if (manifest.model) entry.model = manifest.model as string;
111
- if (Array.isArray(manifest.roles) && manifest.roles.length > 0) {
112
- entry.roles = manifest.roles as string[];
113
- }
114
- return entry;
115
- });
116
-
117
- writeFileSync(join(GENIE_HOME, CACHE_FILE), JSON.stringify(entries, null, 2));
118
- } catch {
119
- // Best effort — never block the CLI on cache regeneration failure
120
- }
121
- }
122
-
123
- // ============================================================================
124
- // One-time migration from JSON → PG
125
- // ============================================================================
126
-
127
- /**
128
- * Migrate the legacy agent-directory.json into the app_store table.
129
- *
130
- * Idempotent: skips if the backup file already exists (meaning migration
131
- * already ran) or if the source file is missing (nothing to migrate).
132
- * Uses ON CONFLICT (name) DO NOTHING so partially-completed runs are safe.
133
- */
134
- export async function migrateAgentDirectory(): Promise<void> {
135
- const sourcePath = join(GENIE_HOME, CACHE_FILE);
136
- const backupPath = join(GENIE_HOME, CACHE_BACKUP);
137
-
138
- // Already migrated or nothing to migrate
139
- if (existsSync(backupPath) || !existsSync(sourcePath)) return;
140
-
141
- try {
142
- const raw = readFileSync(sourcePath, 'utf-8');
143
- const entries: CacheEntry[] = JSON.parse(raw);
144
- if (!Array.isArray(entries) || entries.length === 0) return;
145
-
146
- const sql = await getConnection();
147
-
148
- for (const entry of entries) {
149
- const manifest: Record<string, unknown> = {};
150
- if (entry.promptMode) manifest.promptMode = entry.promptMode;
151
- if (entry.model) manifest.model = entry.model;
152
- if (entry.roles) manifest.roles = entry.roles;
153
- if (entry.repo) manifest.repo = entry.repo;
154
-
155
- await sql`
156
- INSERT INTO app_store (name, item_type, version, install_path, manifest)
157
- VALUES (${entry.name}, 'agent', '0.0.0', ${entry.dir ?? null}, ${sql.json(manifest)})
158
- ON CONFLICT (name) DO NOTHING
159
- `;
160
- }
161
-
162
- renameSync(sourcePath, backupPath);
163
-
164
- await recordAuditEvent('item', 'migration', 'agent_directory_migrated', getActor(), {
165
- count: entries.length,
166
- });
167
- } catch {
168
- // Best effort — migration can be retried on the next run
169
- }
170
- }
171
-
172
- // ============================================================================
173
- // CRUD helpers for app_store
174
- // ============================================================================
175
-
176
- /**
177
- * Insert a new item into the app_store. Returns the generated id.
178
- *
179
- * Throws if an item with the same name already exists — callers should
180
- * check beforehand or use `--force` to remove + re-insert.
181
- */
182
- export async function registerItemInStore(item: StoreInsert): Promise<string> {
183
- const sql = await getConnection();
184
-
185
- const rows = await sql`
186
- INSERT INTO app_store (
187
- name, item_type, version, description,
188
- author_name, author_url, git_url, install_path,
189
- manifest, tags, category, license, dependencies
190
- ) VALUES (
191
- ${item.name},
192
- ${item.itemType},
193
- ${item.version ?? '0.0.0'},
194
- ${item.description ?? null},
195
- ${item.authorName ?? null},
196
- ${item.authorUrl ?? null},
197
- ${item.gitUrl ?? null},
198
- ${item.installPath ?? null},
199
- ${sql.json(item.manifest ?? {})},
200
- ${item.tags ?? []},
201
- ${item.category ?? null},
202
- ${item.license ?? null},
203
- ${item.dependencies ?? []}
204
- )
205
- RETURNING id
206
- `;
207
-
208
- if (rows.length === 0) {
209
- throw new Error(`Failed to insert item "${item.name}" — no id returned.`);
210
- }
211
-
212
- return rows[0].id as string;
213
- }
214
-
215
- /**
216
- * Delete an item from the app_store by name.
217
- *
218
- * @returns true if an item was deleted, false if no matching item was found.
219
- */
220
- export async function removeItemFromStore(name: string): Promise<boolean> {
221
- const sql = await getConnection();
222
- const result = await sql`DELETE FROM app_store WHERE name = ${name}`;
223
- return result.count > 0;
224
- }
225
-
226
- /**
227
- * Update an existing item in the app_store by name.
228
- * Only provided fields are updated; updated_at is always set to now().
229
- *
230
- * @returns true if the item was updated, false if no matching item was found.
231
- */
232
- export async function updateItemInStore(name: string, updates: Partial<StoreInsert>): Promise<boolean> {
233
- const sql = await getConnection();
234
-
235
- const s: Record<string, unknown> = {};
236
- if (updates.name !== undefined) s.name = updates.name;
237
- if (updates.itemType !== undefined) s.item_type = updates.itemType;
238
- if (updates.version !== undefined) s.version = updates.version;
239
- if (updates.description !== undefined) s.description = updates.description;
240
- if (updates.authorName !== undefined) s.author_name = updates.authorName;
241
- if (updates.authorUrl !== undefined) s.author_url = updates.authorUrl;
242
- if (updates.gitUrl !== undefined) s.git_url = updates.gitUrl;
243
- if (updates.installPath !== undefined) s.install_path = updates.installPath;
244
- if (updates.manifest !== undefined) s.manifest = sql.json(updates.manifest);
245
- if (updates.tags !== undefined) s.tags = updates.tags;
246
- if (updates.category !== undefined) s.category = updates.category;
247
- if (updates.license !== undefined) s.license = updates.license;
248
- if (updates.dependencies !== undefined) s.dependencies = updates.dependencies;
249
-
250
- if (Object.keys(s).length === 0) return false;
251
-
252
- s.updated_at = sql`now()`;
253
- const result = await sql`UPDATE app_store SET ${sql(s)} WHERE name = ${name}`;
254
- return result.count > 0;
255
- }
256
-
257
- /**
258
- * Fetch a single item from the app_store by name.
259
- *
260
- * @returns the row or null if not found.
261
- */
262
- export async function getItemFromStore(name: string): Promise<StoreRow | null> {
263
- const sql = await getConnection();
264
- const rows = await sql`SELECT * FROM app_store WHERE name = ${name}`;
265
- return rows.length > 0 ? (rows[0] as unknown as StoreRow) : null;
266
- }
267
-
268
- /**
269
- * List items from the app_store, optionally filtered by item_type.
270
- *
271
- * @param itemType - When provided, only rows matching this type are returned.
272
- * @returns rows ordered by name.
273
- */
274
- export async function listItemsFromStore(itemType?: string): Promise<StoreRow[]> {
275
- const sql = await getConnection();
276
-
277
- const rows = itemType
278
- ? await sql`SELECT * FROM app_store WHERE item_type = ${itemType} ORDER BY name`
279
- : await sql`SELECT * FROM app_store ORDER BY name`;
280
-
281
- return rows as unknown as StoreRow[];
282
- }