hungry-ghost-hive 0.45.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 (113) hide show
  1. package/dist/cli/commands/cluster.d.ts.map +1 -1
  2. package/dist/cli/commands/cluster.js +348 -1
  3. package/dist/cli/commands/cluster.js.map +1 -1
  4. package/dist/cli/commands/cluster.test.js +313 -9
  5. package/dist/cli/commands/cluster.test.js.map +1 -1
  6. package/dist/cli/commands/req-spawn.test.d.ts +2 -0
  7. package/dist/cli/commands/req-spawn.test.d.ts.map +1 -0
  8. package/dist/cli/commands/req-spawn.test.js +116 -0
  9. package/dist/cli/commands/req-spawn.test.js.map +1 -0
  10. package/dist/cli/commands/req.d.ts.map +1 -1
  11. package/dist/cli/commands/req.js +21 -13
  12. package/dist/cli/commands/req.js.map +1 -1
  13. package/dist/cluster/cluster-http-server.d.ts +32 -0
  14. package/dist/cluster/cluster-http-server.d.ts.map +1 -1
  15. package/dist/cluster/cluster-http-server.js +42 -0
  16. package/dist/cluster/cluster-http-server.js.map +1 -1
  17. package/dist/cluster/distributed-runtime-coverage.test.js +9 -0
  18. package/dist/cluster/distributed-runtime-coverage.test.js.map +1 -1
  19. package/dist/cluster/distributed-system.test.js +135 -0
  20. package/dist/cluster/distributed-system.test.js.map +1 -1
  21. package/dist/cluster/events.d.ts +23 -0
  22. package/dist/cluster/events.d.ts.map +1 -1
  23. package/dist/cluster/events.js +74 -0
  24. package/dist/cluster/events.js.map +1 -1
  25. package/dist/cluster/heartbeat-manager.d.ts +2 -0
  26. package/dist/cluster/heartbeat-manager.d.ts.map +1 -1
  27. package/dist/cluster/heartbeat-manager.js +42 -6
  28. package/dist/cluster/heartbeat-manager.js.map +1 -1
  29. package/dist/cluster/membership.test.d.ts +2 -0
  30. package/dist/cluster/membership.test.d.ts.map +1 -0
  31. package/dist/cluster/membership.test.js +416 -0
  32. package/dist/cluster/membership.test.js.map +1 -0
  33. package/dist/cluster/partition-safety.test.d.ts +2 -0
  34. package/dist/cluster/partition-safety.test.d.ts.map +1 -0
  35. package/dist/cluster/partition-safety.test.js +440 -0
  36. package/dist/cluster/partition-safety.test.js.map +1 -0
  37. package/dist/cluster/raft-state-machine.d.ts +33 -1
  38. package/dist/cluster/raft-state-machine.d.ts.map +1 -1
  39. package/dist/cluster/raft-state-machine.js +65 -3
  40. package/dist/cluster/raft-state-machine.js.map +1 -1
  41. package/dist/cluster/raft-store.d.ts +26 -1
  42. package/dist/cluster/raft-store.d.ts.map +1 -1
  43. package/dist/cluster/raft-store.js +137 -0
  44. package/dist/cluster/raft-store.js.map +1 -1
  45. package/dist/cluster/replication-lag.test.d.ts +2 -0
  46. package/dist/cluster/replication-lag.test.d.ts.map +1 -0
  47. package/dist/cluster/replication-lag.test.js +239 -0
  48. package/dist/cluster/replication-lag.test.js.map +1 -0
  49. package/dist/cluster/replication.d.ts +2 -2
  50. package/dist/cluster/replication.d.ts.map +1 -1
  51. package/dist/cluster/replication.js +1 -1
  52. package/dist/cluster/replication.js.map +1 -1
  53. package/dist/cluster/runtime.d.ts +78 -0
  54. package/dist/cluster/runtime.d.ts.map +1 -1
  55. package/dist/cluster/runtime.js +400 -13
  56. package/dist/cluster/runtime.js.map +1 -1
  57. package/dist/cluster/state-recovery.test.d.ts +2 -0
  58. package/dist/cluster/state-recovery.test.d.ts.map +1 -0
  59. package/dist/cluster/state-recovery.test.js +310 -0
  60. package/dist/cluster/state-recovery.test.js.map +1 -0
  61. package/dist/cluster/types.d.ts +30 -0
  62. package/dist/cluster/types.d.ts.map +1 -1
  63. package/dist/config/schema.d.ts +48 -0
  64. package/dist/config/schema.d.ts.map +1 -1
  65. package/dist/config/schema.js +11 -0
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/context-files/generator.js +1 -1
  68. package/dist/context-files/generator.js.map +1 -1
  69. package/dist/context-files/generator.test.js +51 -0
  70. package/dist/context-files/generator.test.js.map +1 -1
  71. package/dist/orchestrator/orphan-recovery.d.ts +1 -1
  72. package/dist/orchestrator/orphan-recovery.d.ts.map +1 -1
  73. package/dist/orchestrator/orphan-recovery.js +4 -4
  74. package/dist/orchestrator/orphan-recovery.js.map +1 -1
  75. package/dist/orchestrator/prompt-templates.d.ts +3 -1
  76. package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
  77. package/dist/orchestrator/prompt-templates.js +45 -8
  78. package/dist/orchestrator/prompt-templates.js.map +1 -1
  79. package/dist/orchestrator/prompt-templates.test.js +210 -0
  80. package/dist/orchestrator/prompt-templates.test.js.map +1 -1
  81. package/dist/orchestrator/scheduler.d.ts +1 -0
  82. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  83. package/dist/orchestrator/scheduler.js +15 -10
  84. package/dist/orchestrator/scheduler.js.map +1 -1
  85. package/dist/orchestrator/scheduler.test.js +97 -6
  86. package/dist/orchestrator/scheduler.test.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/cli/commands/cluster.test.ts +387 -9
  89. package/src/cli/commands/cluster.ts +486 -1
  90. package/src/cli/commands/req-spawn.test.ts +153 -0
  91. package/src/cli/commands/req.ts +31 -18
  92. package/src/cluster/cluster-http-server.ts +80 -0
  93. package/src/cluster/distributed-runtime-coverage.test.ts +9 -0
  94. package/src/cluster/distributed-system.test.ts +168 -0
  95. package/src/cluster/events.ts +90 -0
  96. package/src/cluster/heartbeat-manager.ts +48 -6
  97. package/src/cluster/membership.test.ts +498 -0
  98. package/src/cluster/partition-safety.test.ts +523 -0
  99. package/src/cluster/raft-state-machine.ts +76 -4
  100. package/src/cluster/raft-store.ts +167 -1
  101. package/src/cluster/replication-lag.test.ts +284 -0
  102. package/src/cluster/replication.ts +6 -0
  103. package/src/cluster/runtime.ts +551 -12
  104. package/src/cluster/state-recovery.test.ts +420 -0
  105. package/src/cluster/types.ts +32 -0
  106. package/src/config/schema.ts +11 -0
  107. package/src/context-files/generator.test.ts +55 -0
  108. package/src/context-files/generator.ts +5 -5
  109. package/src/orchestrator/orphan-recovery.ts +32 -13
  110. package/src/orchestrator/prompt-templates.test.ts +263 -0
  111. package/src/orchestrator/prompt-templates.ts +49 -8
  112. package/src/orchestrator/scheduler.test.ts +129 -6
  113. package/src/orchestrator/scheduler.ts +46 -20
@@ -164,6 +164,82 @@ describe('Prompt Templates', () => {
164
164
  expect(prompt).toContain('complexity: ?');
165
165
  });
166
166
 
167
+ it('should include markdown_path in prompt when set on a story', () => {
168
+ const stories: StoryRow[] = [
169
+ {
170
+ id: 'STORY-003',
171
+ title: 'Story With Markdown',
172
+ description: 'DB description',
173
+ complexity_score: 3,
174
+ status: 'planned',
175
+ team_id: 'team-1',
176
+ requirement_id: null,
177
+ acceptance_criteria: null,
178
+ story_points: null,
179
+ assigned_agent_id: null,
180
+ branch_name: null,
181
+ pr_url: null,
182
+ jira_issue_key: null,
183
+ jira_issue_id: null,
184
+ jira_project_key: null,
185
+ jira_subtask_key: null,
186
+ jira_subtask_id: null,
187
+ external_issue_key: null,
188
+ external_issue_id: null,
189
+ external_project_key: null,
190
+ external_subtask_key: null,
191
+ external_subtask_id: null,
192
+ external_provider: null,
193
+ in_sprint: 0,
194
+ markdown_path: '/stories/STORY-003.md',
195
+ created_at: '2024-01-01',
196
+ updated_at: '2024-01-01',
197
+ },
198
+ ];
199
+ const prompt = generateSeniorPrompt(teamName, repoUrl, repoPath, stories);
200
+
201
+ expect(prompt).toContain('/stories/STORY-003.md');
202
+ expect(prompt).toContain('read the story markdown file');
203
+ });
204
+
205
+ it('should fall back to DB description when markdown_path is null', () => {
206
+ const stories: StoryRow[] = [
207
+ {
208
+ id: 'STORY-004',
209
+ title: 'Story Without Markdown',
210
+ description: 'DB only description',
211
+ complexity_score: 2,
212
+ status: 'planned',
213
+ team_id: 'team-1',
214
+ requirement_id: null,
215
+ acceptance_criteria: null,
216
+ story_points: null,
217
+ assigned_agent_id: null,
218
+ branch_name: null,
219
+ pr_url: null,
220
+ jira_issue_key: null,
221
+ jira_issue_id: null,
222
+ jira_project_key: null,
223
+ jira_subtask_key: null,
224
+ jira_subtask_id: null,
225
+ external_issue_key: null,
226
+ external_issue_id: null,
227
+ external_project_key: null,
228
+ external_subtask_key: null,
229
+ external_subtask_id: null,
230
+ external_provider: null,
231
+ in_sprint: 0,
232
+ markdown_path: null,
233
+ created_at: '2024-01-01',
234
+ updated_at: '2024-01-01',
235
+ },
236
+ ];
237
+ const prompt = generateSeniorPrompt(teamName, repoUrl, repoPath, stories);
238
+
239
+ expect(prompt).toContain('DB only description');
240
+ expect(prompt).not.toContain('read the story markdown file');
241
+ });
242
+
167
243
  it('should handle empty stories list', () => {
168
244
  const stories: StoryRow[] = [];
169
245
  const prompt = generateSeniorPrompt(teamName, repoUrl, repoPath, stories);
@@ -814,3 +890,190 @@ describe('Prompt Templates', () => {
814
890
  });
815
891
  });
816
892
  });
893
+
894
+ describe('Chrome Tab Isolation', () => {
895
+ const teamName = 'TestTeam';
896
+ const repoUrl = 'https://github.com/test/repo.git';
897
+ const repoPath = 'repos/test-repo';
898
+ const sessionName = 'hive-test-session';
899
+
900
+ const CHROME_TAB_MARKER = 'Chrome Browser Tab Isolation';
901
+ const CHROME_TAB_CREATE = 'tabs_create_mcp';
902
+
903
+ describe('generateSeniorPrompt', () => {
904
+ it('should include chrome tab isolation section when chromeEnabled is true', () => {
905
+ const prompt = generateSeniorPrompt(teamName, repoUrl, repoPath, [], 'main', {
906
+ chromeEnabled: true,
907
+ });
908
+ expect(prompt).toContain(CHROME_TAB_MARKER);
909
+ expect(prompt).toContain(CHROME_TAB_CREATE);
910
+ });
911
+
912
+ it('should not include chrome tab isolation section when chromeEnabled is false', () => {
913
+ const prompt = generateSeniorPrompt(teamName, repoUrl, repoPath, [], 'main', {
914
+ chromeEnabled: false,
915
+ });
916
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
917
+ });
918
+
919
+ it('should not include chrome tab isolation section when chromeEnabled is not set', () => {
920
+ const prompt = generateSeniorPrompt(teamName, repoUrl, repoPath, []);
921
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
922
+ });
923
+ });
924
+
925
+ describe('generateIntermediatePrompt', () => {
926
+ it('should include chrome tab isolation section when chromeEnabled is true', () => {
927
+ const prompt = generateIntermediatePrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
928
+ chromeEnabled: true,
929
+ });
930
+ expect(prompt).toContain(CHROME_TAB_MARKER);
931
+ expect(prompt).toContain(CHROME_TAB_CREATE);
932
+ });
933
+
934
+ it('should not include chrome tab isolation section when chromeEnabled is false', () => {
935
+ const prompt = generateIntermediatePrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
936
+ chromeEnabled: false,
937
+ });
938
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
939
+ });
940
+
941
+ it('should not include chrome tab isolation section when options not provided', () => {
942
+ const prompt = generateIntermediatePrompt(teamName, repoUrl, repoPath, sessionName);
943
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
944
+ });
945
+ });
946
+
947
+ describe('generateJuniorPrompt', () => {
948
+ it('should include chrome tab isolation section when chromeEnabled is true', () => {
949
+ const prompt = generateJuniorPrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
950
+ chromeEnabled: true,
951
+ });
952
+ expect(prompt).toContain(CHROME_TAB_MARKER);
953
+ expect(prompt).toContain(CHROME_TAB_CREATE);
954
+ });
955
+
956
+ it('should not include chrome tab isolation section when chromeEnabled is false', () => {
957
+ const prompt = generateJuniorPrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
958
+ chromeEnabled: false,
959
+ });
960
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
961
+ });
962
+
963
+ it('should not include chrome tab isolation section when options not provided', () => {
964
+ const prompt = generateJuniorPrompt(teamName, repoUrl, repoPath, sessionName);
965
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
966
+ });
967
+ });
968
+
969
+ describe('generateQAPrompt', () => {
970
+ it('should include chrome tab isolation section when chromeEnabled is true', () => {
971
+ const prompt = generateQAPrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
972
+ chromeEnabled: true,
973
+ });
974
+ expect(prompt).toContain(CHROME_TAB_MARKER);
975
+ expect(prompt).toContain(CHROME_TAB_CREATE);
976
+ });
977
+
978
+ it('should not include chrome tab isolation section when chromeEnabled is false', () => {
979
+ const prompt = generateQAPrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
980
+ chromeEnabled: false,
981
+ });
982
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
983
+ });
984
+
985
+ it('should not include chrome tab isolation section when options not provided', () => {
986
+ const prompt = generateQAPrompt(teamName, repoUrl, repoPath, sessionName);
987
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
988
+ });
989
+ });
990
+
991
+ describe('generateFeatureTestPrompt', () => {
992
+ it('should include chrome tab isolation section when chromeEnabled is true', () => {
993
+ const prompt = generateFeatureTestPrompt(
994
+ teamName,
995
+ repoUrl,
996
+ repoPath,
997
+ sessionName,
998
+ 'feature/test',
999
+ 'REQ-001',
1000
+ 'e2e/tests',
1001
+ { chromeEnabled: true }
1002
+ );
1003
+ expect(prompt).toContain(CHROME_TAB_MARKER);
1004
+ expect(prompt).toContain(CHROME_TAB_CREATE);
1005
+ });
1006
+
1007
+ it('should not include chrome tab isolation section when chromeEnabled is false', () => {
1008
+ const prompt = generateFeatureTestPrompt(
1009
+ teamName,
1010
+ repoUrl,
1011
+ repoPath,
1012
+ sessionName,
1013
+ 'feature/test',
1014
+ 'REQ-001',
1015
+ 'e2e/tests',
1016
+ { chromeEnabled: false }
1017
+ );
1018
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
1019
+ });
1020
+
1021
+ it('should not include chrome tab isolation section when options not provided', () => {
1022
+ const prompt = generateFeatureTestPrompt(
1023
+ teamName,
1024
+ repoUrl,
1025
+ repoPath,
1026
+ sessionName,
1027
+ 'feature/test',
1028
+ 'REQ-001',
1029
+ 'e2e/tests'
1030
+ );
1031
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
1032
+ });
1033
+ });
1034
+
1035
+ describe('generateAuditorPrompt', () => {
1036
+ it('should include chrome tab isolation section when chromeEnabled is true', () => {
1037
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl, {
1038
+ chromeEnabled: true,
1039
+ });
1040
+ expect(prompt).toContain(CHROME_TAB_MARKER);
1041
+ expect(prompt).toContain(CHROME_TAB_CREATE);
1042
+ });
1043
+
1044
+ it('should not include chrome tab isolation section when chromeEnabled is false', () => {
1045
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl, {
1046
+ chromeEnabled: false,
1047
+ });
1048
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
1049
+ });
1050
+
1051
+ it('should not include chrome tab isolation section when options not provided', () => {
1052
+ const prompt = generateAuditorPrompt(sessionName, repoPath, repoUrl);
1053
+ expect(prompt).not.toContain(CHROME_TAB_MARKER);
1054
+ });
1055
+ });
1056
+
1057
+ describe('tab isolation instructions content', () => {
1058
+ it('should instruct agents to store and reuse tab ID', () => {
1059
+ const prompt = generateIntermediatePrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
1060
+ chromeEnabled: true,
1061
+ });
1062
+ expect(prompt).toContain('tab ID');
1063
+ });
1064
+
1065
+ it('should instruct agents to recreate tab when closed externally', () => {
1066
+ const prompt = generateIntermediatePrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
1067
+ chromeEnabled: true,
1068
+ });
1069
+ expect(prompt).toContain('closed externally');
1070
+ });
1071
+
1072
+ it('should warn agents not to use other agents tabs', () => {
1073
+ const prompt = generateIntermediatePrompt(teamName, repoUrl, repoPath, sessionName, 'main', {
1074
+ chromeEnabled: true,
1075
+ });
1076
+ expect(prompt).toContain('Never interact with tabs you did not create');
1077
+ });
1078
+ });
1079
+ });
@@ -6,6 +6,8 @@ export interface AgentPromptOptions {
6
6
  includeProgressUpdates?: boolean;
7
7
  /** The tech lead tmux session name for messaging. Defaults to 'hive-tech-lead' for backwards compatibility. */
8
8
  techLeadSession?: string;
9
+ /** Whether Chrome browser tools are enabled for this agent session. */
10
+ chromeEnabled?: boolean;
9
11
  }
10
12
 
11
13
  /**
@@ -66,6 +68,35 @@ function resolveTechLeadSession(options?: AgentPromptOptions): string {
66
68
  return options?.techLeadSession || 'hive-tech-lead';
67
69
  }
68
70
 
71
+ /**
72
+ * Generate the Chrome tab isolation section for agent prompts.
73
+ * Instructs agents to create a dedicated tab at session start and use it
74
+ * exclusively for all browser operations, preventing tab interference between
75
+ * concurrent agents.
76
+ */
77
+ function chromeTabIsolationSection(): string {
78
+ return `# Chrome Browser Tab Isolation
79
+ Chrome browser tools are enabled for this session. Each agent must use its own dedicated tab to prevent interference with other agents running concurrently.
80
+
81
+ ## Tab Lifecycle
82
+
83
+ **At session start**, create your dedicated tab immediately:
84
+ \`\`\`
85
+ Use mcp__claude-in-chrome__tabs_create_mcp to create a new tab.
86
+ Store the returned tab ID — you will use it for all browser operations.
87
+ \`\`\`
88
+
89
+ **For all browser operations**, always pass your stored tab ID to every \`mcp__claude-in-chrome__*\` tool call. Never interact with tabs you did not create.
90
+
91
+ **If your tab is closed externally** (tool returns a tab-not-found error):
92
+ \`\`\`
93
+ Call mcp__claude-in-chrome__tabs_create_mcp again to get a new tab ID.
94
+ Update your stored tab ID and continue.
95
+ \`\`\`
96
+
97
+ **At session end**, close your tab using the browser tools to free resources.`;
98
+ }
99
+
69
100
  function repositorySection(repoPath: string, repoUrl: string): string {
70
101
  return `## Your Repository
71
102
  - Local path: ${repoPath}
@@ -243,15 +274,19 @@ export function generateSeniorPrompt(
243
274
  const externalInfo = s.external_subtask_key
244
275
  ? ` | External Subtask: ${s.external_subtask_key}`
245
276
  : '';
246
- return `- [${s.id}] ${s.title} (complexity: ${s.complexity_score || '?'}${externalInfo})\n ${s.description}`;
277
+ const descriptionOrMarkdown = s.markdown_path
278
+ ? `For full details, read the story markdown file: \`${s.markdown_path}\``
279
+ : s.description;
280
+ return `- [${s.id}] ${s.title} (complexity: ${s.complexity_score || '?'}${externalInfo})\n ${descriptionOrMarkdown}`;
247
281
  })
248
282
  .join('\n\n');
249
283
 
250
284
  const sessionName = sessionNameOverride || formatSeniorSessionName(teamName);
285
+ const chromeSection = options?.chromeEnabled ? '\n\n' + chromeTabIsolationSection() : '';
251
286
 
252
287
  return `You are a Senior Developer on Team ${teamName}.
253
288
  Your tmux session: ${sessionName}
254
-
289
+ ${chromeSection}
255
290
  ${repositorySection(repoPath, repoUrl)}
256
291
 
257
292
  ## Your Responsibilities
@@ -327,10 +362,11 @@ export function generateIntermediatePrompt(
327
362
  const includeProgressUpdates = shouldIncludeProgressUpdates(options);
328
363
  const techLeadSession = resolveTechLeadSession(options);
329
364
  const seniorSession = formatSeniorSessionName(teamName);
365
+ const chromeSection = options?.chromeEnabled ? '\n\n' + chromeTabIsolationSection() : '';
330
366
 
331
367
  return `You are an Intermediate Developer on Team ${teamName}.
332
368
  Your tmux session: ${sessionName}
333
-
369
+ ${chromeSection}
334
370
  ${repositorySection(repoPath, repoUrl)}
335
371
 
336
372
  ## Your Responsibilities
@@ -399,10 +435,11 @@ export function generateJuniorPrompt(
399
435
  const includeProgressUpdates = shouldIncludeProgressUpdates(options);
400
436
  const techLeadSession = resolveTechLeadSession(options);
401
437
  const seniorSession = formatSeniorSessionName(teamName);
438
+ const chromeSection = options?.chromeEnabled ? '\n\n' + chromeTabIsolationSection() : '';
402
439
 
403
440
  return `You are a Junior Developer on Team ${teamName}.
404
441
  Your tmux session: ${sessionName}
405
-
442
+ ${chromeSection}
406
443
  ${repositorySection(repoPath, repoUrl)}
407
444
 
408
445
  ## Your Responsibilities
@@ -465,11 +502,13 @@ export function generateQAPrompt(
465
502
  repoUrl: string,
466
503
  repoPath: string,
467
504
  sessionName: string,
468
- targetBranch: string = 'main'
505
+ targetBranch: string = 'main',
506
+ options?: AgentPromptOptions
469
507
  ): string {
508
+ const chromeSection = options?.chromeEnabled ? '\n\n' + chromeTabIsolationSection() : '';
470
509
  return `You are a QA Engineer on Team ${teamName}.
471
510
  Your tmux session: ${sessionName}
472
-
511
+ ${chromeSection}
473
512
  ${repositorySection(repoPath, repoUrl)}
474
513
 
475
514
  ## Your Responsibilities
@@ -567,6 +606,7 @@ export function generateFeatureTestPrompt(
567
606
  ): string {
568
607
  const includeProgressUpdates = shouldIncludeProgressUpdates(options);
569
608
  const techLeadSession = resolveTechLeadSession(options);
609
+ const chromeSection = options?.chromeEnabled ? '\n\n' + chromeTabIsolationSection() : '';
570
610
  const reportResultsSection = includeProgressUpdates
571
611
  ? `**If all tests pass:**
572
612
  \`\`\`bash
@@ -592,7 +632,7 @@ hive msg send ${techLeadSession} "E2E tests FAILED for ${requirementId} on ${fea
592
632
 
593
633
  return `You are a Feature Test Agent on Team ${teamName}.
594
634
  Your tmux session: ${sessionName}
595
-
635
+ ${chromeSection}
596
636
  ${repositorySection(repoPath, repoUrl)}
597
637
 
598
638
  ## Your Mission
@@ -670,9 +710,10 @@ export function generateAuditorPrompt(
670
710
  options?: AgentPromptOptions
671
711
  ): string {
672
712
  const techLeadSession = resolveTechLeadSession(options);
713
+ const chromeSection = options?.chromeEnabled ? '\n\n' + chromeTabIsolationSection() : '';
673
714
  return `You are a Hive Auditor Agent.
674
715
  Your tmux session: ${sessionName}
675
-
716
+ ${chromeSection}
676
717
  ${repositorySection(repoPath, repoUrl)}
677
718
 
678
719
  ## Your Mission
@@ -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', () => {
@@ -1905,6 +1956,9 @@ describe('Scheduler checkScaling', () => {
1905
1956
  repoPath: 'test',
1906
1957
  });
1907
1958
 
1959
+ const gapHiveDir = join(mockConfig.rootDir, '.hive');
1960
+ const gapSessionPrefix = generateSessionName('senior', team.name, undefined, gapHiveDir);
1961
+
1908
1962
  for (const index of [2, 3, 4, 5]) {
1909
1963
  db.run(
1910
1964
  `INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
@@ -1913,7 +1967,7 @@ describe('Scheduler checkScaling', () => {
1913
1967
  `senior-gap-${index}`,
1914
1968
  'senior',
1915
1969
  team.id,
1916
- `hive-senior-${team.name}-${index}`,
1970
+ `${gapSessionPrefix}-${index}`,
1917
1971
  'working',
1918
1972
  `STORY-EXISTING-${index}`,
1919
1973
  ]
@@ -1935,7 +1989,7 @@ describe('Scheduler checkScaling', () => {
1935
1989
  db.run(
1936
1990
  `INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
1937
1991
  VALUES (?, ?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`,
1938
- ['senior-gap-6', 'senior', team.id, `hive-senior-${team.name}-6`, 'idle']
1992
+ ['senior-gap-6', 'senior', team.id, `${gapSessionPrefix}-6`, 'idle']
1939
1993
  );
1940
1994
  return {
1941
1995
  id: 'senior-gap-6',
@@ -1943,7 +1997,7 @@ describe('Scheduler checkScaling', () => {
1943
1997
  team_id: team.id,
1944
1998
  status: 'idle',
1945
1999
  current_story_id: null,
1946
- tmux_session: `hive-senior-${team.name}-6`,
2000
+ tmux_session: `${gapSessionPrefix}-6`,
1947
2001
  };
1948
2002
  });
1949
2003
 
@@ -2056,10 +2110,13 @@ describe('Scheduler checkScaling', () => {
2056
2110
  repoPath: 'test',
2057
2111
  });
2058
2112
 
2113
+ const hiveDir = join(mockConfig.rootDir, '.hive');
2114
+ const expectedSession = generateSessionName('senior', team.name, undefined, hiveDir);
2115
+
2059
2116
  db.run(
2060
2117
  `INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
2061
2118
  VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
2062
- ['senior-guard-1', 'senior', team.id, `hive-senior-${team.name}`, 'working', 'STORY-ACTIVE']
2119
+ ['senior-guard-1', 'senior', team.id, expectedSession, 'working', 'STORY-ACTIVE']
2063
2120
  );
2064
2121
 
2065
2122
  const isRunningSpy = vi.spyOn(tmuxModule, 'isTmuxSessionRunning').mockResolvedValue(true);
@@ -2171,6 +2228,72 @@ describe('Scheduler checkScaling', () => {
2171
2228
  });
2172
2229
  });
2173
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
+
2174
2297
  describe('Scheduler Target Branch Propagation', () => {
2175
2298
  it('should retrieve target_branch from requirement when creating story', () => {
2176
2299
  const team = createTeam(db, {