hungry-ghost-hive 0.42.0 → 0.43.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.
Files changed (74) hide show
  1. package/dist/cli/commands/agents.d.ts +1 -0
  2. package/dist/cli/commands/agents.d.ts.map +1 -1
  3. package/dist/cli/commands/agents.js +72 -2
  4. package/dist/cli/commands/agents.js.map +1 -1
  5. package/dist/cli/commands/agents.test.js +106 -3
  6. package/dist/cli/commands/agents.test.js.map +1 -1
  7. package/dist/cli/commands/manager/auditor-lifecycle.d.ts +16 -0
  8. package/dist/cli/commands/manager/auditor-lifecycle.d.ts.map +1 -0
  9. package/dist/cli/commands/manager/auditor-lifecycle.js +88 -0
  10. package/dist/cli/commands/manager/auditor-lifecycle.js.map +1 -0
  11. package/dist/cli/commands/manager/auditor-lifecycle.test.d.ts +2 -0
  12. package/dist/cli/commands/manager/auditor-lifecycle.test.d.ts.map +1 -0
  13. package/dist/cli/commands/manager/auditor-lifecycle.test.js +169 -0
  14. package/dist/cli/commands/manager/auditor-lifecycle.test.js.map +1 -0
  15. package/dist/cli/commands/manager/auto-assignment.js +3 -3
  16. package/dist/cli/commands/manager/auto-assignment.js.map +1 -1
  17. package/dist/cli/commands/manager/auto-assignment.test.js +3 -3
  18. package/dist/cli/commands/manager/auto-assignment.test.js.map +1 -1
  19. package/dist/cli/commands/manager/auto-reject-comment-only-reviews.test.js +1 -0
  20. package/dist/cli/commands/manager/auto-reject-comment-only-reviews.test.js.map +1 -1
  21. package/dist/cli/commands/manager/feature-sign-off.test.js +1 -0
  22. package/dist/cli/commands/manager/feature-sign-off.test.js.map +1 -1
  23. package/dist/cli/commands/manager/index.d.ts.map +1 -1
  24. package/dist/cli/commands/manager/index.js +10 -2
  25. package/dist/cli/commands/manager/index.js.map +1 -1
  26. package/dist/cli/commands/manager/types.d.ts +1 -0
  27. package/dist/cli/commands/manager/types.d.ts.map +1 -1
  28. package/dist/config/schema.d.ts +168 -0
  29. package/dist/config/schema.d.ts.map +1 -1
  30. package/dist/config/schema.js +24 -0
  31. package/dist/config/schema.js.map +1 -1
  32. package/dist/context-files/index.test.js +11 -0
  33. package/dist/context-files/index.test.js.map +1 -1
  34. package/dist/db/client.d.ts +1 -1
  35. package/dist/db/client.d.ts.map +1 -1
  36. package/dist/db/client.js +29 -0
  37. package/dist/db/client.js.map +1 -1
  38. package/dist/db/migrations/014-auditor-agent-type.sql +3 -0
  39. package/dist/db/queries/agents.d.ts +2 -1
  40. package/dist/db/queries/agents.d.ts.map +1 -1
  41. package/dist/db/queries/agents.js +5 -0
  42. package/dist/db/queries/agents.js.map +1 -1
  43. package/dist/orchestrator/prompt-templates.d.ts +5 -0
  44. package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
  45. package/dist/orchestrator/prompt-templates.js +90 -0
  46. package/dist/orchestrator/prompt-templates.js.map +1 -1
  47. package/dist/orchestrator/prompt-templates.test.js +56 -1
  48. package/dist/orchestrator/prompt-templates.test.js.map +1 -1
  49. package/dist/orchestrator/scheduler.d.ts +6 -0
  50. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  51. package/dist/orchestrator/scheduler.js +17 -2
  52. package/dist/orchestrator/scheduler.js.map +1 -1
  53. package/dist/tmux/manager.js +10 -7
  54. package/dist/tmux/manager.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/cli/commands/agents.test.ts +131 -3
  57. package/src/cli/commands/agents.ts +93 -1
  58. package/src/cli/commands/manager/auditor-lifecycle.test.ts +219 -0
  59. package/src/cli/commands/manager/auditor-lifecycle.ts +109 -0
  60. package/src/cli/commands/manager/auto-assignment.test.ts +4 -4
  61. package/src/cli/commands/manager/auto-assignment.ts +3 -3
  62. package/src/cli/commands/manager/auto-reject-comment-only-reviews.test.ts +1 -0
  63. package/src/cli/commands/manager/feature-sign-off.test.ts +1 -0
  64. package/src/cli/commands/manager/index.ts +9 -1
  65. package/src/cli/commands/manager/types.ts +1 -0
  66. package/src/config/schema.ts +24 -0
  67. package/src/context-files/index.test.ts +11 -0
  68. package/src/db/client.ts +33 -1
  69. package/src/db/migrations/014-auditor-agent-type.sql +3 -0
  70. package/src/db/queries/agents.ts +14 -1
  71. package/src/orchestrator/prompt-templates.test.ts +77 -0
  72. package/src/orchestrator/prompt-templates.ts +95 -0
  73. package/src/orchestrator/scheduler.ts +19 -2
  74. package/src/tmux/manager.ts +10 -10
@@ -0,0 +1,109 @@
1
+ // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
+
3
+ import chalk from 'chalk';
4
+ import { getAgentsByType } from '../../../db/queries/agents.js';
5
+ import { createLog } from '../../../db/queries/logs.js';
6
+ import { getAllTeams } from '../../../db/queries/teams.js';
7
+ import type { ManagerCheckContext } from './types.js';
8
+
9
+ function verboseLogCtx(ctx: Pick<ManagerCheckContext, 'verbose'>, message: string): void {
10
+ if (!ctx.verbose) return;
11
+ console.log(chalk.gray(` [verbose] ${message}`));
12
+ }
13
+
14
+ // Module-level state tracking for auditor lifecycle
15
+ let lastAuditorSpawnTime = 0;
16
+
17
+ /** Reset auditor lifecycle state (for testing). */
18
+ export function resetAuditorLifecycleState(): void {
19
+ lastAuditorSpawnTime = 0;
20
+ }
21
+
22
+ /** Get the last auditor spawn timestamp (for testing). */
23
+ export function getLastAuditorSpawnTime(): number {
24
+ return lastAuditorSpawnTime;
25
+ }
26
+
27
+ /**
28
+ * Spawn an auditor agent if conditions are met:
29
+ * 1. auditor_enabled is true in config
30
+ * 2. Enough time has passed since last spawn (auditor_interval_ms)
31
+ * 3. No previous auditor is still running (active in agents table)
32
+ *
33
+ * Returns true if an auditor was spawned or skipped (auditor path handled),
34
+ * false if auditor is disabled (caller should fall back to nudge).
35
+ */
36
+ export async function spawnAuditorIfNeeded(ctx: ManagerCheckContext): Promise<boolean> {
37
+ const { config } = ctx;
38
+
39
+ if (!config.manager.auditor_enabled) {
40
+ verboseLogCtx(ctx, 'spawnAuditorIfNeeded: skip=auditor_disabled');
41
+ return false;
42
+ }
43
+
44
+ const now = Date.now();
45
+ const intervalMs = config.manager.auditor_interval_ms;
46
+ const elapsed = now - lastAuditorSpawnTime;
47
+
48
+ if (elapsed < intervalMs) {
49
+ verboseLogCtx(
50
+ ctx,
51
+ `spawnAuditorIfNeeded: skip=interval_not_reached elapsedMs=${elapsed} intervalMs=${intervalMs}`
52
+ );
53
+ return true;
54
+ }
55
+
56
+ // Check if a previous auditor is still running
57
+ const hasActiveAuditor = await ctx.withDb(async db => {
58
+ const auditors = getAgentsByType(db.db, 'auditor');
59
+ return auditors.some(a => a.status === 'idle' || a.status === 'working');
60
+ });
61
+
62
+ if (hasActiveAuditor) {
63
+ verboseLogCtx(ctx, 'spawnAuditorIfNeeded: skip=active_auditor_running');
64
+ return true;
65
+ }
66
+
67
+ // Spawn a new auditor agent
68
+ try {
69
+ const agent = await ctx.withDb(async (db, scheduler) => {
70
+ const teams = getAllTeams(db.db);
71
+ if (teams.length === 0) {
72
+ verboseLogCtx(ctx, 'spawnAuditorIfNeeded: skip=no_teams');
73
+ return null;
74
+ }
75
+
76
+ const team = teams[0];
77
+ const spawned = await scheduler.spawnAuditor(team.id, team.name, team.repo_path);
78
+
79
+ createLog(db.db, {
80
+ agentId: spawned.id,
81
+ eventType: 'AGENT_SPAWNED',
82
+ message: `Spawned auditor agent ${spawned.id} for team ${team.name}`,
83
+ metadata: {
84
+ agent_type: 'auditor',
85
+ team_id: team.id,
86
+ team_name: team.name,
87
+ },
88
+ });
89
+
90
+ db.save();
91
+ return spawned;
92
+ });
93
+
94
+ if (agent) {
95
+ lastAuditorSpawnTime = now;
96
+ ctx.counters.auditorsSpawned++;
97
+ console.log(chalk.green(` Auditor spawned: ${agent.id}`));
98
+ verboseLogCtx(ctx, `spawnAuditorIfNeeded: spawned agent=${agent.id}`);
99
+ }
100
+ } catch (err) {
101
+ console.error(chalk.red(' Auditor spawn failed:'), err instanceof Error ? err.message : err);
102
+ verboseLogCtx(
103
+ ctx,
104
+ `spawnAuditorIfNeeded: error=${err instanceof Error ? err.message : String(err)}`
105
+ );
106
+ }
107
+
108
+ return true;
109
+ }
@@ -15,7 +15,7 @@ describe('autoAssignPlannedStories', () => {
15
15
  });
16
16
 
17
17
  function makeCtx(options?: {
18
- plannedUnassigned?: number;
18
+ assignableUnassigned?: number;
19
19
  assigned?: number;
20
20
  errors?: string[];
21
21
  verbose?: boolean;
@@ -35,7 +35,7 @@ describe('autoAssignPlannedStories', () => {
35
35
  const withDb = vi.fn(async (fn: (db: typeof mockDb, scheduler: any) => unknown) => {
36
36
  callCount += 1;
37
37
  vi.mocked(queryAll).mockReturnValueOnce([
38
- { count: options?.plannedUnassigned ?? 0 },
38
+ { count: options?.assignableUnassigned ?? 0 },
39
39
  ] as never);
40
40
  return fn(mockDb, scheduler);
41
41
  });
@@ -61,7 +61,7 @@ describe('autoAssignPlannedStories', () => {
61
61
 
62
62
  it('skips assignment when there are no planned unassigned stories', async () => {
63
63
  const { ctx, checkScaling, checkMergeQueue, assignStories, withDb } = makeCtx({
64
- plannedUnassigned: 0,
64
+ assignableUnassigned: 0,
65
65
  });
66
66
 
67
67
  await autoAssignPlannedStories(ctx);
@@ -75,7 +75,7 @@ describe('autoAssignPlannedStories', () => {
75
75
 
76
76
  it('runs scaling, merge queue check, and assignment when planned stories exist', async () => {
77
77
  const { ctx, checkScaling, checkMergeQueue, assignStories, withDb } = makeCtx({
78
- plannedUnassigned: 2,
78
+ assignableUnassigned: 2,
79
79
  assigned: 1,
80
80
  });
81
81
 
@@ -20,10 +20,10 @@ async function getAssignableUnassignedStoryCount(ctx: ManagerCheckContext): Prom
20
20
  }
21
21
 
22
22
  export async function autoAssignPlannedStories(ctx: ManagerCheckContext): Promise<void> {
23
- const plannedUnassigned = await getAssignableUnassignedStoryCount(ctx);
24
- verboseLog(ctx, `autoAssignPlannedStories: assignableUnassigned=${plannedUnassigned}`);
23
+ const assignableUnassigned = await getAssignableUnassignedStoryCount(ctx);
24
+ verboseLog(ctx, `autoAssignPlannedStories: assignableUnassigned=${assignableUnassigned}`);
25
25
 
26
- if (plannedUnassigned === 0) {
26
+ if (assignableUnassigned === 0) {
27
27
  return;
28
28
  }
29
29
 
@@ -191,6 +191,7 @@ function makeMockCtx(overrides: Partial<ManagerCheckContext> = {}): ManagerCheck
191
191
  plannedAutoAssigned: 0,
192
192
  jiraSynced: 0,
193
193
  featureTestsSpawned: 0,
194
+ auditorsSpawned: 0,
194
195
  },
195
196
  escalatedSessions: new Set(),
196
197
  agentsBySessionName: new Map(),
@@ -87,6 +87,7 @@ function makeCtx(overrides: Partial<ManagerCheckContext> = {}): ManagerCheckCont
87
87
  plannedAutoAssigned: 0,
88
88
  jiraSynced: 0,
89
89
  featureTestsSpawned: 0,
90
+ auditorsSpawned: 0,
90
91
  },
91
92
  escalatedSessions: new Set(),
92
93
  agentsBySessionName: new Map(),
@@ -83,6 +83,7 @@ import {
83
83
  submitManagerNudgeWithVerification,
84
84
  updateAgentStateTracking,
85
85
  } from './agent-monitoring.js';
86
+ import { spawnAuditorIfNeeded } from './auditor-lifecycle.js';
86
87
  import { autoAssignPlannedStories } from './auto-assignment.js';
87
88
  import { assessCompletionFromOutput } from './done-intelligence.js';
88
89
  import { handleEscalationAndNudge } from './escalation-handler.js';
@@ -928,6 +929,7 @@ async function managerCheck(
928
929
  plannedAutoAssigned: 0,
929
930
  jiraSynced: 0,
930
931
  featureTestsSpawned: 0,
932
+ auditorsSpawned: 0,
931
933
  },
932
934
  escalatedSessions: new Set(),
933
935
  agentsBySessionName: new Map(),
@@ -1002,7 +1004,11 @@ async function managerCheck(
1002
1004
  verboseLogCtx(ctx, 'Step: spin down idle agents');
1003
1005
  await spinDownIdleAgents(ctx);
1004
1006
  verboseLogCtx(ctx, 'Step: evaluate stuck stories');
1005
- await nudgeStuckStories(ctx);
1007
+ const auditorHandled = await spawnAuditorIfNeeded(ctx);
1008
+ if (!auditorHandled) {
1009
+ // auditor_enabled is false — fall back to existing nudge behavior
1010
+ await nudgeStuckStories(ctx);
1011
+ }
1006
1012
  verboseLogCtx(ctx, 'Step: notify seniors about unassigned stories');
1007
1013
  await notifyUnassignedStories(ctx);
1008
1014
  await printSummary(ctx);
@@ -3355,6 +3361,7 @@ async function printSummary(ctx: ManagerCheckContext): Promise<void> {
3355
3361
  plannedAutoAssigned,
3356
3362
  jiraSynced,
3357
3363
  featureTestsSpawned,
3364
+ auditorsSpawned,
3358
3365
  } = ctx.counters;
3359
3366
  const summary = [];
3360
3367
 
@@ -3381,6 +3388,7 @@ async function printSummary(ctx: ManagerCheckContext): Promise<void> {
3381
3388
  summary.push(`${plannedAutoAssigned} planned story(ies) auto-assigned`);
3382
3389
  if (jiraSynced > 0) summary.push(`${jiraSynced} synced from Jira`);
3383
3390
  if (featureTestsSpawned > 0) summary.push(`${featureTestsSpawned} feature test(s) spawned`);
3391
+ if (auditorsSpawned > 0) summary.push(`${auditorsSpawned} auditor(s) spawned`);
3384
3392
 
3385
3393
  if (summary.length > 0) {
3386
3394
  console.log(chalk.yellow(` ${summary.join(', ')}`));
@@ -82,6 +82,7 @@ export interface ManagerCheckContext {
82
82
  plannedAutoAssigned: number;
83
83
  jiraSynced: number;
84
84
  featureTestsSpawned: number;
85
+ auditorsSpawned: number;
85
86
  };
86
87
  // Shared state for dedup
87
88
  escalatedSessions: Set<string | null>;
@@ -69,6 +69,14 @@ const ModelsConfigSchema = z.object({
69
69
  cli_tool: 'claude',
70
70
  safety_mode: 'unsafe',
71
71
  }),
72
+ auditor: ModelConfigSchema.default({
73
+ provider: 'anthropic',
74
+ model: 'claude-opus-4-6',
75
+ max_tokens: 16000,
76
+ temperature: 0.3,
77
+ cli_tool: 'claude',
78
+ safety_mode: 'unsafe',
79
+ }),
72
80
  });
73
81
 
74
82
  // Scaling rules
@@ -233,6 +241,10 @@ const ManagerConfigSchema = z.object({
233
241
  tmux_timeout_ms: z.number().int().positive().default(10000), // 10s for tmux commands
234
242
  // Tech lead session max age in hours before graceful restart for context freshness
235
243
  tech_lead_max_age_hours: z.number().positive().default(6),
244
+ // How often to spawn an auditor agent (ms, default 5 minutes)
245
+ auditor_interval_ms: z.number().int().positive().default(300000),
246
+ // Whether auditor agent is enabled (false falls back to nudge behavior)
247
+ auditor_enabled: z.boolean().default(true),
236
248
  });
237
249
 
238
250
  // Merge queue configuration
@@ -446,6 +458,14 @@ models:
446
458
  # Agent personas can be configured during 'hive init'.
447
459
  # Re-run 'hive init --force' to reconfigure.
448
460
 
461
+ auditor:
462
+ provider: anthropic
463
+ model: claude-opus-4-6
464
+ max_tokens: 16000
465
+ temperature: 0.3
466
+ cli_tool: claude
467
+ safety_mode: unsafe
468
+
449
469
  # Team scaling rules
450
470
  scaling:
451
471
  # Story points threshold before hiring additional senior
@@ -529,6 +549,10 @@ manager:
529
549
  gh_timeout_ms: 60000
530
550
  # Timeout for tmux operations to prevent manager hangs (ms)
531
551
  tmux_timeout_ms: 10000
552
+ # How often to spawn an auditor agent (ms, default 5 minutes)
553
+ auditor_interval_ms: 300000
554
+ # Whether auditor agent is enabled (false falls back to nudge behavior)
555
+ auditor_enabled: true
532
556
 
533
557
  # Merge queue configuration
534
558
  merge_queue:
@@ -169,6 +169,15 @@ describe('context-files module', () => {
169
169
  safety_mode: 'unsafe',
170
170
  personas: [],
171
171
  },
172
+ auditor: {
173
+ provider: 'anthropic',
174
+ model: 'claude-opus-4-6',
175
+ max_tokens: 16000,
176
+ temperature: 0.3,
177
+ cli_tool: 'claude',
178
+ safety_mode: 'unsafe',
179
+ personas: [],
180
+ },
172
181
  },
173
182
  scaling: {
174
183
  senior_capacity: 20,
@@ -214,6 +223,8 @@ describe('context-files module', () => {
214
223
  gh_timeout_ms: 60000,
215
224
  tmux_timeout_ms: 10000,
216
225
  tech_lead_max_age_hours: 6,
226
+ auditor_interval_ms: 300000,
227
+ auditor_enabled: true,
217
228
  },
218
229
  logging: { level: 'info', retention_days: 30 },
219
230
  cluster: {
package/src/db/client.ts CHANGED
@@ -519,6 +519,38 @@ const MIGRATIONS: MigrationDefinition[] = [
519
519
  db.run('PRAGMA foreign_keys = ON');
520
520
  },
521
521
  },
522
+ {
523
+ name: '014-auditor-agent-type.sql',
524
+ up: db => {
525
+ // Recreate agents table with updated type CHECK constraint (add 'auditor')
526
+ db.run('PRAGMA foreign_keys = OFF');
527
+
528
+ db.run(`
529
+ CREATE TABLE agents_new (
530
+ id TEXT PRIMARY KEY,
531
+ type TEXT NOT NULL CHECK (type IN ('tech_lead', 'senior', 'intermediate', 'junior', 'qa', 'feature_test', 'auditor')),
532
+ team_id TEXT REFERENCES teams(id),
533
+ tmux_session TEXT,
534
+ model TEXT,
535
+ status TEXT DEFAULT 'idle' CHECK (status IN ('idle', 'working', 'blocked', 'terminated')),
536
+ current_story_id TEXT,
537
+ memory_state TEXT,
538
+ last_seen TIMESTAMP,
539
+ worktree_path TEXT,
540
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
541
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
542
+ )
543
+ `);
544
+ db.run('INSERT INTO agents_new SELECT * FROM agents');
545
+ db.run('DROP TABLE agents');
546
+ db.run('ALTER TABLE agents_new RENAME TO agents');
547
+
548
+ db.run('CREATE INDEX IF NOT EXISTS idx_agents_team_id ON agents(team_id)');
549
+ db.run('CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)');
550
+
551
+ db.run('PRAGMA foreign_keys = ON');
552
+ },
553
+ },
522
554
  {
523
555
  name: '007-backfill-story-points.sql',
524
556
  up: db => {
@@ -721,7 +753,7 @@ export interface TeamRow {
721
753
 
722
754
  export interface AgentRow {
723
755
  id: string;
724
- type: 'tech_lead' | 'senior' | 'intermediate' | 'junior' | 'qa' | 'feature_test';
756
+ type: 'tech_lead' | 'senior' | 'intermediate' | 'junior' | 'qa' | 'feature_test' | 'auditor';
725
757
  team_id: string | null;
726
758
  tmux_session: string | null;
727
759
  model: string | null;
@@ -0,0 +1,3 @@
1
+ -- Migration 014: Add auditor agent type
2
+ -- The CHECK constraint update is handled programmatically in client.ts
3
+ -- (requires table recreation for SQLite CHECK constraint changes)
@@ -6,7 +6,14 @@ import { queryAll, queryOne, run, type AgentRow } from '../client.js';
6
6
 
7
7
  export type { AgentRow };
8
8
 
9
- export type AgentType = 'tech_lead' | 'senior' | 'intermediate' | 'junior' | 'qa' | 'feature_test';
9
+ export type AgentType =
10
+ | 'tech_lead'
11
+ | 'senior'
12
+ | 'intermediate'
13
+ | 'junior'
14
+ | 'qa'
15
+ | 'feature_test'
16
+ | 'auditor';
10
17
  export type AgentStatus = 'idle' | 'working' | 'blocked' | 'terminated';
11
18
 
12
19
  export interface CreateAgentInput {
@@ -134,6 +141,12 @@ export function deleteAgent(db: Database, id: string): void {
134
141
  run(db, 'DELETE FROM agents WHERE id = ?', [id]);
135
142
  }
136
143
 
144
+ export function getAgentByTmuxSession(db: Database, tmuxSession: string): AgentRow | undefined {
145
+ return queryOne<AgentRow>(db, 'SELECT * FROM agents WHERE tmux_session = ? LIMIT 1', [
146
+ tmuxSession,
147
+ ]);
148
+ }
149
+
137
150
  export function terminateAgent(db: Database, id: string): void {
138
151
  updateAgent(db, id, { status: 'terminated', tmuxSession: null });
139
152
  }
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
4
4
  import type { StoryRow } from '../db/client.js';
5
5
  import {
6
6
  formatSeniorSessionName,
7
+ generateAuditorPrompt,
7
8
  generateFeatureTestPrompt,
8
9
  generateIntermediatePrompt,
9
10
  generateJuniorPrompt,
@@ -645,6 +646,82 @@ describe('Prompt Templates', () => {
645
646
  });
646
647
  });
647
648
 
649
+ describe('generateAuditorPrompt', () => {
650
+ const sessionName = 'hive-auditor-1234567890';
651
+
652
+ it('should generate prompt with correct agent role', () => {
653
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
654
+
655
+ expect(prompt).toContain('You are a Hive Auditor Agent');
656
+ });
657
+
658
+ it('should include session name', () => {
659
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
660
+
661
+ expect(prompt).toContain(`Your tmux session: ${sessionName}`);
662
+ });
663
+
664
+ it('should include repository information', () => {
665
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
666
+
667
+ expect(prompt).toContain(`Local path: ${repoPath}`);
668
+ expect(prompt).toContain(`Remote: ${repoUrl}`);
669
+ });
670
+
671
+ it('should include hive CLI commands reference', () => {
672
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
673
+
674
+ expect(prompt).toContain('hive status');
675
+ expect(prompt).toContain('hive agents list --active');
676
+ expect(prompt).toContain('hive agents inspect');
677
+ expect(prompt).toContain('hive stories list');
678
+ expect(prompt).toContain('hive pr queue');
679
+ expect(prompt).toContain('hive msg send');
680
+ expect(prompt).toContain('hive agent self-terminate');
681
+ });
682
+
683
+ it('should include orphaned story detection instructions', () => {
684
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
685
+
686
+ expect(prompt).toContain('Orphaned stories');
687
+ expect(prompt).toContain('hive stories update');
688
+ expect(prompt).toContain('--status planned');
689
+ });
690
+
691
+ it('should include stuck agent detection instructions', () => {
692
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
693
+
694
+ expect(prompt).toContain('Plan mode');
695
+ expect(prompt).toContain('Permission prompts');
696
+ expect(prompt).toContain('BTab');
697
+ });
698
+
699
+ it('should include escalation instructions', () => {
700
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
701
+
702
+ expect(prompt).toContain('hive msg send hive-tech-lead');
703
+ expect(prompt).toContain(`--from ${sessionName}`);
704
+ });
705
+
706
+ it('should include self-terminate instruction', () => {
707
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
708
+
709
+ expect(prompt).toContain('hive agent self-terminate');
710
+ });
711
+
712
+ it('should prohibit nudging agents via arbitrary tmux send-keys', () => {
713
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
714
+
715
+ expect(prompt).toContain('Do NOT nudge or interrupt agents');
716
+ });
717
+
718
+ it('should include tmux capture-pane instructions', () => {
719
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
720
+
721
+ expect(prompt).toContain('tmux capture-pane');
722
+ });
723
+ });
724
+
648
725
  describe('Target Branch Propagation', () => {
649
726
  describe('generateSeniorPrompt with custom target branch', () => {
650
727
  it('should include custom target_branch in merge conflict check instructions', () => {
@@ -648,3 +648,98 @@ hive msg outbox ${sessionName}
648
648
 
649
649
  Start by checking out the feature branch and reading the TESTING.md file.`;
650
650
  }
651
+
652
+ /**
653
+ * Generate prompt for Auditor agent.
654
+ * The auditor is an ephemeral Opus agent that performs an intelligent audit of all active work.
655
+ */
656
+ export function generateAuditorPrompt(
657
+ sessionName: string,
658
+ repoPath: string,
659
+ repoUrl: string
660
+ ): string {
661
+ return `You are a Hive Auditor Agent.
662
+ Your tmux session: ${sessionName}
663
+
664
+ ${repositorySection(repoPath, repoUrl)}
665
+
666
+ ## Your Mission
667
+ Perform a rapid, intelligent audit of all active work in this Hive workspace.
668
+ You are an ephemeral agent — complete your audit quickly and self-terminate.
669
+
670
+ ## Available Hive CLI Commands
671
+ \`\`\`
672
+ hive status # Overview of all stories, agents, PRs
673
+ hive agents list --active # List active agents with tmux sessions
674
+ hive agents inspect <agent-id> # Detailed agent state
675
+ hive stories list # List all stories
676
+ hive stories list --status <s> # Filter stories by status (planned, in_progress, review, qa, merged)
677
+ hive pr queue # View pending PRs in the merge queue
678
+ hive msg send <session> "<msg>" --from ${sessionName} # Send a message to another agent
679
+ hive agent self-terminate # Terminate yourself when done
680
+ \`\`\`
681
+
682
+ ## Audit Workflow
683
+
684
+ ### 1. Get workspace overview
685
+ \`\`\`bash
686
+ hive status
687
+ \`\`\`
688
+ Review the overall state: stories, agents, PRs.
689
+
690
+ ### 2. Check active agents
691
+ \`\`\`bash
692
+ hive agents list --active --json
693
+ \`\`\`
694
+ For each active agent, note their tmux session and current story.
695
+
696
+ ### 3. Verify tmux sessions are alive
697
+ For each active agent's tmux session, capture the last few lines to check their state:
698
+ \`\`\`bash
699
+ tmux capture-pane -t <session-name> -p -S -30
700
+ \`\`\`
701
+
702
+ ### 4. Detect issues
703
+
704
+ **Orphaned stories:** Stories with status \`in_progress\` but their assigned agent is terminated or has no live tmux session.
705
+ - Fix: Reset the story status to \`planned\` so the scheduler can reassign it:
706
+ \`\`\`bash
707
+ hive stories update <story-id> --status planned
708
+ \`\`\`
709
+
710
+ **Stuck agents — Plan mode:** Agent output shows it is in plan mode (e.g., "Plan Mode" prompt, waiting for plan approval).
711
+ - Fix: Send BTab to exit plan mode:
712
+ \`\`\`bash
713
+ tmux send-keys -t <session-name> BTab
714
+ \`\`\`
715
+
716
+ **Stuck agents — Permission prompts:** Agent output shows a permission/approval prompt (e.g., "Allow?", "Yes/No", tool approval dialogs).
717
+ - Fix: Send Enter or 'y' to approve:
718
+ \`\`\`bash
719
+ tmux send-keys -t <session-name> Enter
720
+ \`\`\`
721
+
722
+ **Stuck agents — At idle prompt for extended time:** Agent appears to have finished but hasn't signaled completion.
723
+ - Escalate: Cannot fix directly — notify tech lead.
724
+
725
+ **Other unfixable issues:** Any issue you cannot resolve with the above actions.
726
+ - Escalate to tech lead:
727
+ \`\`\`bash
728
+ hive msg send hive-tech-lead "AUDITOR: <description of issue, including agent id and story id>" --from ${sessionName}
729
+ \`\`\`
730
+
731
+ ### 5. Self-terminate
732
+ After completing your audit:
733
+ \`\`\`bash
734
+ hive agent self-terminate
735
+ \`\`\`
736
+
737
+ ## IMPORTANT Rules
738
+ - Do NOT nudge or interrupt agents by sending arbitrary text via tmux send-keys. Only use tmux send-keys for the specific fixes above (BTab for plan mode, Enter for permission prompts).
739
+ - Use hive CLI commands for all actions — do not modify the database directly.
740
+ - Be concise and efficient — this audit runs every few minutes.
741
+ - Do NOT create branches, PRs, or modify any code.
742
+ - If in doubt about an issue, escalate to the tech lead rather than taking action.
743
+
744
+ Start by running \`hive status\` to get the workspace overview.`;
745
+ }
@@ -55,6 +55,7 @@ import {
55
55
  } from './feature-branch.js';
56
56
  import { detectAndRecoverOrphanedStories } from './orphan-recovery.js';
57
57
  import {
58
+ generateAuditorPrompt,
58
59
  generateFeatureTestPrompt,
59
60
  generateIntermediatePrompt,
60
61
  generateJuniorPrompt,
@@ -236,6 +237,7 @@ export class Scheduler {
236
237
  const agents = getAgentsByTeam(this.db, teamId).filter(
237
238
  a =>
238
239
  a.type !== 'qa' &&
240
+ a.type !== 'auditor' &&
239
241
  (a.status === 'idle' || (a.status === 'working' && a.current_story_id === null))
240
242
  );
241
243
  const activeSeniors = getAgentsByTeam(this.db, teamId).filter(
@@ -893,7 +895,7 @@ export class Scheduler {
893
895
  * Handles spawning of all agent types (senior, intermediate, junior, qa)
894
896
  */
895
897
  private async spawnAgent(
896
- type: 'senior' | 'intermediate' | 'junior' | 'qa' | 'feature_test',
898
+ type: 'senior' | 'intermediate' | 'junior' | 'qa' | 'feature_test' | 'auditor',
897
899
  teamId: string,
898
900
  teamName: string,
899
901
  repoPath: string,
@@ -904,7 +906,11 @@ export class Scheduler {
904
906
  e2eTestsPath: string;
905
907
  }
906
908
  ): Promise<AgentRow> {
907
- const sessionName = generateSessionName(type, teamName, index);
909
+ // Auditor uses a timestamp-based session name since it's ephemeral
910
+ const sessionName =
911
+ type === 'auditor'
912
+ ? `hive-auditor-${Date.now()}`
913
+ : generateSessionName(type, teamName, index);
908
914
 
909
915
  // Prevent creating duplicate agents on same tmux session (for senior agents)
910
916
  if (type === 'senior') {
@@ -1033,6 +1039,8 @@ export class Scheduler {
1033
1039
  featureTestContext.e2eTestsPath,
1034
1040
  { includeProgressUpdates }
1035
1041
  );
1042
+ } else if (type === 'auditor') {
1043
+ prompt = generateAuditorPrompt(sessionName, worktreePath, team?.repo_url || '');
1036
1044
  } else {
1037
1045
  prompt = generateQAPrompt(
1038
1046
  teamName,
@@ -1135,6 +1143,15 @@ export class Scheduler {
1135
1143
  );
1136
1144
  }
1137
1145
 
1146
+ /**
1147
+ * Spawn an auditor agent for intelligent audit of all active work.
1148
+ * The auditor is ephemeral — it audits the workspace and self-terminates.
1149
+ * Uses a shared worktree from main branch (read-only, does not commit).
1150
+ */
1151
+ async spawnAuditor(teamId: string, teamName: string, repoPath: string): Promise<AgentRow> {
1152
+ return this.spawnAgent('auditor', teamId, teamName, repoPath);
1153
+ }
1154
+
1138
1155
  private async spawnSenior(
1139
1156
  teamId: string,
1140
1157
  teamName: string,
@@ -269,16 +269,16 @@ export async function sendToTmuxSession(
269
269
  const isMultiLine = text.includes('\n');
270
270
 
271
271
  if (isMultiLine) {
272
- // Multi-line text: write to temp file and paste via $(cat ...) to avoid
273
- // the [Pasted text #N] buffering issue in Claude CLI. The shell expands
274
- // $(cat file) into a single argument, bypassing tmux's paste-buffer handling.
275
- const tempFile = join(
276
- tmpdir(),
277
- `hive-nudge-${Date.now()}-${sessionName.replace(/[^a-zA-Z0-9-]/g, '_')}.txt`
278
- );
279
- writeFileSync(tempFile, text, 'utf-8');
280
- const catCmd = `$(cat ${tempFile})`;
281
- await execa('tmux', ['send-keys', '-t', sessionName, '-l', '--', catCmd]);
272
+ // Multi-line text: use tmux set-buffer + paste-buffer to deliver the full
273
+ // text as a single paste event. This works because:
274
+ // 1. send-keys -l sends characters literally to the app (Claude CLI), NOT
275
+ // to a shell, so $(cat ...) is never expanded — it appears verbatim.
276
+ // 2. paste-buffer uses bracketed paste mode, which tells Claude CLI that
277
+ // the incoming text is pasted content, so newlines don't trigger submit.
278
+ const bufferName = `hive-nudge-${Date.now()}`;
279
+ await execa('tmux', ['set-buffer', '-b', bufferName, text]);
280
+ // -d deletes the buffer immediately after pasting (no cleanup needed)
281
+ await execa('tmux', ['paste-buffer', '-b', bufferName, '-t', sessionName, '-d']);
282
282
  } else {
283
283
  // Single-line: use send-keys with literal flag directly.
284
284
  // '--' signals end of options, preventing text starting with '-' from being parsed as flags.