hungry-ghost-hive 0.44.0 → 0.46.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 (238) 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/cluster.d.ts.map +1 -1
  24. package/dist/cli/commands/cluster.js +348 -1
  25. package/dist/cli/commands/cluster.js.map +1 -1
  26. package/dist/cli/commands/cluster.test.js +313 -9
  27. package/dist/cli/commands/cluster.test.js.map +1 -1
  28. package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
  29. package/dist/cli/commands/manager/handoff-recovery.js +4 -2
  30. package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
  31. package/dist/cli/commands/manager/index.d.ts.map +1 -1
  32. package/dist/cli/commands/manager/index.js +16 -12
  33. package/dist/cli/commands/manager/index.js.map +1 -1
  34. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
  35. package/dist/cli/commands/manager/tech-lead-lifecycle.js +4 -2
  36. package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
  37. package/dist/cli/commands/msg.d.ts.map +1 -1
  38. package/dist/cli/commands/msg.js +8 -7
  39. package/dist/cli/commands/msg.js.map +1 -1
  40. package/dist/cli/commands/my-stories.js +3 -3
  41. package/dist/cli/commands/my-stories.js.map +1 -1
  42. package/dist/cli/commands/nuke.d.ts.map +1 -1
  43. package/dist/cli/commands/nuke.js +18 -7
  44. package/dist/cli/commands/nuke.js.map +1 -1
  45. package/dist/cli/commands/nuke.test.js +24 -0
  46. package/dist/cli/commands/nuke.test.js.map +1 -1
  47. package/dist/cli/commands/req-spawn.test.d.ts +2 -0
  48. package/dist/cli/commands/req-spawn.test.d.ts.map +1 -0
  49. package/dist/cli/commands/req-spawn.test.js +116 -0
  50. package/dist/cli/commands/req-spawn.test.js.map +1 -0
  51. package/dist/cli/commands/req.d.ts +1 -1
  52. package/dist/cli/commands/req.d.ts.map +1 -1
  53. package/dist/cli/commands/req.js +28 -18
  54. package/dist/cli/commands/req.js.map +1 -1
  55. package/dist/cli/commands/stories.js +3 -3
  56. package/dist/cli/commands/stories.js.map +1 -1
  57. package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
  58. package/dist/cli/dashboard/panels/agents.js +7 -3
  59. package/dist/cli/dashboard/panels/agents.js.map +1 -1
  60. package/dist/cluster/cluster-http-server.d.ts +32 -0
  61. package/dist/cluster/cluster-http-server.d.ts.map +1 -1
  62. package/dist/cluster/cluster-http-server.js +42 -0
  63. package/dist/cluster/cluster-http-server.js.map +1 -1
  64. package/dist/cluster/distributed-runtime-coverage.test.js +9 -0
  65. package/dist/cluster/distributed-runtime-coverage.test.js.map +1 -1
  66. package/dist/cluster/distributed-system.test.js +135 -0
  67. package/dist/cluster/distributed-system.test.js.map +1 -1
  68. package/dist/cluster/events.d.ts +23 -0
  69. package/dist/cluster/events.d.ts.map +1 -1
  70. package/dist/cluster/events.js +74 -0
  71. package/dist/cluster/events.js.map +1 -1
  72. package/dist/cluster/heartbeat-manager.d.ts +2 -0
  73. package/dist/cluster/heartbeat-manager.d.ts.map +1 -1
  74. package/dist/cluster/heartbeat-manager.js +42 -6
  75. package/dist/cluster/heartbeat-manager.js.map +1 -1
  76. package/dist/cluster/membership.test.d.ts +2 -0
  77. package/dist/cluster/membership.test.d.ts.map +1 -0
  78. package/dist/cluster/membership.test.js +416 -0
  79. package/dist/cluster/membership.test.js.map +1 -0
  80. package/dist/cluster/partition-safety.test.d.ts +2 -0
  81. package/dist/cluster/partition-safety.test.d.ts.map +1 -0
  82. package/dist/cluster/partition-safety.test.js +440 -0
  83. package/dist/cluster/partition-safety.test.js.map +1 -0
  84. package/dist/cluster/raft-state-machine.d.ts +33 -1
  85. package/dist/cluster/raft-state-machine.d.ts.map +1 -1
  86. package/dist/cluster/raft-state-machine.js +65 -3
  87. package/dist/cluster/raft-state-machine.js.map +1 -1
  88. package/dist/cluster/raft-store.d.ts +26 -1
  89. package/dist/cluster/raft-store.d.ts.map +1 -1
  90. package/dist/cluster/raft-store.js +137 -0
  91. package/dist/cluster/raft-store.js.map +1 -1
  92. package/dist/cluster/replication-lag.test.d.ts +2 -0
  93. package/dist/cluster/replication-lag.test.d.ts.map +1 -0
  94. package/dist/cluster/replication-lag.test.js +239 -0
  95. package/dist/cluster/replication-lag.test.js.map +1 -0
  96. package/dist/cluster/replication.d.ts +2 -2
  97. package/dist/cluster/replication.d.ts.map +1 -1
  98. package/dist/cluster/replication.js +1 -1
  99. package/dist/cluster/replication.js.map +1 -1
  100. package/dist/cluster/runtime.d.ts +78 -0
  101. package/dist/cluster/runtime.d.ts.map +1 -1
  102. package/dist/cluster/runtime.js +400 -13
  103. package/dist/cluster/runtime.js.map +1 -1
  104. package/dist/cluster/state-recovery.test.d.ts +2 -0
  105. package/dist/cluster/state-recovery.test.d.ts.map +1 -0
  106. package/dist/cluster/state-recovery.test.js +310 -0
  107. package/dist/cluster/state-recovery.test.js.map +1 -0
  108. package/dist/cluster/types.d.ts +30 -0
  109. package/dist/cluster/types.d.ts.map +1 -1
  110. package/dist/config/schema.d.ts +48 -0
  111. package/dist/config/schema.d.ts.map +1 -1
  112. package/dist/config/schema.js +11 -0
  113. package/dist/config/schema.js.map +1 -1
  114. package/dist/context-files/generator.d.ts +1 -1
  115. package/dist/context-files/generator.d.ts.map +1 -1
  116. package/dist/context-files/generator.js +4 -3
  117. package/dist/context-files/generator.js.map +1 -1
  118. package/dist/context-files/generator.test.js +51 -0
  119. package/dist/context-files/generator.test.js.map +1 -1
  120. package/dist/context-files/index.test.js +1 -0
  121. package/dist/context-files/index.test.js.map +1 -1
  122. package/dist/db/client.d.ts +1 -0
  123. package/dist/db/client.d.ts.map +1 -1
  124. package/dist/db/client.js +6 -0
  125. package/dist/db/client.js.map +1 -1
  126. package/dist/db/migrations/015-add-story-markdown-path.sql +5 -0
  127. package/dist/db/queries/stories.d.ts +3 -3
  128. package/dist/db/queries/stories.d.ts.map +1 -1
  129. package/dist/db/queries/stories.js +23 -5
  130. package/dist/db/queries/stories.js.map +1 -1
  131. package/dist/db/queries/test-helpers.d.ts.map +1 -1
  132. package/dist/db/queries/test-helpers.js +1 -0
  133. package/dist/db/queries/test-helpers.js.map +1 -1
  134. package/dist/git/worktree.d.ts.map +1 -1
  135. package/dist/git/worktree.js +7 -0
  136. package/dist/git/worktree.js.map +1 -1
  137. package/dist/git/worktree.test.js +30 -0
  138. package/dist/git/worktree.test.js.map +1 -1
  139. package/dist/orchestrator/orphan-recovery.d.ts +1 -1
  140. package/dist/orchestrator/orphan-recovery.d.ts.map +1 -1
  141. package/dist/orchestrator/orphan-recovery.js +4 -4
  142. package/dist/orchestrator/orphan-recovery.js.map +1 -1
  143. package/dist/orchestrator/prompt-templates.d.ts +6 -2
  144. package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
  145. package/dist/orchestrator/prompt-templates.js +61 -16
  146. package/dist/orchestrator/prompt-templates.js.map +1 -1
  147. package/dist/orchestrator/prompt-templates.test.js +214 -0
  148. package/dist/orchestrator/prompt-templates.test.js.map +1 -1
  149. package/dist/orchestrator/scheduler.d.ts +1 -0
  150. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  151. package/dist/orchestrator/scheduler.js +30 -17
  152. package/dist/orchestrator/scheduler.js.map +1 -1
  153. package/dist/orchestrator/scheduler.test.js +98 -6
  154. package/dist/orchestrator/scheduler.test.js.map +1 -1
  155. package/dist/tmux/manager.d.ts +7 -6
  156. package/dist/tmux/manager.d.ts.map +1 -1
  157. package/dist/tmux/manager.js +29 -13
  158. package/dist/tmux/manager.js.map +1 -1
  159. package/dist/utils/instance.d.ts +32 -0
  160. package/dist/utils/instance.d.ts.map +1 -0
  161. package/dist/utils/instance.js +82 -0
  162. package/dist/utils/instance.js.map +1 -0
  163. package/dist/utils/instance.test.d.ts +2 -0
  164. package/dist/utils/instance.test.d.ts.map +1 -0
  165. package/dist/utils/instance.test.js +103 -0
  166. package/dist/utils/instance.test.js.map +1 -0
  167. package/dist/utils/paths.d.ts +2 -0
  168. package/dist/utils/paths.d.ts.map +1 -1
  169. package/dist/utils/paths.js +2 -0
  170. package/dist/utils/paths.js.map +1 -1
  171. package/dist/utils/paths.test.js +6 -0
  172. package/dist/utils/paths.test.js.map +1 -1
  173. package/dist/utils/story-markdown.d.ts +16 -0
  174. package/dist/utils/story-markdown.d.ts.map +1 -0
  175. package/dist/utils/story-markdown.js +82 -0
  176. package/dist/utils/story-markdown.js.map +1 -0
  177. package/dist/utils/story-markdown.test.d.ts +2 -0
  178. package/dist/utils/story-markdown.test.d.ts.map +1 -0
  179. package/dist/utils/story-markdown.test.js +143 -0
  180. package/dist/utils/story-markdown.test.js.map +1 -0
  181. package/package.json +1 -1
  182. package/src/agents/base-agent.ts +5 -0
  183. package/src/agents/intermediate.ts +2 -2
  184. package/src/agents/junior.ts +2 -2
  185. package/src/agents/qa.ts +13 -8
  186. package/src/agents/senior.ts +21 -11
  187. package/src/agents/tech-lead.ts +24 -12
  188. package/src/cli/commands/assign.test.ts +5 -0
  189. package/src/cli/commands/assign.ts +4 -2
  190. package/src/cli/commands/cluster.test.ts +387 -9
  191. package/src/cli/commands/cluster.ts +486 -1
  192. package/src/cli/commands/manager/handoff-recovery.ts +4 -2
  193. package/src/cli/commands/manager/index.ts +16 -11
  194. package/src/cli/commands/manager/tech-lead-lifecycle.ts +5 -2
  195. package/src/cli/commands/msg.ts +8 -7
  196. package/src/cli/commands/my-stories.ts +22 -13
  197. package/src/cli/commands/nuke.test.ts +31 -0
  198. package/src/cli/commands/nuke.ts +18 -7
  199. package/src/cli/commands/req-spawn.test.ts +153 -0
  200. package/src/cli/commands/req.ts +40 -23
  201. package/src/cli/commands/stories.ts +22 -13
  202. package/src/cli/dashboard/panels/agents.ts +7 -3
  203. package/src/cluster/cluster-http-server.ts +80 -0
  204. package/src/cluster/distributed-runtime-coverage.test.ts +9 -0
  205. package/src/cluster/distributed-system.test.ts +168 -0
  206. package/src/cluster/events.ts +90 -0
  207. package/src/cluster/heartbeat-manager.ts +48 -6
  208. package/src/cluster/membership.test.ts +498 -0
  209. package/src/cluster/partition-safety.test.ts +523 -0
  210. package/src/cluster/raft-state-machine.ts +76 -4
  211. package/src/cluster/raft-store.ts +167 -1
  212. package/src/cluster/replication-lag.test.ts +284 -0
  213. package/src/cluster/replication.ts +6 -0
  214. package/src/cluster/runtime.ts +551 -12
  215. package/src/cluster/state-recovery.test.ts +420 -0
  216. package/src/cluster/types.ts +32 -0
  217. package/src/config/schema.ts +11 -0
  218. package/src/context-files/generator.test.ts +55 -0
  219. package/src/context-files/generator.ts +8 -7
  220. package/src/context-files/index.test.ts +1 -0
  221. package/src/db/client.ts +7 -0
  222. package/src/db/migrations/015-add-story-markdown-path.sql +5 -0
  223. package/src/db/queries/stories.ts +29 -5
  224. package/src/db/queries/test-helpers.ts +1 -0
  225. package/src/git/worktree.test.ts +43 -0
  226. package/src/git/worktree.ts +10 -0
  227. package/src/orchestrator/orphan-recovery.ts +32 -13
  228. package/src/orchestrator/prompt-templates.test.ts +267 -0
  229. package/src/orchestrator/prompt-templates.ts +69 -16
  230. package/src/orchestrator/scheduler.test.ts +130 -6
  231. package/src/orchestrator/scheduler.ts +66 -27
  232. package/src/tmux/manager.ts +42 -13
  233. package/src/utils/instance.test.ts +129 -0
  234. package/src/utils/instance.ts +95 -0
  235. package/src/utils/paths.test.ts +8 -0
  236. package/src/utils/paths.ts +3 -0
  237. package/src/utils/story-markdown.test.ts +176 -0
  238. package/src/utils/story-markdown.ts +94 -0
@@ -1,8 +1,11 @@
1
1
  // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
2
 
3
+ import { existsSync, mkdirSync, rmSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
3
6
  import type { Database } from 'sql.js';
4
7
  import initSqlJs from 'sql.js';
5
- import { beforeEach, describe, expect, it, vi } from 'vitest';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
9
 
7
10
  import { getLogsByEventType } from '../db/queries/logs.js';
8
11
  import { createPullRequest } from '../db/queries/pull-requests.js';
@@ -17,6 +20,7 @@ import {
17
20
  import { createTeam } from '../db/queries/teams.js';
18
21
  import * as worktreeModule from '../git/worktree.js';
19
22
  import * as tmuxModule from '../tmux/manager.js';
23
+ import { generateSessionName } from '../tmux/manager.js';
20
24
  import { getAgentWorkload, selectAgentWithLeastWorkload } from './agent-selector.js';
21
25
  import {
22
26
  getCapacityPoints,
@@ -107,13 +111,15 @@ CREATE TABLE IF NOT EXISTS teams (
107
111
 
108
112
  CREATE TABLE IF NOT EXISTS agents (
109
113
  id TEXT PRIMARY KEY,
110
- type TEXT NOT NULL CHECK (type IN ('tech_lead', 'senior', 'intermediate', 'junior', 'qa')),
114
+ type TEXT NOT NULL CHECK (type IN ('tech_lead', 'senior', 'intermediate', 'junior', 'qa', 'feature_test', 'auditor')),
111
115
  team_id TEXT REFERENCES teams(id),
112
116
  tmux_session TEXT,
113
117
  model TEXT,
114
118
  status TEXT DEFAULT 'idle' CHECK (status IN ('idle', 'working', 'blocked', 'terminated')),
115
119
  current_story_id TEXT,
116
120
  memory_state TEXT,
121
+ last_seen TIMESTAMP,
122
+ worktree_path TEXT,
117
123
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
118
124
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
119
125
  );
@@ -141,6 +147,7 @@ CREATE TABLE IF NOT EXISTS stories (
141
147
  assigned_agent_id TEXT REFERENCES agents(id),
142
148
  branch_name TEXT,
143
149
  pr_url TEXT,
150
+ markdown_path TEXT,
144
151
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
152
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
146
153
  );
@@ -793,6 +800,50 @@ describe('Scheduler Orphaned Story Recovery', () => {
793
800
  expect(recovered).toContain(story1.id);
794
801
  expect(recovered).toContain(story2.id);
795
802
  });
803
+
804
+ it('should write markdown files when storiesDir is provided during orphan recovery', () => {
805
+ const storiesDir = join(tmpdir(), `hive-test-stories-${Date.now()}`);
806
+ mkdirSync(storiesDir, { recursive: true });
807
+
808
+ try {
809
+ const team = createTeam(db, {
810
+ name: 'MD Test Team',
811
+ repoUrl: 'https://github.com/test/repo',
812
+ repoPath: 'test',
813
+ });
814
+
815
+ const terminatedAgentId = 'agent-md-terminated';
816
+ db.run(
817
+ `INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
818
+ VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`,
819
+ [terminatedAgentId, 'intermediate', team.id, 'terminated']
820
+ );
821
+
822
+ const story = createStory(db, {
823
+ teamId: team.id,
824
+ title: 'Story with Markdown',
825
+ description: 'Should get a markdown file on recovery',
826
+ });
827
+ updateStory(db, story.id, {
828
+ assignedAgentId: terminatedAgentId,
829
+ status: 'in_progress',
830
+ });
831
+
832
+ const recovered = detectAndRecoverOrphanedStories(db, '/tmp', storiesDir);
833
+
834
+ expect(recovered).toContain(story.id);
835
+
836
+ // Verify markdown file was written
837
+ const mdPath = join(storiesDir, `${story.id}.md`);
838
+ expect(existsSync(mdPath)).toBe(true);
839
+
840
+ // Verify markdown_path was set in DB
841
+ const updatedStory = getStoryById(db, story.id);
842
+ expect(updatedStory?.markdown_path).toBe(mdPath);
843
+ } finally {
844
+ rmSync(storiesDir, { recursive: true, force: true });
845
+ }
846
+ });
796
847
  });
797
848
 
798
849
  describe('Scheduler Refactor Capacity Policy', () => {
@@ -945,6 +996,7 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
945
996
  external_subtask_id: null,
946
997
  external_provider: null,
947
998
  in_sprint: 0,
999
+ markdown_path: null,
948
1000
  created_at: new Date().toISOString(),
949
1001
  updated_at: new Date().toISOString(),
950
1002
  };
@@ -1904,6 +1956,9 @@ describe('Scheduler checkScaling', () => {
1904
1956
  repoPath: 'test',
1905
1957
  });
1906
1958
 
1959
+ const gapHiveDir = join(mockConfig.rootDir, '.hive');
1960
+ const gapSessionPrefix = generateSessionName('senior', team.name, undefined, gapHiveDir);
1961
+
1907
1962
  for (const index of [2, 3, 4, 5]) {
1908
1963
  db.run(
1909
1964
  `INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
@@ -1912,7 +1967,7 @@ describe('Scheduler checkScaling', () => {
1912
1967
  `senior-gap-${index}`,
1913
1968
  'senior',
1914
1969
  team.id,
1915
- `hive-senior-${team.name}-${index}`,
1970
+ `${gapSessionPrefix}-${index}`,
1916
1971
  'working',
1917
1972
  `STORY-EXISTING-${index}`,
1918
1973
  ]
@@ -1934,7 +1989,7 @@ describe('Scheduler checkScaling', () => {
1934
1989
  db.run(
1935
1990
  `INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
1936
1991
  VALUES (?, ?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`,
1937
- ['senior-gap-6', 'senior', team.id, `hive-senior-${team.name}-6`, 'idle']
1992
+ ['senior-gap-6', 'senior', team.id, `${gapSessionPrefix}-6`, 'idle']
1938
1993
  );
1939
1994
  return {
1940
1995
  id: 'senior-gap-6',
@@ -1942,7 +1997,7 @@ describe('Scheduler checkScaling', () => {
1942
1997
  team_id: team.id,
1943
1998
  status: 'idle',
1944
1999
  current_story_id: null,
1945
- tmux_session: `hive-senior-${team.name}-6`,
2000
+ tmux_session: `${gapSessionPrefix}-6`,
1946
2001
  };
1947
2002
  });
1948
2003
 
@@ -2055,10 +2110,13 @@ describe('Scheduler checkScaling', () => {
2055
2110
  repoPath: 'test',
2056
2111
  });
2057
2112
 
2113
+ const hiveDir = join(mockConfig.rootDir, '.hive');
2114
+ const expectedSession = generateSessionName('senior', team.name, undefined, hiveDir);
2115
+
2058
2116
  db.run(
2059
2117
  `INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
2060
2118
  VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
2061
- ['senior-guard-1', 'senior', team.id, `hive-senior-${team.name}`, 'working', 'STORY-ACTIVE']
2119
+ ['senior-guard-1', 'senior', team.id, expectedSession, 'working', 'STORY-ACTIVE']
2062
2120
  );
2063
2121
 
2064
2122
  const isRunningSpy = vi.spyOn(tmuxModule, 'isTmuxSessionRunning').mockResolvedValue(true);
@@ -2170,6 +2228,72 @@ describe('Scheduler checkScaling', () => {
2170
2228
  });
2171
2229
  });
2172
2230
 
2231
+ describe('Scheduler Markdown File Writing', () => {
2232
+ let storiesDir: string;
2233
+
2234
+ beforeEach(() => {
2235
+ storiesDir = join(
2236
+ tmpdir(),
2237
+ `hive-test-stories-${Date.now()}-${Math.random().toString(36).slice(2)}`
2238
+ );
2239
+ mkdirSync(storiesDir, { recursive: true });
2240
+ });
2241
+
2242
+ afterEach(() => {
2243
+ rmSync(storiesDir, { recursive: true, force: true });
2244
+ });
2245
+
2246
+ it('should write markdown files when assigning stories via scheduler', async () => {
2247
+ const team = createTeam(db, {
2248
+ name: 'MD Write Team',
2249
+ repoUrl: 'https://github.com/test/repo',
2250
+ repoPath: 'test',
2251
+ });
2252
+
2253
+ // Create an idle senior agent
2254
+ db.run(
2255
+ `INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
2256
+ VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`,
2257
+ ['senior-md-1', 'senior', team.id, 'idle']
2258
+ );
2259
+
2260
+ const story = createStory(db, {
2261
+ teamId: team.id,
2262
+ title: 'Markdown Test Story',
2263
+ description: 'Should get a markdown file on assignment',
2264
+ });
2265
+ updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
2266
+
2267
+ // Create scheduler with rootDir pointing to a temp dir that has .hive/stories/
2268
+ const hiveRoot = join(tmpdir(), `hive-md-root-${Date.now()}`);
2269
+ const hiveStoriesDir = join(hiveRoot, '.hive', 'stories');
2270
+ mkdirSync(hiveStoriesDir, { recursive: true });
2271
+
2272
+ const mdScheduler = new Scheduler(db, {
2273
+ ...mockConfig,
2274
+ rootDir: hiveRoot,
2275
+ } as any);
2276
+
2277
+ // Mock spawnSenior to avoid actual tmux operations
2278
+ vi.spyOn(mdScheduler as any, 'sendAssignmentHandoff').mockResolvedValue(undefined);
2279
+
2280
+ const result = await mdScheduler.assignStories();
2281
+
2282
+ expect(result.assigned).toBe(1);
2283
+
2284
+ // Verify markdown file was written
2285
+ const mdPath = join(hiveStoriesDir, `${story.id}.md`);
2286
+ expect(existsSync(mdPath)).toBe(true);
2287
+
2288
+ // Verify markdown_path was set in DB
2289
+ const updatedStory = getStoryById(db, story.id);
2290
+ expect(updatedStory?.markdown_path).toBe(mdPath);
2291
+ expect(updatedStory?.status).toBe('in_progress');
2292
+
2293
+ rmSync(hiveRoot, { recursive: true, force: true });
2294
+ });
2295
+ });
2296
+
2173
2297
  describe('Scheduler Target Branch Propagation', () => {
2174
2298
  it('should retrieve target_branch from requirement when creating story', () => {
2175
2299
  const team = createTeam(db, {
@@ -1,5 +1,6 @@
1
1
  // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
2
 
3
+ import { join } from 'path';
3
4
  import type { Database } from 'sql.js';
4
5
  import {
5
6
  getCliRuntimeBuilder,
@@ -45,7 +46,9 @@ import {
45
46
  spawnTmuxSession,
46
47
  startManager,
47
48
  } from '../tmux/manager.js';
49
+ import { getTechLeadSessionName } from '../utils/instance.js';
48
50
  import * as logger from '../utils/logger.js';
51
+ import { getHivePaths } from '../utils/paths.js';
49
52
  import { selectAgentWithLeastWorkload } from './agent-selector.js';
50
53
  import { getCapacityPoints, selectStoriesForCapacity } from './capacity-planner.js';
51
54
  import { areDependenciesSatisfied, topologicalSort } from './dependency-resolver.js';
@@ -104,6 +107,10 @@ export class Scheduler {
104
107
  this.pmQueue = new PMOperationQueue();
105
108
  }
106
109
 
110
+ private get storiesDir(): string {
111
+ return getHivePaths(this.config.rootDir).storiesDir;
112
+ }
113
+
107
114
  /**
108
115
  * Wait for all pending Jira operations to complete.
109
116
  * Call this before closing the database to prevent "Database closed" errors.
@@ -243,7 +250,12 @@ export class Scheduler {
243
250
  const activeSeniors = getAgentsByTeam(this.db, teamId).filter(
244
251
  a => a.type === 'senior' && a.status !== 'terminated'
245
252
  );
246
- const seniorSessionPrefix = generateSessionName('senior', team.name);
253
+ const seniorSessionPrefix = generateSessionName(
254
+ 'senior',
255
+ team.name,
256
+ undefined,
257
+ join(this.config.rootDir, '.hive')
258
+ );
247
259
  const indexedSeniorSessions = activeSeniors
248
260
  .map(senior => {
249
261
  if (!senior.tmux_session) return null;
@@ -389,10 +401,15 @@ export class Scheduler {
389
401
  await withTransaction(
390
402
  this.db,
391
403
  () => {
392
- updateStory(this.db, story.id, {
393
- assignedAgentId: targetAgent.id,
394
- status: 'in_progress',
395
- });
404
+ updateStory(
405
+ this.db,
406
+ story.id,
407
+ {
408
+ assignedAgentId: targetAgent.id,
409
+ status: 'in_progress',
410
+ },
411
+ this.storiesDir
412
+ );
396
413
 
397
414
  updateAgent(this.db, targetAgent.id, {
398
415
  status: 'working',
@@ -544,10 +561,15 @@ export class Scheduler {
544
561
 
545
562
  if (subtask) {
546
563
  // Persist subtask reference back to the story
547
- updateStory(this.db, freshStory.id, {
548
- externalSubtaskKey: subtask.key,
549
- externalSubtaskId: subtask.id,
550
- });
564
+ updateStory(
565
+ this.db,
566
+ freshStory.id,
567
+ {
568
+ externalSubtaskKey: subtask.key,
569
+ externalSubtaskId: subtask.id,
570
+ },
571
+ this.storiesDir
572
+ );
551
573
  if (this.saveFn) this.saveFn();
552
574
 
553
575
  logger.info(`Created subtask ${subtask.key} for story ${freshStory.id}`);
@@ -670,7 +692,7 @@ export class Scheduler {
670
692
  `
671
693
  );
672
694
 
673
- const liveSessions = await getHiveSessions();
695
+ const liveSessions = await getHiveSessions(join(this.config.rootDir, '.hive'));
674
696
  const liveSessionNames = new Set(liveSessions.map(s => s.name));
675
697
 
676
698
  let terminated = 0;
@@ -704,10 +726,15 @@ export class Scheduler {
704
726
 
705
727
  // If agent was working on a story, mark it for reassignment
706
728
  if (agent.current_story_id) {
707
- updateStory(this.db, agent.current_story_id, {
708
- status: 'planned',
709
- assignedAgentId: null,
710
- });
729
+ updateStory(
730
+ this.db,
731
+ agent.current_story_id,
732
+ {
733
+ status: 'planned',
734
+ assignedAgentId: null,
735
+ },
736
+ this.storiesDir
737
+ );
711
738
  revived.push(agent.current_story_id);
712
739
 
713
740
  // Sync status change to Jira (fire and forget)
@@ -717,7 +744,11 @@ export class Scheduler {
717
744
  }
718
745
 
719
746
  // Detect and recover orphaned stories (assigned to terminated agents)
720
- const orphanedRecovered = detectAndRecoverOrphanedStories(this.db, this.config.rootDir);
747
+ const orphanedRecovered = detectAndRecoverOrphanedStories(
748
+ this.db,
749
+ this.config.rootDir,
750
+ this.storiesDir
751
+ );
721
752
 
722
753
  return { terminated, revived, orphanedRecovered };
723
754
  }
@@ -885,8 +916,9 @@ export class Scheduler {
885
916
  }
886
917
 
887
918
  private async ensureManagerRunning(): Promise<void> {
888
- if (!(await isManagerRunning())) {
889
- await startManager(DEFAULT_MANAGER_INTERVAL_SECONDS);
919
+ const hiveDir = join(this.config.rootDir, '.hive');
920
+ if (!(await isManagerRunning(hiveDir))) {
921
+ await startManager(DEFAULT_MANAGER_INTERVAL_SECONDS, hiveDir);
890
922
  }
891
923
  }
892
924
 
@@ -907,10 +939,11 @@ export class Scheduler {
907
939
  }
908
940
  ): Promise<AgentRow> {
909
941
  // Auditor uses a timestamp-based session name since it's ephemeral
942
+ const hiveDir = join(this.config.rootDir, '.hive');
910
943
  const sessionName =
911
944
  type === 'auditor'
912
- ? `hive-auditor-${Date.now()}`
913
- : generateSessionName(type, teamName, index);
945
+ ? generateSessionName('auditor', `${Date.now()}`, undefined, hiveDir)
946
+ : generateSessionName(type, teamName, index, hiveDir);
914
947
 
915
948
  // Prevent creating duplicate agents on same tmux session (for senior agents)
916
949
  if (type === 'senior') {
@@ -997,6 +1030,10 @@ export class Scheduler {
997
1030
  // Build the initial prompt for this agent type
998
1031
  const team = getTeamById(this.db, teamId);
999
1032
  const includeProgressUpdates = this.shouldIncludeProgressUpdates();
1033
+ const hiveDir = join(this.config.rootDir, '.hive');
1034
+ const techLeadSession = getTechLeadSessionName(hiveDir);
1035
+ const chromeEnabled =
1036
+ this.config.hiveConfig?.agents?.chrome_enabled === true && cliTool === 'claude';
1000
1037
  let prompt: string;
1001
1038
 
1002
1039
  if (type === 'senior') {
@@ -1007,7 +1044,7 @@ export class Scheduler {
1007
1044
  worktreePath,
1008
1045
  stories,
1009
1046
  targetBranch,
1010
- { includeProgressUpdates },
1047
+ { includeProgressUpdates, techLeadSession, chromeEnabled },
1011
1048
  sessionName
1012
1049
  );
1013
1050
  } else if (type === 'intermediate') {
@@ -1017,7 +1054,7 @@ export class Scheduler {
1017
1054
  worktreePath,
1018
1055
  sessionName,
1019
1056
  targetBranch,
1020
- { includeProgressUpdates }
1057
+ { includeProgressUpdates, techLeadSession, chromeEnabled }
1021
1058
  );
1022
1059
  } else if (type === 'junior') {
1023
1060
  prompt = generateJuniorPrompt(
@@ -1026,7 +1063,7 @@ export class Scheduler {
1026
1063
  worktreePath,
1027
1064
  sessionName,
1028
1065
  targetBranch,
1029
- { includeProgressUpdates }
1066
+ { includeProgressUpdates, techLeadSession, chromeEnabled }
1030
1067
  );
1031
1068
  } else if (type === 'feature_test' && featureTestContext) {
1032
1069
  prompt = generateFeatureTestPrompt(
@@ -1037,23 +1074,25 @@ export class Scheduler {
1037
1074
  featureTestContext.featureBranch,
1038
1075
  featureTestContext.requirementId,
1039
1076
  featureTestContext.e2eTestsPath,
1040
- { includeProgressUpdates }
1077
+ { includeProgressUpdates, techLeadSession, chromeEnabled }
1041
1078
  );
1042
1079
  } else if (type === 'auditor') {
1043
- prompt = generateAuditorPrompt(sessionName, worktreePath, team?.repo_url || '');
1080
+ prompt = generateAuditorPrompt(sessionName, worktreePath, team?.repo_url || '', {
1081
+ techLeadSession,
1082
+ chromeEnabled,
1083
+ });
1044
1084
  } else {
1045
1085
  prompt = generateQAPrompt(
1046
1086
  teamName,
1047
1087
  team?.repo_url || '',
1048
1088
  worktreePath,
1049
1089
  sessionName,
1050
- targetBranch
1090
+ targetBranch,
1091
+ { chromeEnabled }
1051
1092
  );
1052
1093
  }
1053
1094
 
1054
1095
  // Build CLI command using the configured runtime
1055
- const chromeEnabled =
1056
- this.config.hiveConfig?.agents?.chrome_enabled === true && cliTool === 'claude';
1057
1096
  const commandArgs = getCliRuntimeBuilder(cliTool).buildSpawnCommand(
1058
1097
  runtimeModel,
1059
1098
  safetyMode,
@@ -4,6 +4,11 @@ import { execa } from 'execa';
4
4
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'fs';
5
5
  import { tmpdir } from 'os';
6
6
  import { dirname, isAbsolute, join, resolve } from 'path';
7
+ import {
8
+ buildInstanceSessionName,
9
+ getInstancePrefix,
10
+ getManagerSessionName,
11
+ } from '../utils/instance.js';
7
12
 
8
13
  // --- Named constants (extracted from inline magic numbers) ---
9
14
 
@@ -162,8 +167,12 @@ export async function listTmuxSessions(): Promise<TmuxSession[]> {
162
167
  }
163
168
  }
164
169
 
165
- export async function getHiveSessions(): Promise<TmuxSession[]> {
170
+ export async function getHiveSessions(hiveDir?: string): Promise<TmuxSession[]> {
166
171
  const sessions = await listTmuxSessions();
172
+ if (hiveDir) {
173
+ const prefix = getInstancePrefix(hiveDir);
174
+ return sessions.filter(s => s.name.startsWith(prefix));
175
+ }
167
176
  return sessions.filter(s => s.name.startsWith('hive-'));
168
177
  }
169
178
 
@@ -235,8 +244,8 @@ export async function killTmuxSession(sessionName: string): Promise<void> {
235
244
  }
236
245
  }
237
246
 
238
- export async function killAllHiveSessions(): Promise<number> {
239
- const sessions = await getHiveSessions();
247
+ export async function killAllHiveSessions(hiveDir?: string): Promise<number> {
248
+ const sessions = await getHiveSessions(hiveDir);
240
249
  let killed = 0;
241
250
 
242
251
  for (const session of sessions) {
@@ -513,7 +522,15 @@ export async function autoApprovePermission(
513
522
  return false;
514
523
  }
515
524
 
516
- export function generateSessionName(agentType: string, teamName?: string, index?: number): string {
525
+ export function generateSessionName(
526
+ agentType: string,
527
+ teamName?: string,
528
+ index?: number,
529
+ hiveDir?: string
530
+ ): string {
531
+ if (hiveDir) {
532
+ return buildInstanceSessionName(hiveDir, agentType, teamName, index);
533
+ }
517
534
  let name = `hive-${agentType}`;
518
535
  if (teamName) {
519
536
  name += `-${teamName}`;
@@ -526,12 +543,23 @@ export function generateSessionName(agentType: string, teamName?: string, index?
526
543
 
527
544
  const MANAGER_SESSION = 'hive-manager';
528
545
 
529
- export async function isManagerRunning(): Promise<boolean> {
530
- return isTmuxSessionRunning(MANAGER_SESSION);
546
+ export function getManagerSession(hiveDir?: string): string {
547
+ if (hiveDir) {
548
+ return getManagerSessionName(hiveDir);
549
+ }
550
+ return MANAGER_SESSION;
551
+ }
552
+
553
+ export async function isManagerRunning(hiveDir?: string): Promise<boolean> {
554
+ return isTmuxSessionRunning(getManagerSession(hiveDir));
531
555
  }
532
556
 
533
- export async function startManager(interval = DEFAULT_MANAGER_INTERVAL): Promise<boolean> {
534
- if (await isManagerRunning()) {
557
+ export async function startManager(
558
+ interval = DEFAULT_MANAGER_INTERVAL,
559
+ hiveDir?: string
560
+ ): Promise<boolean> {
561
+ const session = getManagerSession(hiveDir);
562
+ if (await isTmuxSessionRunning(session)) {
535
563
  return false; // Already running
536
564
  }
537
565
 
@@ -539,22 +567,23 @@ export async function startManager(interval = DEFAULT_MANAGER_INTERVAL): Promise
539
567
  const sessionEnv = buildTmuxSessionEnv(workDir);
540
568
 
541
569
  // Start the manager in a detached tmux session
542
- await execa('tmux', ['new-session', '-d', '-s', MANAGER_SESSION, '-c', workDir], {
570
+ await execa('tmux', ['new-session', '-d', '-s', session, '-c', workDir], {
543
571
  env: sessionEnv,
544
572
  });
545
573
 
546
574
  // Send the manager command
547
575
  const managerCommand = `${buildHiveInvokeCommand()} manager start -i ${interval}`;
548
- await execa('tmux', ['send-keys', '-t', MANAGER_SESSION, managerCommand, 'Enter']);
576
+ await execa('tmux', ['send-keys', '-t', session, managerCommand, 'Enter']);
549
577
 
550
578
  return true;
551
579
  }
552
580
 
553
- export async function stopManager(): Promise<boolean> {
554
- if (!(await isManagerRunning())) {
581
+ export async function stopManager(hiveDir?: string): Promise<boolean> {
582
+ const session = getManagerSession(hiveDir);
583
+ if (!(await isTmuxSessionRunning(session))) {
555
584
  return false; // Not running
556
585
  }
557
586
 
558
- await killTmuxSession(MANAGER_SESSION);
587
+ await killTmuxSession(session);
559
588
  return true;
560
589
  }
@@ -0,0 +1,129 @@
1
+ // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
+
3
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import {
8
+ buildInstanceSessionName,
9
+ getInstanceId,
10
+ getInstancePrefix,
11
+ getManagerLockPath,
12
+ getManagerSessionName,
13
+ getTechLeadSessionName,
14
+ } from './instance.js';
15
+
16
+ describe('instance', () => {
17
+ let testHiveDir: string;
18
+
19
+ beforeEach(() => {
20
+ testHiveDir = join(tmpdir(), `hive-test-instance-${Date.now()}`);
21
+ mkdirSync(testHiveDir, { recursive: true });
22
+ });
23
+
24
+ afterEach(() => {
25
+ rmSync(testHiveDir, { recursive: true, force: true });
26
+ });
27
+
28
+ describe('getInstanceId', () => {
29
+ it('should return null if hive directory does not exist', () => {
30
+ const result = getInstanceId('/nonexistent/path/.hive');
31
+ expect(result).toBeNull();
32
+ });
33
+
34
+ it('should create and persist an instance ID', () => {
35
+ const id = getInstanceId(testHiveDir);
36
+ expect(id).toBeTruthy();
37
+ expect(typeof id).toBe('string');
38
+ expect(id!.length).toBe(6);
39
+
40
+ // Should be persisted
41
+ const fileContent = readFileSync(join(testHiveDir, 'instance.id'), 'utf-8').trim();
42
+ expect(fileContent).toBe(id);
43
+ });
44
+
45
+ it('should return the same ID on subsequent calls', () => {
46
+ const id1 = getInstanceId(testHiveDir);
47
+ const id2 = getInstanceId(testHiveDir);
48
+ expect(id1).toBe(id2);
49
+ });
50
+
51
+ it('should read existing instance ID from file', () => {
52
+ writeFileSync(join(testHiveDir, 'instance.id'), 'testid', 'utf-8');
53
+ const id = getInstanceId(testHiveDir);
54
+ expect(id).toBe('testid');
55
+ });
56
+ });
57
+
58
+ describe('getInstancePrefix', () => {
59
+ it('should return hive-<instanceId> when hive dir exists', () => {
60
+ writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
61
+ const prefix = getInstancePrefix(testHiveDir);
62
+ expect(prefix).toBe('hive-abc123');
63
+ });
64
+
65
+ it('should fall back to hive when hive dir does not exist', () => {
66
+ const prefix = getInstancePrefix('/nonexistent/path/.hive');
67
+ expect(prefix).toBe('hive');
68
+ });
69
+ });
70
+
71
+ describe('buildInstanceSessionName', () => {
72
+ it('should build instance-scoped session name', () => {
73
+ writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
74
+ const name = buildInstanceSessionName(testHiveDir, 'senior', 'my-team');
75
+ expect(name).toBe('hive-abc123-senior-my-team');
76
+ });
77
+
78
+ it('should include index when > 1', () => {
79
+ writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
80
+ const name = buildInstanceSessionName(testHiveDir, 'senior', 'my-team', 3);
81
+ expect(name).toBe('hive-abc123-senior-my-team-3');
82
+ });
83
+
84
+ it('should omit index when 1', () => {
85
+ writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
86
+ const name = buildInstanceSessionName(testHiveDir, 'senior', 'my-team', 1);
87
+ expect(name).toBe('hive-abc123-senior-my-team');
88
+ });
89
+
90
+ it('should fall back to old format when no hive dir', () => {
91
+ const name = buildInstanceSessionName('/nonexistent', 'senior', 'my-team');
92
+ expect(name).toBe('hive-senior-my-team');
93
+ });
94
+ });
95
+
96
+ describe('getTechLeadSessionName', () => {
97
+ it('should return instance-scoped tech lead session name', () => {
98
+ writeFileSync(join(testHiveDir, 'instance.id'), 'xyz789', 'utf-8');
99
+ const name = getTechLeadSessionName(testHiveDir);
100
+ expect(name).toBe('hive-xyz789-tech-lead');
101
+ });
102
+
103
+ it('should fall back to hive-tech-lead when no instance', () => {
104
+ const name = getTechLeadSessionName('/nonexistent');
105
+ expect(name).toBe('hive-tech-lead');
106
+ });
107
+ });
108
+
109
+ describe('getManagerSessionName', () => {
110
+ it('should return instance-scoped manager session name', () => {
111
+ writeFileSync(join(testHiveDir, 'instance.id'), 'xyz789', 'utf-8');
112
+ const name = getManagerSessionName(testHiveDir);
113
+ expect(name).toBe('hive-xyz789-manager');
114
+ });
115
+ });
116
+
117
+ describe('getManagerLockPath', () => {
118
+ it('should return instance-scoped lock path', () => {
119
+ writeFileSync(join(testHiveDir, 'instance.id'), 'xyz789', 'utf-8');
120
+ const lockPath = getManagerLockPath(testHiveDir);
121
+ expect(lockPath).toBe(join(testHiveDir, 'manager-xyz789.lock'));
122
+ });
123
+
124
+ it('should fall back to default lock path when no instance', () => {
125
+ const lockPath = getManagerLockPath('/nonexistent');
126
+ expect(lockPath).toBe(join('/nonexistent', 'manager.lock'));
127
+ });
128
+ });
129
+ });