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.
- package/dist/agents/base-agent.d.ts +1 -0
- package/dist/agents/base-agent.d.ts.map +1 -1
- package/dist/agents/base-agent.js +4 -0
- package/dist/agents/base-agent.js.map +1 -1
- package/dist/agents/intermediate.js +2 -2
- package/dist/agents/intermediate.js.map +1 -1
- package/dist/agents/junior.js +2 -2
- package/dist/agents/junior.js.map +1 -1
- package/dist/agents/qa.d.ts.map +1 -1
- package/dist/agents/qa.js +5 -5
- package/dist/agents/qa.js.map +1 -1
- package/dist/agents/senior.d.ts.map +1 -1
- package/dist/agents/senior.js +5 -5
- package/dist/agents/senior.js.map +1 -1
- package/dist/agents/tech-lead.d.ts.map +1 -1
- package/dist/agents/tech-lead.js +4 -2
- package/dist/agents/tech-lead.js.map +1 -1
- package/dist/cli/commands/assign.d.ts.map +1 -1
- package/dist/cli/commands/assign.js +4 -2
- package/dist/cli/commands/assign.js.map +1 -1
- package/dist/cli/commands/assign.test.js +5 -0
- package/dist/cli/commands/assign.test.js.map +1 -1
- package/dist/cli/commands/cluster.d.ts.map +1 -1
- package/dist/cli/commands/cluster.js +348 -1
- package/dist/cli/commands/cluster.js.map +1 -1
- package/dist/cli/commands/cluster.test.js +313 -9
- package/dist/cli/commands/cluster.test.js.map +1 -1
- package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
- package/dist/cli/commands/manager/handoff-recovery.js +4 -2
- package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
- package/dist/cli/commands/manager/index.d.ts.map +1 -1
- package/dist/cli/commands/manager/index.js +16 -12
- package/dist/cli/commands/manager/index.js.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js +4 -2
- package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
- package/dist/cli/commands/msg.d.ts.map +1 -1
- package/dist/cli/commands/msg.js +8 -7
- package/dist/cli/commands/msg.js.map +1 -1
- package/dist/cli/commands/my-stories.js +3 -3
- package/dist/cli/commands/my-stories.js.map +1 -1
- package/dist/cli/commands/nuke.d.ts.map +1 -1
- package/dist/cli/commands/nuke.js +18 -7
- package/dist/cli/commands/nuke.js.map +1 -1
- package/dist/cli/commands/nuke.test.js +24 -0
- package/dist/cli/commands/nuke.test.js.map +1 -1
- package/dist/cli/commands/req-spawn.test.d.ts +2 -0
- package/dist/cli/commands/req-spawn.test.d.ts.map +1 -0
- package/dist/cli/commands/req-spawn.test.js +116 -0
- package/dist/cli/commands/req-spawn.test.js.map +1 -0
- package/dist/cli/commands/req.d.ts +1 -1
- package/dist/cli/commands/req.d.ts.map +1 -1
- package/dist/cli/commands/req.js +28 -18
- package/dist/cli/commands/req.js.map +1 -1
- package/dist/cli/commands/stories.js +3 -3
- package/dist/cli/commands/stories.js.map +1 -1
- package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/agents.js +7 -3
- package/dist/cli/dashboard/panels/agents.js.map +1 -1
- package/dist/cluster/cluster-http-server.d.ts +32 -0
- package/dist/cluster/cluster-http-server.d.ts.map +1 -1
- package/dist/cluster/cluster-http-server.js +42 -0
- package/dist/cluster/cluster-http-server.js.map +1 -1
- package/dist/cluster/distributed-runtime-coverage.test.js +9 -0
- package/dist/cluster/distributed-runtime-coverage.test.js.map +1 -1
- package/dist/cluster/distributed-system.test.js +135 -0
- package/dist/cluster/distributed-system.test.js.map +1 -1
- package/dist/cluster/events.d.ts +23 -0
- package/dist/cluster/events.d.ts.map +1 -1
- package/dist/cluster/events.js +74 -0
- package/dist/cluster/events.js.map +1 -1
- package/dist/cluster/heartbeat-manager.d.ts +2 -0
- package/dist/cluster/heartbeat-manager.d.ts.map +1 -1
- package/dist/cluster/heartbeat-manager.js +42 -6
- package/dist/cluster/heartbeat-manager.js.map +1 -1
- package/dist/cluster/membership.test.d.ts +2 -0
- package/dist/cluster/membership.test.d.ts.map +1 -0
- package/dist/cluster/membership.test.js +416 -0
- package/dist/cluster/membership.test.js.map +1 -0
- package/dist/cluster/partition-safety.test.d.ts +2 -0
- package/dist/cluster/partition-safety.test.d.ts.map +1 -0
- package/dist/cluster/partition-safety.test.js +440 -0
- package/dist/cluster/partition-safety.test.js.map +1 -0
- package/dist/cluster/raft-state-machine.d.ts +33 -1
- package/dist/cluster/raft-state-machine.d.ts.map +1 -1
- package/dist/cluster/raft-state-machine.js +65 -3
- package/dist/cluster/raft-state-machine.js.map +1 -1
- package/dist/cluster/raft-store.d.ts +26 -1
- package/dist/cluster/raft-store.d.ts.map +1 -1
- package/dist/cluster/raft-store.js +137 -0
- package/dist/cluster/raft-store.js.map +1 -1
- package/dist/cluster/replication-lag.test.d.ts +2 -0
- package/dist/cluster/replication-lag.test.d.ts.map +1 -0
- package/dist/cluster/replication-lag.test.js +239 -0
- package/dist/cluster/replication-lag.test.js.map +1 -0
- package/dist/cluster/replication.d.ts +2 -2
- package/dist/cluster/replication.d.ts.map +1 -1
- package/dist/cluster/replication.js +1 -1
- package/dist/cluster/replication.js.map +1 -1
- package/dist/cluster/runtime.d.ts +78 -0
- package/dist/cluster/runtime.d.ts.map +1 -1
- package/dist/cluster/runtime.js +400 -13
- package/dist/cluster/runtime.js.map +1 -1
- package/dist/cluster/state-recovery.test.d.ts +2 -0
- package/dist/cluster/state-recovery.test.d.ts.map +1 -0
- package/dist/cluster/state-recovery.test.js +310 -0
- package/dist/cluster/state-recovery.test.js.map +1 -0
- package/dist/cluster/types.d.ts +30 -0
- package/dist/cluster/types.d.ts.map +1 -1
- package/dist/config/schema.d.ts +48 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +11 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/context-files/generator.d.ts +1 -1
- package/dist/context-files/generator.d.ts.map +1 -1
- package/dist/context-files/generator.js +4 -3
- package/dist/context-files/generator.js.map +1 -1
- package/dist/context-files/generator.test.js +51 -0
- package/dist/context-files/generator.test.js.map +1 -1
- package/dist/context-files/index.test.js +1 -0
- package/dist/context-files/index.test.js.map +1 -1
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +6 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/migrations/015-add-story-markdown-path.sql +5 -0
- package/dist/db/queries/stories.d.ts +3 -3
- package/dist/db/queries/stories.d.ts.map +1 -1
- package/dist/db/queries/stories.js +23 -5
- package/dist/db/queries/stories.js.map +1 -1
- package/dist/db/queries/test-helpers.d.ts.map +1 -1
- package/dist/db/queries/test-helpers.js +1 -0
- package/dist/db/queries/test-helpers.js.map +1 -1
- package/dist/git/worktree.d.ts.map +1 -1
- package/dist/git/worktree.js +7 -0
- package/dist/git/worktree.js.map +1 -1
- package/dist/git/worktree.test.js +30 -0
- package/dist/git/worktree.test.js.map +1 -1
- package/dist/orchestrator/orphan-recovery.d.ts +1 -1
- package/dist/orchestrator/orphan-recovery.d.ts.map +1 -1
- package/dist/orchestrator/orphan-recovery.js +4 -4
- package/dist/orchestrator/orphan-recovery.js.map +1 -1
- package/dist/orchestrator/prompt-templates.d.ts +6 -2
- package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
- package/dist/orchestrator/prompt-templates.js +61 -16
- package/dist/orchestrator/prompt-templates.js.map +1 -1
- package/dist/orchestrator/prompt-templates.test.js +214 -0
- package/dist/orchestrator/prompt-templates.test.js.map +1 -1
- package/dist/orchestrator/scheduler.d.ts +1 -0
- package/dist/orchestrator/scheduler.d.ts.map +1 -1
- package/dist/orchestrator/scheduler.js +30 -17
- package/dist/orchestrator/scheduler.js.map +1 -1
- package/dist/orchestrator/scheduler.test.js +98 -6
- package/dist/orchestrator/scheduler.test.js.map +1 -1
- package/dist/tmux/manager.d.ts +7 -6
- package/dist/tmux/manager.d.ts.map +1 -1
- package/dist/tmux/manager.js +29 -13
- package/dist/tmux/manager.js.map +1 -1
- package/dist/utils/instance.d.ts +32 -0
- package/dist/utils/instance.d.ts.map +1 -0
- package/dist/utils/instance.js +82 -0
- package/dist/utils/instance.js.map +1 -0
- package/dist/utils/instance.test.d.ts +2 -0
- package/dist/utils/instance.test.d.ts.map +1 -0
- package/dist/utils/instance.test.js +103 -0
- package/dist/utils/instance.test.js.map +1 -0
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +2 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/paths.test.js +6 -0
- package/dist/utils/paths.test.js.map +1 -1
- package/dist/utils/story-markdown.d.ts +16 -0
- package/dist/utils/story-markdown.d.ts.map +1 -0
- package/dist/utils/story-markdown.js +82 -0
- package/dist/utils/story-markdown.js.map +1 -0
- package/dist/utils/story-markdown.test.d.ts +2 -0
- package/dist/utils/story-markdown.test.d.ts.map +1 -0
- package/dist/utils/story-markdown.test.js +143 -0
- package/dist/utils/story-markdown.test.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/base-agent.ts +5 -0
- package/src/agents/intermediate.ts +2 -2
- package/src/agents/junior.ts +2 -2
- package/src/agents/qa.ts +13 -8
- package/src/agents/senior.ts +21 -11
- package/src/agents/tech-lead.ts +24 -12
- package/src/cli/commands/assign.test.ts +5 -0
- package/src/cli/commands/assign.ts +4 -2
- package/src/cli/commands/cluster.test.ts +387 -9
- package/src/cli/commands/cluster.ts +486 -1
- package/src/cli/commands/manager/handoff-recovery.ts +4 -2
- package/src/cli/commands/manager/index.ts +16 -11
- package/src/cli/commands/manager/tech-lead-lifecycle.ts +5 -2
- package/src/cli/commands/msg.ts +8 -7
- package/src/cli/commands/my-stories.ts +22 -13
- package/src/cli/commands/nuke.test.ts +31 -0
- package/src/cli/commands/nuke.ts +18 -7
- package/src/cli/commands/req-spawn.test.ts +153 -0
- package/src/cli/commands/req.ts +40 -23
- package/src/cli/commands/stories.ts +22 -13
- package/src/cli/dashboard/panels/agents.ts +7 -3
- package/src/cluster/cluster-http-server.ts +80 -0
- package/src/cluster/distributed-runtime-coverage.test.ts +9 -0
- package/src/cluster/distributed-system.test.ts +168 -0
- package/src/cluster/events.ts +90 -0
- package/src/cluster/heartbeat-manager.ts +48 -6
- package/src/cluster/membership.test.ts +498 -0
- package/src/cluster/partition-safety.test.ts +523 -0
- package/src/cluster/raft-state-machine.ts +76 -4
- package/src/cluster/raft-store.ts +167 -1
- package/src/cluster/replication-lag.test.ts +284 -0
- package/src/cluster/replication.ts +6 -0
- package/src/cluster/runtime.ts +551 -12
- package/src/cluster/state-recovery.test.ts +420 -0
- package/src/cluster/types.ts +32 -0
- package/src/config/schema.ts +11 -0
- package/src/context-files/generator.test.ts +55 -0
- package/src/context-files/generator.ts +8 -7
- package/src/context-files/index.test.ts +1 -0
- package/src/db/client.ts +7 -0
- package/src/db/migrations/015-add-story-markdown-path.sql +5 -0
- package/src/db/queries/stories.ts +29 -5
- package/src/db/queries/test-helpers.ts +1 -0
- package/src/git/worktree.test.ts +43 -0
- package/src/git/worktree.ts +10 -0
- package/src/orchestrator/orphan-recovery.ts +32 -13
- package/src/orchestrator/prompt-templates.test.ts +267 -0
- package/src/orchestrator/prompt-templates.ts +69 -16
- package/src/orchestrator/scheduler.test.ts +130 -6
- package/src/orchestrator/scheduler.ts +66 -27
- package/src/tmux/manager.ts +42 -13
- package/src/utils/instance.test.ts +129 -0
- package/src/utils/instance.ts +95 -0
- package/src/utils/paths.test.ts +8 -0
- package/src/utils/paths.ts +3 -0
- package/src/utils/story-markdown.test.ts +176 -0
- 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
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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(
|
|
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(
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
548
|
-
|
|
549
|
-
|
|
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(
|
|
708
|
-
|
|
709
|
-
|
|
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(
|
|
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
|
-
|
|
889
|
-
|
|
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
|
-
?
|
|
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,
|
package/src/tmux/manager.ts
CHANGED
|
@@ -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(
|
|
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
|
|
530
|
-
|
|
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(
|
|
534
|
-
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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(
|
|
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
|
+
});
|