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.
- package/dist/cli/commands/agents.d.ts +1 -0
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +72 -2
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/agents.test.js +106 -3
- package/dist/cli/commands/agents.test.js.map +1 -1
- package/dist/cli/commands/manager/auditor-lifecycle.d.ts +16 -0
- package/dist/cli/commands/manager/auditor-lifecycle.d.ts.map +1 -0
- package/dist/cli/commands/manager/auditor-lifecycle.js +88 -0
- package/dist/cli/commands/manager/auditor-lifecycle.js.map +1 -0
- package/dist/cli/commands/manager/auditor-lifecycle.test.d.ts +2 -0
- package/dist/cli/commands/manager/auditor-lifecycle.test.d.ts.map +1 -0
- package/dist/cli/commands/manager/auditor-lifecycle.test.js +169 -0
- package/dist/cli/commands/manager/auditor-lifecycle.test.js.map +1 -0
- package/dist/cli/commands/manager/auto-assignment.js +3 -3
- package/dist/cli/commands/manager/auto-assignment.js.map +1 -1
- package/dist/cli/commands/manager/auto-assignment.test.js +3 -3
- package/dist/cli/commands/manager/auto-assignment.test.js.map +1 -1
- package/dist/cli/commands/manager/auto-reject-comment-only-reviews.test.js +1 -0
- package/dist/cli/commands/manager/auto-reject-comment-only-reviews.test.js.map +1 -1
- package/dist/cli/commands/manager/feature-sign-off.test.js +1 -0
- package/dist/cli/commands/manager/feature-sign-off.test.js.map +1 -1
- package/dist/cli/commands/manager/index.d.ts.map +1 -1
- package/dist/cli/commands/manager/index.js +10 -2
- package/dist/cli/commands/manager/index.js.map +1 -1
- package/dist/cli/commands/manager/types.d.ts +1 -0
- package/dist/cli/commands/manager/types.d.ts.map +1 -1
- package/dist/config/schema.d.ts +168 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +24 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/context-files/index.test.js +11 -0
- package/dist/context-files/index.test.js.map +1 -1
- package/dist/db/client.d.ts +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +29 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/migrations/014-auditor-agent-type.sql +3 -0
- package/dist/db/queries/agents.d.ts +2 -1
- package/dist/db/queries/agents.d.ts.map +1 -1
- package/dist/db/queries/agents.js +5 -0
- package/dist/db/queries/agents.js.map +1 -1
- package/dist/orchestrator/prompt-templates.d.ts +5 -0
- package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
- package/dist/orchestrator/prompt-templates.js +90 -0
- package/dist/orchestrator/prompt-templates.js.map +1 -1
- package/dist/orchestrator/prompt-templates.test.js +56 -1
- package/dist/orchestrator/prompt-templates.test.js.map +1 -1
- package/dist/orchestrator/scheduler.d.ts +6 -0
- package/dist/orchestrator/scheduler.d.ts.map +1 -1
- package/dist/orchestrator/scheduler.js +17 -2
- package/dist/orchestrator/scheduler.js.map +1 -1
- package/dist/tmux/manager.js +10 -7
- package/dist/tmux/manager.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/agents.test.ts +131 -3
- package/src/cli/commands/agents.ts +93 -1
- package/src/cli/commands/manager/auditor-lifecycle.test.ts +219 -0
- package/src/cli/commands/manager/auditor-lifecycle.ts +109 -0
- package/src/cli/commands/manager/auto-assignment.test.ts +4 -4
- package/src/cli/commands/manager/auto-assignment.ts +3 -3
- package/src/cli/commands/manager/auto-reject-comment-only-reviews.test.ts +1 -0
- package/src/cli/commands/manager/feature-sign-off.test.ts +1 -0
- package/src/cli/commands/manager/index.ts +9 -1
- package/src/cli/commands/manager/types.ts +1 -0
- package/src/config/schema.ts +24 -0
- package/src/context-files/index.test.ts +11 -0
- package/src/db/client.ts +33 -1
- package/src/db/migrations/014-auditor-agent-type.sql +3 -0
- package/src/db/queries/agents.ts +14 -1
- package/src/orchestrator/prompt-templates.test.ts +77 -0
- package/src/orchestrator/prompt-templates.ts +95 -0
- package/src/orchestrator/scheduler.ts +19 -2
- 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
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
24
|
-
verboseLog(ctx, `autoAssignPlannedStories: assignableUnassigned=${
|
|
23
|
+
const assignableUnassigned = await getAssignableUnassignedStoryCount(ctx);
|
|
24
|
+
verboseLog(ctx, `autoAssignPlannedStories: assignableUnassigned=${assignableUnassigned}`);
|
|
25
25
|
|
|
26
|
-
if (
|
|
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(),
|
|
@@ -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
|
|
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(', ')}`));
|
package/src/config/schema.ts
CHANGED
|
@@ -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;
|
package/src/db/queries/agents.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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,
|
package/src/tmux/manager.ts
CHANGED
|
@@ -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:
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
await execa('tmux', ['
|
|
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.
|