hungry-ghost-hive 0.44.0 → 0.45.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 (148) hide show
  1. package/dist/agents/base-agent.d.ts +1 -0
  2. package/dist/agents/base-agent.d.ts.map +1 -1
  3. package/dist/agents/base-agent.js +4 -0
  4. package/dist/agents/base-agent.js.map +1 -1
  5. package/dist/agents/intermediate.js +2 -2
  6. package/dist/agents/intermediate.js.map +1 -1
  7. package/dist/agents/junior.js +2 -2
  8. package/dist/agents/junior.js.map +1 -1
  9. package/dist/agents/qa.d.ts.map +1 -1
  10. package/dist/agents/qa.js +5 -5
  11. package/dist/agents/qa.js.map +1 -1
  12. package/dist/agents/senior.d.ts.map +1 -1
  13. package/dist/agents/senior.js +5 -5
  14. package/dist/agents/senior.js.map +1 -1
  15. package/dist/agents/tech-lead.d.ts.map +1 -1
  16. package/dist/agents/tech-lead.js +4 -2
  17. package/dist/agents/tech-lead.js.map +1 -1
  18. package/dist/cli/commands/assign.d.ts.map +1 -1
  19. package/dist/cli/commands/assign.js +4 -2
  20. package/dist/cli/commands/assign.js.map +1 -1
  21. package/dist/cli/commands/assign.test.js +5 -0
  22. package/dist/cli/commands/assign.test.js.map +1 -1
  23. package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
  24. package/dist/cli/commands/manager/handoff-recovery.js +4 -2
  25. package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
  26. package/dist/cli/commands/manager/index.d.ts.map +1 -1
  27. package/dist/cli/commands/manager/index.js +16 -12
  28. package/dist/cli/commands/manager/index.js.map +1 -1
  29. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
  30. package/dist/cli/commands/manager/tech-lead-lifecycle.js +4 -2
  31. package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
  32. package/dist/cli/commands/msg.d.ts.map +1 -1
  33. package/dist/cli/commands/msg.js +8 -7
  34. package/dist/cli/commands/msg.js.map +1 -1
  35. package/dist/cli/commands/my-stories.js +3 -3
  36. package/dist/cli/commands/my-stories.js.map +1 -1
  37. package/dist/cli/commands/nuke.d.ts.map +1 -1
  38. package/dist/cli/commands/nuke.js +18 -7
  39. package/dist/cli/commands/nuke.js.map +1 -1
  40. package/dist/cli/commands/nuke.test.js +24 -0
  41. package/dist/cli/commands/nuke.test.js.map +1 -1
  42. package/dist/cli/commands/req.d.ts +1 -1
  43. package/dist/cli/commands/req.d.ts.map +1 -1
  44. package/dist/cli/commands/req.js +7 -5
  45. package/dist/cli/commands/req.js.map +1 -1
  46. package/dist/cli/commands/stories.js +3 -3
  47. package/dist/cli/commands/stories.js.map +1 -1
  48. package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
  49. package/dist/cli/dashboard/panels/agents.js +7 -3
  50. package/dist/cli/dashboard/panels/agents.js.map +1 -1
  51. package/dist/context-files/generator.d.ts +1 -1
  52. package/dist/context-files/generator.d.ts.map +1 -1
  53. package/dist/context-files/generator.js +3 -2
  54. package/dist/context-files/generator.js.map +1 -1
  55. package/dist/context-files/index.test.js +1 -0
  56. package/dist/context-files/index.test.js.map +1 -1
  57. package/dist/db/client.d.ts +1 -0
  58. package/dist/db/client.d.ts.map +1 -1
  59. package/dist/db/client.js +6 -0
  60. package/dist/db/client.js.map +1 -1
  61. package/dist/db/migrations/015-add-story-markdown-path.sql +5 -0
  62. package/dist/db/queries/stories.d.ts +3 -3
  63. package/dist/db/queries/stories.d.ts.map +1 -1
  64. package/dist/db/queries/stories.js +23 -5
  65. package/dist/db/queries/stories.js.map +1 -1
  66. package/dist/db/queries/test-helpers.d.ts.map +1 -1
  67. package/dist/db/queries/test-helpers.js +1 -0
  68. package/dist/db/queries/test-helpers.js.map +1 -1
  69. package/dist/git/worktree.d.ts.map +1 -1
  70. package/dist/git/worktree.js +7 -0
  71. package/dist/git/worktree.js.map +1 -1
  72. package/dist/git/worktree.test.js +30 -0
  73. package/dist/git/worktree.test.js.map +1 -1
  74. package/dist/orchestrator/prompt-templates.d.ts +3 -1
  75. package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
  76. package/dist/orchestrator/prompt-templates.js +16 -8
  77. package/dist/orchestrator/prompt-templates.js.map +1 -1
  78. package/dist/orchestrator/prompt-templates.test.js +4 -0
  79. package/dist/orchestrator/prompt-templates.test.js.map +1 -1
  80. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  81. package/dist/orchestrator/scheduler.js +19 -11
  82. package/dist/orchestrator/scheduler.js.map +1 -1
  83. package/dist/orchestrator/scheduler.test.js +1 -0
  84. package/dist/orchestrator/scheduler.test.js.map +1 -1
  85. package/dist/tmux/manager.d.ts +7 -6
  86. package/dist/tmux/manager.d.ts.map +1 -1
  87. package/dist/tmux/manager.js +29 -13
  88. package/dist/tmux/manager.js.map +1 -1
  89. package/dist/utils/instance.d.ts +32 -0
  90. package/dist/utils/instance.d.ts.map +1 -0
  91. package/dist/utils/instance.js +82 -0
  92. package/dist/utils/instance.js.map +1 -0
  93. package/dist/utils/instance.test.d.ts +2 -0
  94. package/dist/utils/instance.test.d.ts.map +1 -0
  95. package/dist/utils/instance.test.js +103 -0
  96. package/dist/utils/instance.test.js.map +1 -0
  97. package/dist/utils/paths.d.ts +2 -0
  98. package/dist/utils/paths.d.ts.map +1 -1
  99. package/dist/utils/paths.js +2 -0
  100. package/dist/utils/paths.js.map +1 -1
  101. package/dist/utils/paths.test.js +6 -0
  102. package/dist/utils/paths.test.js.map +1 -1
  103. package/dist/utils/story-markdown.d.ts +16 -0
  104. package/dist/utils/story-markdown.d.ts.map +1 -0
  105. package/dist/utils/story-markdown.js +82 -0
  106. package/dist/utils/story-markdown.js.map +1 -0
  107. package/dist/utils/story-markdown.test.d.ts +2 -0
  108. package/dist/utils/story-markdown.test.d.ts.map +1 -0
  109. package/dist/utils/story-markdown.test.js +143 -0
  110. package/dist/utils/story-markdown.test.js.map +1 -0
  111. package/package.json +1 -1
  112. package/src/agents/base-agent.ts +5 -0
  113. package/src/agents/intermediate.ts +2 -2
  114. package/src/agents/junior.ts +2 -2
  115. package/src/agents/qa.ts +13 -8
  116. package/src/agents/senior.ts +21 -11
  117. package/src/agents/tech-lead.ts +24 -12
  118. package/src/cli/commands/assign.test.ts +5 -0
  119. package/src/cli/commands/assign.ts +4 -2
  120. package/src/cli/commands/manager/handoff-recovery.ts +4 -2
  121. package/src/cli/commands/manager/index.ts +16 -11
  122. package/src/cli/commands/manager/tech-lead-lifecycle.ts +5 -2
  123. package/src/cli/commands/msg.ts +8 -7
  124. package/src/cli/commands/my-stories.ts +22 -13
  125. package/src/cli/commands/nuke.test.ts +31 -0
  126. package/src/cli/commands/nuke.ts +18 -7
  127. package/src/cli/commands/req.ts +9 -5
  128. package/src/cli/commands/stories.ts +22 -13
  129. package/src/cli/dashboard/panels/agents.ts +7 -3
  130. package/src/context-files/generator.ts +3 -2
  131. package/src/context-files/index.test.ts +1 -0
  132. package/src/db/client.ts +7 -0
  133. package/src/db/migrations/015-add-story-markdown-path.sql +5 -0
  134. package/src/db/queries/stories.ts +29 -5
  135. package/src/db/queries/test-helpers.ts +1 -0
  136. package/src/git/worktree.test.ts +43 -0
  137. package/src/git/worktree.ts +10 -0
  138. package/src/orchestrator/prompt-templates.test.ts +4 -0
  139. package/src/orchestrator/prompt-templates.ts +20 -8
  140. package/src/orchestrator/scheduler.test.ts +1 -0
  141. package/src/orchestrator/scheduler.ts +24 -11
  142. package/src/tmux/manager.ts +42 -13
  143. package/src/utils/instance.test.ts +129 -0
  144. package/src/utils/instance.ts +95 -0
  145. package/src/utils/paths.test.ts +8 -0
  146. package/src/utils/paths.ts +3 -0
  147. package/src/utils/story-markdown.test.ts +176 -0
  148. package/src/utils/story-markdown.ts +94 -0
@@ -106,7 +106,7 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
106
106
 
107
107
  // Update story with branch name if not set
108
108
  if (!this.story.branch_name) {
109
- updateStory(this.db, this.story.id, { branchName });
109
+ updateStory(this.db, this.story.id, { branchName }, this.storiesDir);
110
110
  }
111
111
 
112
112
  const prompt = `Implement this story:
@@ -144,7 +144,7 @@ Begin implementation.`;
144
144
  this.log('STORY_PROGRESS_UPDATE', 'Code changes proposed', { storyId: this.story.id });
145
145
 
146
146
  // Mark as complete
147
- updateStory(this.db, this.story.id, { status: 'review' });
147
+ updateStory(this.db, this.story.id, { status: 'review' }, this.storiesDir);
148
148
  this.log('STORY_COMPLETED', 'Implementation complete, ready for review', {
149
149
  storyId: this.story.id,
150
150
  branchName,
@@ -111,7 +111,7 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
111
111
  .substring(0, 30)}`;
112
112
 
113
113
  if (!this.story.branch_name) {
114
- updateStory(this.db, this.story.id, { branchName });
114
+ updateStory(this.db, this.story.id, { branchName }, this.storiesDir);
115
115
  }
116
116
 
117
117
  const prompt = `I need to implement this simple story:
@@ -149,7 +149,7 @@ Please help me identify which files I need to read and modify.`;
149
149
  this.log('STORY_PROGRESS_UPDATE', 'Making code changes', { storyId: this.story.id });
150
150
 
151
151
  // Complete
152
- updateStory(this.db, this.story.id, { status: 'review' });
152
+ updateStory(this.db, this.story.id, { status: 'review' }, this.storiesDir);
153
153
  this.log('STORY_COMPLETED', 'Implementation complete, ready for review', {
154
154
  storyId: this.story.id,
155
155
  branchName,
package/src/agents/qa.ts CHANGED
@@ -98,7 +98,7 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
98
98
  storyId: story.id,
99
99
  }
100
100
  );
101
- updateStory(this.db, story.id, { status: 'qa_failed' });
101
+ updateStory(this.db, story.id, { status: 'qa_failed' }, this.storiesDir);
102
102
  this.checkAndEscalate(story);
103
103
  return;
104
104
  }
@@ -124,7 +124,7 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
124
124
  if (this.qaConfig.testCommand) {
125
125
  const testsPassed = await this.runTests(story.id);
126
126
  if (!testsPassed) {
127
- updateStory(this.db, story.id, { status: 'qa_failed' });
127
+ updateStory(this.db, story.id, { status: 'qa_failed' }, this.storiesDir);
128
128
  this.checkAndEscalate(story);
129
129
  return;
130
130
  }
@@ -199,7 +199,7 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
199
199
  private async createPR(story: StoryRow): Promise<void> {
200
200
  if (!story.branch_name) {
201
201
  this.log('STORY_PR_CREATED', 'No branch name, skipping PR creation', { storyId: story.id });
202
- updateStory(this.db, story.id, { status: 'pr_submitted' });
202
+ updateStory(this.db, story.id, { status: 'pr_submitted' }, this.storiesDir);
203
203
  return;
204
204
  }
205
205
 
@@ -250,10 +250,15 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
250
250
  });
251
251
 
252
252
  // Update story
253
- updateStory(this.db, story.id, {
254
- prUrl,
255
- status: 'pr_submitted',
256
- });
253
+ updateStory(
254
+ this.db,
255
+ story.id,
256
+ {
257
+ prUrl,
258
+ status: 'pr_submitted',
259
+ },
260
+ this.storiesDir
261
+ );
257
262
 
258
263
  this.log('STORY_PR_CREATED', `PR created: ${prUrl}`, {
259
264
  storyId: story.id,
@@ -267,7 +272,7 @@ ${this.memoryState.conversationSummary || 'Starting fresh.'}`;
267
272
  });
268
273
 
269
274
  // Still mark as pr_submitted since QA passed
270
- updateStory(this.db, story.id, { status: 'pr_submitted' });
275
+ updateStory(this.db, story.id, { status: 'pr_submitted' }, this.storiesDir);
271
276
  }
272
277
  }
273
278
 
@@ -118,10 +118,15 @@ This will help with story estimation and implementation.`;
118
118
 
119
119
  private async implementStory(story: StoryRow): Promise<void> {
120
120
  // Update assignment
121
- updateStory(this.db, story.id, {
122
- assignedAgentId: this.agentId,
123
- status: 'in_progress',
124
- });
121
+ updateStory(
122
+ this.db,
123
+ story.id,
124
+ {
125
+ assignedAgentId: this.agentId,
126
+ status: 'in_progress',
127
+ },
128
+ this.storiesDir
129
+ );
125
130
  updateAgent(this.db, this.agentId, { currentStoryId: story.id });
126
131
 
127
132
  // Create feature branch
@@ -162,10 +167,15 @@ Let me know when you're ready to proceed or if you have questions.`;
162
167
  this.log('STORY_PROGRESS_UPDATE', response.substring(0, 200), { storyId: story.id });
163
168
 
164
169
  // Mark for review when done (simplified)
165
- updateStory(this.db, story.id, {
166
- branchName,
167
- status: 'review',
168
- });
170
+ updateStory(
171
+ this.db,
172
+ story.id,
173
+ {
174
+ branchName,
175
+ status: 'review',
176
+ },
177
+ this.storiesDir
178
+ );
169
179
 
170
180
  this.log('STORY_COMPLETED', `Implementation complete, ready for review`, {
171
181
  storyId: story.id,
@@ -176,7 +186,7 @@ Let me know when you're ready to proceed or if you have questions.`;
176
186
  private async reviewStory(story: StoryRow): Promise<void> {
177
187
  if (story.assigned_agent_id === this.agentId) {
178
188
  // Self-review, move to QA
179
- updateStory(this.db, story.id, { status: 'qa' });
189
+ updateStory(this.db, story.id, { status: 'qa' }, this.storiesDir);
180
190
  this.log('STORY_REVIEW_REQUESTED', 'Self-implemented, moving to QA', { storyId: story.id });
181
191
  return;
182
192
  }
@@ -207,14 +217,14 @@ If issues found, describe them. If approved, confirm.`;
207
217
 
208
218
  if (hasIssues) {
209
219
  // Send back for fixes
210
- updateStory(this.db, story.id, { status: 'in_progress' });
220
+ updateStory(this.db, story.id, { status: 'in_progress' }, this.storiesDir);
211
221
  this.log('STORY_PROGRESS_UPDATE', 'Review issues found, sent back for fixes', {
212
222
  storyId: story.id,
213
223
  review: review.substring(0, 200),
214
224
  });
215
225
  } else {
216
226
  // Approve and move to QA
217
- updateStory(this.db, story.id, { status: 'qa' });
227
+ updateStory(this.db, story.id, { status: 'qa' }, this.storiesDir);
218
228
  this.log('STORY_REVIEW_REQUESTED', 'Review passed, moving to QA', { storyId: story.id });
219
229
  }
220
230
  }
@@ -196,23 +196,35 @@ Respond in JSON format:
196
196
  const storyIds: string[] = [];
197
197
  const storyIdMap: Record<string, string> = {};
198
198
 
199
+ const hiveRoot = findHiveRoot(this.workDir);
200
+ const storiesDir = hiveRoot ? getHivePaths(hiveRoot).storiesDir : undefined;
201
+
199
202
  for (const story of analysis.stories) {
200
203
  const team = this.teams.find(t => t.name === story.teamName);
201
204
 
202
- const storyRow = createStory(this.db, {
203
- requirementId: this.requirement!.id,
204
- teamId: team?.id,
205
- title: story.title,
206
- description: story.description,
207
- acceptanceCriteria: story.acceptanceCriteria,
208
- });
205
+ const storyRow = createStory(
206
+ this.db,
207
+ {
208
+ requirementId: this.requirement!.id,
209
+ teamId: team?.id,
210
+ title: story.title,
211
+ description: story.description,
212
+ acceptanceCriteria: story.acceptanceCriteria,
213
+ },
214
+ storiesDir
215
+ );
209
216
 
210
217
  // Update with complexity
211
- updateStory(this.db, storyRow.id, {
212
- complexityScore: story.estimatedComplexity,
213
- storyPoints: story.estimatedComplexity,
214
- status: 'estimated',
215
- });
218
+ updateStory(
219
+ this.db,
220
+ storyRow.id,
221
+ {
222
+ complexityScore: story.estimatedComplexity,
223
+ storyPoints: story.estimatedComplexity,
224
+ status: 'estimated',
225
+ },
226
+ storiesDir
227
+ );
216
228
 
217
229
  storyIds.push(storyRow.id);
218
230
  storyIdMap[story.title] = storyRow.id;
@@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS stories (
133
133
  external_subtask_id: null,
134
134
  external_provider: null,
135
135
  in_sprint: 0,
136
+ markdown_path: null,
136
137
  created_at: new Date().toISOString(),
137
138
  updated_at: new Date().toISOString(),
138
139
  },
@@ -161,6 +162,7 @@ CREATE TABLE IF NOT EXISTS stories (
161
162
  external_subtask_id: null,
162
163
  external_provider: null,
163
164
  in_sprint: 0,
165
+ markdown_path: null,
164
166
  created_at: new Date().toISOString(),
165
167
  updated_at: new Date().toISOString(),
166
168
  },
@@ -246,6 +248,7 @@ CREATE TABLE IF NOT EXISTS stories (
246
248
  external_subtask_id: null,
247
249
  external_provider: null,
248
250
  in_sprint: 0,
251
+ markdown_path: null,
249
252
  created_at: new Date().toISOString(),
250
253
  updated_at: new Date().toISOString(),
251
254
  },
@@ -288,6 +291,7 @@ CREATE TABLE IF NOT EXISTS stories (
288
291
  external_subtask_id: null,
289
292
  external_provider: null,
290
293
  in_sprint: 0,
294
+ markdown_path: null,
291
295
  created_at: new Date().toISOString(),
292
296
  updated_at: new Date().toISOString(),
293
297
  },
@@ -316,6 +320,7 @@ CREATE TABLE IF NOT EXISTS stories (
316
320
  external_subtask_id: null,
317
321
  external_provider: null,
318
322
  in_sprint: 0,
323
+ markdown_path: null,
319
324
  created_at: new Date().toISOString(),
320
325
  updated_at: new Date().toISOString(),
321
326
  },
@@ -18,8 +18,10 @@ export const assignCommand = new Command('assign')
18
18
  .action(async (options: { dryRun?: boolean }) => {
19
19
  // Track if we need to start the manager after DB is closed
20
20
  let shouldStartManager = false;
21
+ let savedHiveDir: string | undefined;
21
22
 
22
23
  await withHiveContext(async ({ root, paths, db }) => {
24
+ savedHiveDir = paths.hiveDir;
23
25
  const spinner = ora('Assigning stories...').start();
24
26
 
25
27
  try {
@@ -167,7 +169,7 @@ export const assignCommand = new Command('assign')
167
169
  // Determine if we should start the manager, but don't start it yet
168
170
  // Wait until after withHiveContext closes the DB to prevent race condition
169
171
  if (result.assigned > 0) {
170
- shouldStartManager = !(await isManagerRunning());
172
+ shouldStartManager = !(await isManagerRunning(paths.hiveDir));
171
173
  }
172
174
 
173
175
  console.log();
@@ -185,7 +187,7 @@ export const assignCommand = new Command('assign')
185
187
  // This prevents the race condition where manager loads stale DB state
186
188
  if (shouldStartManager) {
187
189
  const spinner = ora('Starting manager daemon...').start();
188
- const started = await startManager(60);
190
+ const started = await startManager(60, savedHiveDir);
189
191
  if (started) {
190
192
  spinner.succeed(chalk.green('Manager daemon started (checking every 60s)'));
191
193
  } else {
@@ -10,6 +10,7 @@ import { createLog } from '../../../db/queries/logs.js';
10
10
  import { updateRequirement } from '../../../db/queries/requirements.js';
11
11
  import { getStoriesByStatus, updateStory } from '../../../db/queries/stories.js';
12
12
  import { isTmuxSessionRunning } from '../../../tmux/manager.js';
13
+ import { getTechLeadSessionName } from '../../../utils/instance.js';
13
14
  import {
14
15
  createManagerNudgeEnvelope,
15
16
  sendToTmuxSession,
@@ -86,14 +87,15 @@ async function nudgeTechLeadForStalledHandoff(
86
87
  estimatedCount: number
87
88
  ): Promise<boolean> {
88
89
  // Brief lock for DB read
90
+ const fallbackSession = getTechLeadSessionName(ctx.paths.hiveDir);
89
91
  const techLeadInfo = await ctx.withDb(async db => {
90
92
  const techLead = getTechLead(db.db);
91
93
  return techLead
92
94
  ? {
93
- sessionName: techLead.tmux_session || 'hive-tech-lead',
95
+ sessionName: techLead.tmux_session || fallbackSession,
94
96
  cliTool: (techLead.cli_tool || 'claude') as CLITool,
95
97
  }
96
- : { sessionName: 'hive-tech-lead', cliTool: 'claude' as CLITool };
98
+ : { sessionName: fallbackSession, cliTool: 'claude' as CLITool };
97
99
  });
98
100
 
99
101
  if (!(await isTmuxSessionRunning(techLeadInfo.sessionName))) {
@@ -3,7 +3,6 @@
3
3
  import chalk from 'chalk';
4
4
  import { Command } from 'commander';
5
5
  import { createHash } from 'crypto';
6
- import { join } from 'path';
7
6
  import { ClusterRuntime, fetchLocalClusterStatus } from '../../../cluster/runtime.js';
8
7
  import { loadConfig } from '../../../config/loader.js';
9
8
  import type { HiveConfig } from '../../../config/schema.js';
@@ -26,6 +25,7 @@ import { AgentState } from '../../../state-detectors/types.js';
26
25
  import {
27
26
  captureTmuxPane,
28
27
  getHiveSessions,
28
+ getManagerSession,
29
29
  isManagerRunning,
30
30
  isTmuxSessionRunning,
31
31
  killTmuxSession,
@@ -34,6 +34,7 @@ import {
34
34
  import type { WithLockFn } from '../../../utils/auto-merge.js';
35
35
  import { autoMergeApprovedPRs } from '../../../utils/auto-merge.js';
36
36
  import type { CLITool } from '../../../utils/cli-commands.js';
37
+ import { getManagerLockPath, getTechLeadSessionName } from '../../../utils/instance.js';
37
38
  import { withHiveContext, withHiveRoot } from '../../../utils/with-hive-context.js';
38
39
  import {
39
40
  agentStates,
@@ -265,7 +266,7 @@ managerCommand
265
266
  const config = loadConfig(paths.hiveDir);
266
267
  let clusterRuntime: ClusterRuntime | null = null;
267
268
 
268
- const lockPath = join(paths.hiveDir, 'manager.lock');
269
+ const lockPath = getManagerLockPath(paths.hiveDir);
269
270
 
270
271
  // Acquire manager lock to ensure singleton
271
272
  let releaseLock: (() => Promise<void>) | null = null;
@@ -474,10 +475,12 @@ managerCommand
474
475
  .command('status')
475
476
  .description('Check if the manager daemon is running')
476
477
  .action(async () => {
477
- const running = await isManagerRunning();
478
+ const { paths } = withHiveRoot(c => c);
479
+ const managerSession = getManagerSession(paths.hiveDir);
480
+ const running = await isManagerRunning(paths.hiveDir);
478
481
  if (running) {
479
- console.log(chalk.green('Manager daemon is running (hive-manager tmux session)'));
480
- console.log(chalk.gray('To view: tmux attach -t hive-manager'));
482
+ console.log(chalk.green(`Manager daemon is running (${managerSession} tmux session)`));
483
+ console.log(chalk.gray(`To view: tmux attach -t ${managerSession}`));
481
484
  console.log(chalk.gray('To stop: hive manager stop'));
482
485
  } else {
483
486
  console.log(chalk.yellow('Manager daemon is not running'));
@@ -490,7 +493,8 @@ managerCommand
490
493
  .command('stop')
491
494
  .description('Stop the manager daemon')
492
495
  .action(async () => {
493
- const stopped = await stopManagerSession();
496
+ const { paths: stopPaths } = withHiveRoot(c => c);
497
+ const stopped = await stopManagerSession(stopPaths.hiveDir);
494
498
  if (stopped) {
495
499
  console.log(chalk.green('Manager daemon stopped'));
496
500
  } else {
@@ -535,8 +539,9 @@ async function managerCheck(
535
539
  const syncResult = await clusterRuntime.sync(db.db);
536
540
  if (!clusterRuntime.isLeader()) {
537
541
  const status = clusterRuntime.getStatus();
538
- if (await isTmuxSessionRunning('hive-tech-lead')) {
539
- await killTmuxSession('hive-tech-lead');
542
+ const techLeadSession = getTechLeadSessionName(paths.hiveDir);
543
+ if (await isTmuxSessionRunning(techLeadSession)) {
544
+ await killTmuxSession(techLeadSession);
540
545
  }
541
546
  const details = [];
542
547
  if (syncResult.local_events_emitted > 0) {
@@ -648,8 +653,8 @@ async function managerCheck(
648
653
 
649
654
  // Discover active tmux sessions
650
655
  verboseLogCtx(ctx, 'Step: discover hive tmux sessions');
651
- const sessions = await getHiveSessions();
652
- ctx.hiveSessions = sessions.filter(s => s.name.startsWith('hive-'));
656
+ const sessions = await getHiveSessions(ctx.paths.hiveDir);
657
+ ctx.hiveSessions = sessions;
653
658
  verboseLogCtx(ctx, `Discovered ${ctx.hiveSessions.length} hive session(s)`);
654
659
  await resolveOrphanedSessionEscalations(ctx);
655
660
 
@@ -966,7 +971,7 @@ async function scanAgentSessions(ctx: ManagerCheckContext): Promise<void> {
966
971
 
967
972
  // Phase 2: Per-session processing (tmux/AI outside lock, DB writes under brief locks)
968
973
  for (const session of ctx.hiveSessions) {
969
- if (session.name === 'hive-manager') continue;
974
+ if (session.name === getManagerSession(ctx.paths.hiveDir)) continue;
970
975
 
971
976
  const agent = ctx.agentsBySessionName.get(session.name);
972
977
 
@@ -15,6 +15,7 @@ import {
15
15
  spawnTmuxSession,
16
16
  } from '../../../tmux/manager.js';
17
17
  import type { CLITool } from '../../../utils/cli-commands.js';
18
+ import { getTechLeadSessionName } from '../../../utils/instance.js';
18
19
  import { findHiveRoot as findHiveRootFromDir, getHivePaths } from '../../../utils/paths.js';
19
20
  import { generateTechLeadPrompt } from '../req.js';
20
21
  import { detectAgentState } from './agent-monitoring.js';
@@ -150,10 +151,12 @@ export async function restartStaleTechLead(ctx: ManagerCheckContext): Promise<vo
150
151
  activeReq.description,
151
152
  teams,
152
153
  activeReq.godmode === 1,
153
- activeReq.target_branch || 'main'
154
+ activeReq.target_branch || 'main',
155
+ getTechLeadSessionName(paths.hiveDir)
154
156
  );
155
157
  }
156
158
 
159
+ const techLeadInbox = getTechLeadSessionName(paths.hiveDir);
157
160
  return `You are the Tech Lead of Hive, an AI development team orchestrator.
158
161
 
159
162
  You have been restarted to refresh your context. No active requirement is currently being planned.
@@ -167,7 +170,7 @@ hive status
167
170
 
168
171
  2. Check your inbox for messages from developers:
169
172
  \`\`\`bash
170
- hive msg inbox hive-tech-lead
173
+ hive msg inbox ${techLeadInbox}
171
174
  \`\`\`
172
175
 
173
176
  3. If there are pending requirements, begin planning them. If all work is complete, monitor for new requirements.`;
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { Command } from 'commander';
5
5
  import { nanoid } from 'nanoid';
6
6
  import { queryAll, queryOne, run } from '../../db/client.js';
7
+ import { getTechLeadSessionName } from '../../utils/instance.js';
7
8
  import { withHiveContext, withReadOnlyHiveContext } from '../../utils/with-hive-context.js';
8
9
 
9
10
  interface MessageRow {
@@ -24,12 +25,12 @@ msgCommand
24
25
  .command('send <to-session> <message>')
25
26
  .description('Send a message to another agent')
26
27
  .option('-s, --subject <subject>', 'Message subject')
27
- .option('-f, --from <session>', 'Your session name (defaults to hive-tech-lead)')
28
+ .option('-f, --from <session>', 'Your session name (defaults to tech lead session)')
28
29
  .action(
29
30
  async (toSession: string, message: string, options: { subject?: string; from?: string }) => {
30
- await withHiveContext(async ({ db }) => {
31
+ await withHiveContext(async ({ db, paths }) => {
31
32
  const id = `msg-${nanoid(8)}`;
32
- const fromSession = options.from || 'hive-tech-lead';
33
+ const fromSession = options.from || getTechLeadSessionName(paths.hiveDir);
33
34
 
34
35
  run(
35
36
  db.db,
@@ -53,8 +54,8 @@ msgCommand
53
54
  .description('Check inbox for messages')
54
55
  .option('--all', 'Show all messages including read')
55
56
  .action(async (session: string | undefined, options: { all?: boolean }) => {
56
- await withReadOnlyHiveContext(async ({ db }) => {
57
- const targetSession = session || 'hive-tech-lead';
57
+ await withReadOnlyHiveContext(async ({ db, paths }) => {
58
+ const targetSession = session || getTechLeadSessionName(paths.hiveDir);
58
59
 
59
60
  let query = `
60
61
  SELECT * FROM messages
@@ -165,8 +166,8 @@ msgCommand
165
166
  .command('outbox [session]')
166
167
  .description('Check sent messages and their replies')
167
168
  .action(async (session: string | undefined) => {
168
- await withReadOnlyHiveContext(async ({ db }) => {
169
- const fromSession = session || 'hive-tech-lead';
169
+ await withReadOnlyHiveContext(async ({ db, paths }) => {
170
+ const fromSession = session || getTechLeadSessionName(paths.hiveDir);
170
171
 
171
172
  const messages = queryAll<MessageRow>(
172
173
  db.db,
@@ -228,7 +228,7 @@ myStoriesCommand
228
228
  process.exit(1);
229
229
  }
230
230
 
231
- await withHiveContext(async ({ db }) => {
231
+ await withHiveContext(async ({ paths, db }) => {
232
232
  const agent = requireAgentBySession(db.db, options.session);
233
233
 
234
234
  if (!agent.team_id) {
@@ -245,19 +245,28 @@ myStoriesCommand
245
245
  ? trimmedTitle
246
246
  : `Refactor: ${trimmedTitle}`;
247
247
 
248
- const story = createStory(db.db, {
249
- teamId: agent.team_id,
250
- title: normalizedTitle,
251
- description: options.description.trim(),
252
- acceptanceCriteria:
253
- options.criteria && options.criteria.length > 0 ? options.criteria : null,
254
- });
248
+ const story = createStory(
249
+ db.db,
250
+ {
251
+ teamId: agent.team_id,
252
+ title: normalizedTitle,
253
+ description: options.description.trim(),
254
+ acceptanceCriteria:
255
+ options.criteria && options.criteria.length > 0 ? options.criteria : null,
256
+ },
257
+ paths.storiesDir
258
+ );
255
259
 
256
- const updatedStory = updateStory(db.db, story.id, {
257
- complexityScore: points,
258
- storyPoints: points,
259
- status,
260
- });
260
+ const updatedStory = updateStory(
261
+ db.db,
262
+ story.id,
263
+ {
264
+ complexityScore: points,
265
+ storyPoints: points,
266
+ status,
267
+ },
268
+ paths.storiesDir
269
+ );
261
270
 
262
271
  createLog(db.db, {
263
272
  agentId: agent.id,
@@ -23,6 +23,16 @@ vi.mock('../../utils/with-hive-context.js', () => ({
23
23
  ),
24
24
  }));
25
25
 
26
+ vi.mock('fs', async importOriginal => {
27
+ const actual = await importOriginal<typeof import('fs')>();
28
+ return {
29
+ ...actual,
30
+ existsSync: vi.fn(() => true),
31
+ unlinkSync: vi.fn(),
32
+ };
33
+ });
34
+
35
+ import { existsSync } from 'fs';
26
36
  import { queryAll, run } from '../../db/client.js';
27
37
  import { removeWorktree } from '../../git/worktree.js';
28
38
  import { killAllHiveSessions } from '../../tmux/manager.js';
@@ -31,6 +41,7 @@ import { nukeCommand } from './nuke.js';
31
41
  describe('nuke command', () => {
32
42
  beforeEach(() => {
33
43
  vi.clearAllMocks();
44
+ vi.mocked(existsSync).mockReturnValue(true);
34
45
  });
35
46
 
36
47
  describe('command structure', () => {
@@ -145,6 +156,26 @@ describe('nuke command', () => {
145
156
 
146
157
  expect(killAllHiveSessions).toHaveBeenCalled();
147
158
  });
159
+
160
+ it('should skip worktree removal when path does not exist on disk', async () => {
161
+ vi.mocked(queryAll).mockReturnValue([{ worktree_path: 'repos/team-agent-stale' }]);
162
+ vi.mocked(existsSync).mockReturnValue(false);
163
+
164
+ const agentsCmd = nukeCommand.commands.find(cmd => cmd.name() === 'agents');
165
+ await agentsCmd?.parseAsync(['--force'], { from: 'user' });
166
+
167
+ expect(removeWorktree).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it('should call removeWorktree when worktree path exists on disk', async () => {
171
+ vi.mocked(queryAll).mockReturnValue([{ worktree_path: 'repos/team-agent-abc123' }]);
172
+ vi.mocked(existsSync).mockReturnValue(true);
173
+
174
+ const agentsCmd = nukeCommand.commands.find(cmd => cmd.name() === 'agents');
175
+ await agentsCmd?.parseAsync(['--force'], { from: 'user' });
176
+
177
+ expect(removeWorktree).toHaveBeenCalledWith('/root', 'repos/team-agent-abc123');
178
+ });
148
179
  });
149
180
 
150
181
  describe('nuke all worktree and table cleanup', () => {
@@ -3,7 +3,7 @@
3
3
  import chalk from 'chalk';
4
4
  import { Command } from 'commander';
5
5
  import { existsSync, unlinkSync } from 'fs';
6
- import { join } from 'path';
6
+ import { join, resolve } from 'path';
7
7
  import readline from 'readline';
8
8
  import { queryAll, queryOne, run } from '../../db/client.js';
9
9
  import { removeWorktree } from '../../git/worktree.js';
@@ -101,6 +101,13 @@ async function removeAgentWorktrees(
101
101
  let removed = 0;
102
102
  for (const agent of agents) {
103
103
  if (agent.worktree_path) {
104
+ const fullPath = resolve(root, agent.worktree_path);
105
+ if (!existsSync(fullPath)) {
106
+ if (process.env.HIVE_DEBUG) {
107
+ console.log(`[debug] skipping missing worktree: ${fullPath}`);
108
+ }
109
+ continue;
110
+ }
104
111
  const result = removeWorktree(root, agent.worktree_path);
105
112
  if (result.success) {
106
113
  removed++;
@@ -174,7 +181,7 @@ export const nukeCommand = new Command('nuke')
174
181
  .option('--force', 'Skip confirmation')
175
182
  .action(async (options: { force?: boolean }) => {
176
183
  try {
177
- await withHiveContext(async ({ db, root }) => {
184
+ await withHiveContext(async ({ db, root, paths }) => {
178
185
  const count = queryOne<{ count: number }>(
179
186
  db.db,
180
187
  'SELECT COUNT(*) as count FROM agents'
@@ -199,7 +206,7 @@ export const nukeCommand = new Command('nuke')
199
206
  }
200
207
 
201
208
  // Kill all hive tmux sessions first (this should always work regardless of DB state)
202
- const killed = await killAllHiveSessions();
209
+ const killed = await killAllHiveSessions(paths.hiveDir);
203
210
  console.log(chalk.gray(`Killed ${killed} tmux sessions.`));
204
211
 
205
212
  // Remove agent worktrees from filesystem before deleting agents from DB
@@ -232,7 +239,9 @@ export const nukeCommand = new Command('nuke')
232
239
  ) {
233
240
  console.error(chalk.red(`\nDatabase is corrupted: ${message}`));
234
241
  console.log(chalk.yellow('Killing tmux sessions anyway...'));
235
- const killed = await killAllHiveSessions();
242
+ const hiveRoot = findHiveRoot();
243
+ const hDir = hiveRoot ? getHivePaths(hiveRoot).hiveDir : undefined;
244
+ const killed = await killAllHiveSessions(hDir);
236
245
  console.log(chalk.gray(`Killed ${killed} tmux sessions.`));
237
246
  console.log(
238
247
  chalk.yellow('Agent records could not be deleted from the corrupted database.')
@@ -309,7 +318,7 @@ export const nukeCommand = new Command('nuke')
309
318
  .option('--force', 'Skip confirmation')
310
319
  .action(async (options: { force?: boolean }) => {
311
320
  try {
312
- await withHiveContext(async ({ db, root }) => {
321
+ await withHiveContext(async ({ db, root, paths }) => {
313
322
  console.log(chalk.red('\nThis will:'));
314
323
  console.log(chalk.yellow(' - Kill all hive tmux sessions'));
315
324
  console.log(chalk.yellow(' - Remove all agent git worktrees'));
@@ -333,7 +342,7 @@ export const nukeCommand = new Command('nuke')
333
342
  }
334
343
 
335
344
  // Kill all hive tmux sessions first (always works regardless of DB state)
336
- const killed = await killAllHiveSessions();
345
+ const killed = await killAllHiveSessions(paths.hiveDir);
337
346
  console.log(chalk.gray(`Killed ${killed} tmux sessions.`));
338
347
 
339
348
  // Remove agent worktrees from filesystem before deleting agents from DB
@@ -371,7 +380,9 @@ export const nukeCommand = new Command('nuke')
371
380
  ) {
372
381
  console.error(chalk.red(`\nDatabase is corrupted: ${message}`));
373
382
  console.log(chalk.yellow('Killing tmux sessions anyway...'));
374
- const killed = await killAllHiveSessions();
383
+ const hiveRoot2 = findHiveRoot();
384
+ const hDir2 = hiveRoot2 ? getHivePaths(hiveRoot2).hiveDir : undefined;
385
+ const killed = await killAllHiveSessions(hDir2);
375
386
  console.log(chalk.gray(`Killed ${killed} tmux sessions.`));
376
387
  console.log(
377
388
  chalk.yellow('Data records could not be deleted from the corrupted database.')