hungry-ghost-hive 0.43.0 → 0.43.2

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 (133) hide show
  1. package/dist/cli/commands/agents.d.ts.map +1 -1
  2. package/dist/cli/commands/agents.js +4 -11
  3. package/dist/cli/commands/agents.js.map +1 -1
  4. package/dist/cli/commands/approach.d.ts.map +1 -1
  5. package/dist/cli/commands/approach.js +2 -6
  6. package/dist/cli/commands/approach.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +9 -0
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/init.test.js +3 -0
  11. package/dist/cli/commands/init.test.js.map +1 -1
  12. package/dist/cli/commands/manager/index.d.ts +2 -27
  13. package/dist/cli/commands/manager/index.d.ts.map +1 -1
  14. package/dist/cli/commands/manager/index.js +23 -1519
  15. package/dist/cli/commands/manager/index.js.map +1 -1
  16. package/dist/cli/commands/manager/manager-utils.d.ts +9 -0
  17. package/dist/cli/commands/manager/manager-utils.d.ts.map +1 -0
  18. package/dist/cli/commands/manager/manager-utils.js +49 -0
  19. package/dist/cli/commands/manager/manager-utils.js.map +1 -0
  20. package/dist/cli/commands/manager/pr-sync-orchestrator.d.ts +7 -0
  21. package/dist/cli/commands/manager/pr-sync-orchestrator.d.ts.map +1 -0
  22. package/dist/cli/commands/manager/pr-sync-orchestrator.js +537 -0
  23. package/dist/cli/commands/manager/pr-sync-orchestrator.js.map +1 -0
  24. package/dist/cli/commands/manager/qa-review-handler.d.ts +15 -0
  25. package/dist/cli/commands/manager/qa-review-handler.d.ts.map +1 -0
  26. package/dist/cli/commands/manager/qa-review-handler.js +290 -0
  27. package/dist/cli/commands/manager/qa-review-handler.js.map +1 -0
  28. package/dist/cli/commands/manager/stuck-story-helpers.d.ts +32 -0
  29. package/dist/cli/commands/manager/stuck-story-helpers.d.ts.map +1 -0
  30. package/dist/cli/commands/manager/stuck-story-helpers.js +163 -0
  31. package/dist/cli/commands/manager/stuck-story-helpers.js.map +1 -0
  32. package/dist/cli/commands/manager/stuck-story-processor.d.ts +8 -0
  33. package/dist/cli/commands/manager/stuck-story-processor.d.ts.map +1 -0
  34. package/dist/cli/commands/manager/stuck-story-processor.js +392 -0
  35. package/dist/cli/commands/manager/stuck-story-processor.js.map +1 -0
  36. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts +3 -0
  37. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -0
  38. package/dist/cli/commands/manager/tech-lead-lifecycle.js +141 -0
  39. package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -0
  40. package/dist/cli/commands/my-stories.d.ts.map +1 -1
  41. package/dist/cli/commands/my-stories.js +5 -20
  42. package/dist/cli/commands/my-stories.js.map +1 -1
  43. package/dist/cli/commands/pr.js +7 -22
  44. package/dist/cli/commands/pr.js.map +1 -1
  45. package/dist/cli/commands/progress.d.ts.map +1 -1
  46. package/dist/cli/commands/progress.js +2 -5
  47. package/dist/cli/commands/progress.js.map +1 -1
  48. package/dist/cli/commands/resume.d.ts.map +1 -1
  49. package/dist/cli/commands/resume.js +3 -6
  50. package/dist/cli/commands/resume.js.map +1 -1
  51. package/dist/cli/commands/status.d.ts.map +1 -1
  52. package/dist/cli/commands/status.js +2 -5
  53. package/dist/cli/commands/status.js.map +1 -1
  54. package/dist/cli/commands/stories.d.ts.map +1 -1
  55. package/dist/cli/commands/stories.js +2 -5
  56. package/dist/cli/commands/stories.js.map +1 -1
  57. package/dist/cluster/adapters.d.ts +3 -2
  58. package/dist/cluster/adapters.d.ts.map +1 -1
  59. package/dist/cluster/adapters.js +2 -11
  60. package/dist/cluster/adapters.js.map +1 -1
  61. package/dist/cluster/cluster-http-server.d.ts +20 -0
  62. package/dist/cluster/cluster-http-server.d.ts.map +1 -0
  63. package/dist/cluster/cluster-http-server.js +140 -0
  64. package/dist/cluster/cluster-http-server.js.map +1 -0
  65. package/dist/cluster/heartbeat-manager.d.ts +24 -0
  66. package/dist/cluster/heartbeat-manager.d.ts.map +1 -0
  67. package/dist/cluster/heartbeat-manager.js +74 -0
  68. package/dist/cluster/heartbeat-manager.js.map +1 -0
  69. package/dist/cluster/raft-state-machine.d.ts +48 -0
  70. package/dist/cluster/raft-state-machine.d.ts.map +1 -0
  71. package/dist/cluster/raft-state-machine.js +207 -0
  72. package/dist/cluster/raft-state-machine.js.map +1 -0
  73. package/dist/cluster/runtime.d.ts +5 -29
  74. package/dist/cluster/runtime.d.ts.map +1 -1
  75. package/dist/cluster/runtime.js +58 -406
  76. package/dist/cluster/runtime.js.map +1 -1
  77. package/dist/integrations/jira/sync.d.ts +2 -5
  78. package/dist/integrations/jira/sync.d.ts.map +1 -1
  79. package/dist/integrations/jira/sync.js +116 -178
  80. package/dist/integrations/jira/sync.js.map +1 -1
  81. package/dist/utils/cli-helpers.d.ts +19 -0
  82. package/dist/utils/cli-helpers.d.ts.map +1 -0
  83. package/dist/utils/cli-helpers.js +51 -0
  84. package/dist/utils/cli-helpers.js.map +1 -0
  85. package/dist/utils/cli-helpers.test.d.ts +2 -0
  86. package/dist/utils/cli-helpers.test.d.ts.map +1 -0
  87. package/dist/utils/cli-helpers.test.js +100 -0
  88. package/dist/utils/cli-helpers.test.js.map +1 -0
  89. package/dist/utils/github-cli.d.ts +3 -0
  90. package/dist/utils/github-cli.d.ts.map +1 -0
  91. package/dist/utils/github-cli.js +4 -0
  92. package/dist/utils/github-cli.js.map +1 -0
  93. package/dist/utils/pr-sync.d.ts.map +1 -1
  94. package/dist/utils/pr-sync.js +1 -2
  95. package/dist/utils/pr-sync.js.map +1 -1
  96. package/dist/utils/story-status.d.ts +19 -0
  97. package/dist/utils/story-status.d.ts.map +1 -0
  98. package/dist/utils/story-status.js +58 -0
  99. package/dist/utils/story-status.js.map +1 -0
  100. package/dist/utils/story-status.test.d.ts +2 -0
  101. package/dist/utils/story-status.test.d.ts.map +1 -0
  102. package/dist/utils/story-status.test.js +65 -0
  103. package/dist/utils/story-status.test.js.map +1 -0
  104. package/package.json +1 -1
  105. package/src/cli/commands/agents.ts +3 -11
  106. package/src/cli/commands/approach.ts +2 -7
  107. package/src/cli/commands/init.test.ts +4 -0
  108. package/src/cli/commands/init.ts +9 -0
  109. package/src/cli/commands/manager/index.ts +166 -2236
  110. package/src/cli/commands/manager/manager-utils.ts +85 -0
  111. package/src/cli/commands/manager/pr-sync-orchestrator.ts +659 -0
  112. package/src/cli/commands/manager/qa-review-handler.ts +399 -0
  113. package/src/cli/commands/manager/stuck-story-helpers.ts +255 -0
  114. package/src/cli/commands/manager/stuck-story-processor.ts +604 -0
  115. package/src/cli/commands/manager/tech-lead-lifecycle.ts +210 -0
  116. package/src/cli/commands/my-stories.ts +5 -30
  117. package/src/cli/commands/pr.ts +6 -22
  118. package/src/cli/commands/progress.ts +2 -7
  119. package/src/cli/commands/resume.ts +3 -6
  120. package/src/cli/commands/status.ts +2 -5
  121. package/src/cli/commands/stories.ts +2 -5
  122. package/src/cluster/adapters.ts +3 -12
  123. package/src/cluster/cluster-http-server.ts +187 -0
  124. package/src/cluster/heartbeat-manager.ts +112 -0
  125. package/src/cluster/raft-state-machine.ts +267 -0
  126. package/src/cluster/runtime.ts +71 -515
  127. package/src/integrations/jira/sync.ts +157 -215
  128. package/src/utils/cli-helpers.test.ts +138 -0
  129. package/src/utils/cli-helpers.ts +61 -0
  130. package/src/utils/github-cli.ts +4 -0
  131. package/src/utils/pr-sync.ts +1 -3
  132. package/src/utils/story-status.test.ts +74 -0
  133. package/src/utils/story-status.ts +62 -0
@@ -0,0 +1,112 @@
1
+ // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
+
3
+ import type { ClusterConfig, ClusterPeerConfig } from '../config/schema.js';
4
+ import type { RaftStateMachine } from './raft-state-machine.js';
5
+
6
+ interface HeartbeatRequest {
7
+ term: number;
8
+ leader_id: string;
9
+ }
10
+
11
+ interface HeartbeatResponse {
12
+ term: number;
13
+ success: boolean;
14
+ }
15
+
16
+ export interface HeartbeatManagerDeps {
17
+ raft: RaftStateMachine;
18
+ postJson: <T>(peer: ClusterPeerConfig, path: string, body: unknown) => Promise<T | null>;
19
+ isActive: () => boolean;
20
+ handleBackgroundError: (error: unknown) => void;
21
+ }
22
+
23
+ export class HeartbeatManager {
24
+ private heartbeatTimer: NodeJS.Timeout | null = null;
25
+
26
+ constructor(
27
+ private readonly config: ClusterConfig,
28
+ private readonly deps: HeartbeatManagerDeps
29
+ ) {}
30
+
31
+ startHeartbeatLoop(): void {
32
+ this.heartbeatTimer = setInterval(() => {
33
+ if (!this.config.enabled) return;
34
+ if (this.deps.raft.role !== 'leader') return;
35
+ void this.sendHeartbeats().catch(error => this.deps.handleBackgroundError(error));
36
+ }, this.config.heartbeat_interval_ms);
37
+ }
38
+
39
+ stopHeartbeatLoop(): void {
40
+ if (this.heartbeatTimer) {
41
+ clearInterval(this.heartbeatTimer);
42
+ this.heartbeatTimer = null;
43
+ }
44
+ }
45
+
46
+ async sendHeartbeats(): Promise<void> {
47
+ if (!this.deps.isActive()) return;
48
+
49
+ const { raft } = this.deps;
50
+
51
+ const heartbeat: HeartbeatRequest = {
52
+ term: raft.currentTerm,
53
+ leader_id: this.config.node_id,
54
+ };
55
+
56
+ raft.appendDurableEntry('heartbeat_sent', {
57
+ term: raft.currentTerm,
58
+ leader_id: this.config.node_id,
59
+ peer_count: this.config.peers.filter(peer => peer.id !== this.config.node_id).length,
60
+ });
61
+
62
+ await Promise.all(
63
+ this.config.peers
64
+ .filter(peer => peer.id !== this.config.node_id)
65
+ .map(async peer => {
66
+ const response = await this.deps.postJson<HeartbeatResponse>(
67
+ peer,
68
+ '/cluster/v1/election/heartbeat',
69
+ heartbeat
70
+ );
71
+
72
+ if (response && response.term > raft.currentTerm) {
73
+ raft.stepDown(response.term, peer.id);
74
+ }
75
+ })
76
+ );
77
+ }
78
+
79
+ handleHeartbeat(body: unknown): HeartbeatResponse {
80
+ const { raft } = this.deps;
81
+
82
+ const request = body as Partial<HeartbeatRequest>;
83
+ const term = Number(request.term || 0);
84
+ const leaderId = typeof request.leader_id === 'string' ? request.leader_id : null;
85
+
86
+ if (term < raft.currentTerm) {
87
+ return { term: raft.currentTerm, success: false };
88
+ }
89
+
90
+ const changed =
91
+ term > raft.currentTerm || leaderId !== raft.leaderId || raft.role !== 'follower';
92
+
93
+ if (term > raft.currentTerm) {
94
+ raft.stepDown(term, leaderId);
95
+ } else {
96
+ raft.role = 'follower';
97
+ raft.leaderId = leaderId;
98
+ raft.persistRaftState();
99
+ }
100
+
101
+ raft.resetElectionDeadline();
102
+
103
+ if (changed) {
104
+ raft.appendDurableEntry('heartbeat_received', {
105
+ term,
106
+ leader_id: leaderId,
107
+ });
108
+ }
109
+
110
+ return { term: raft.currentTerm, success: true };
111
+ }
112
+ }
@@ -0,0 +1,267 @@
1
+ // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
+
3
+ import { join } from 'path';
4
+ import type { ClusterConfig, ClusterPeerConfig } from '../config/schema.js';
5
+ import type { DurableLogEntryType } from './raft-store.js';
6
+ import { RaftMetadataStore } from './raft-store.js';
7
+
8
+ type NodeRole = 'leader' | 'follower' | 'candidate';
9
+
10
+ interface VoteRequest {
11
+ term: number;
12
+ candidate_id: string;
13
+ }
14
+
15
+ interface VoteResponse {
16
+ term: number;
17
+ vote_granted: boolean;
18
+ leader_id: string | null;
19
+ }
20
+
21
+ export interface RaftStateMachineDeps {
22
+ postJson: <T>(peer: ClusterPeerConfig, path: string, body: unknown) => Promise<T | null>;
23
+ isActive: () => boolean;
24
+ handleBackgroundError: (error: unknown) => void;
25
+ }
26
+
27
+ export class RaftStateMachine {
28
+ role: NodeRole = 'follower';
29
+ currentTerm = 0;
30
+ votedFor: string | null = null;
31
+ leaderId: string | null = null;
32
+
33
+ private electionDeadline = 0;
34
+ private electionInFlight = false;
35
+ private electionTimer: NodeJS.Timeout | null = null;
36
+ private raftStore: RaftMetadataStore | null = null;
37
+
38
+ constructor(
39
+ private readonly config: ClusterConfig,
40
+ private readonly deps: RaftStateMachineDeps
41
+ ) {}
42
+
43
+ initializeRaftStore(hiveDir: string): void {
44
+ if (this.raftStore) return;
45
+
46
+ const clusterDir = join(hiveDir, 'cluster');
47
+ this.raftStore = new RaftMetadataStore({
48
+ clusterDir,
49
+ nodeId: this.config.node_id,
50
+ });
51
+
52
+ const persisted = this.raftStore.getState();
53
+ this.currentTerm = persisted.current_term;
54
+ this.votedFor = persisted.voted_for;
55
+ this.leaderId = persisted.leader_id;
56
+ this.role = 'follower';
57
+ }
58
+
59
+ getRaftStore(): RaftMetadataStore | null {
60
+ return this.raftStore;
61
+ }
62
+
63
+ clearRaftStore(): void {
64
+ this.raftStore = null;
65
+ }
66
+
67
+ startElectionLoop(): void {
68
+ this.resetElectionDeadline();
69
+ this.electionTimer = setInterval(() => {
70
+ if (!this.config.enabled) return;
71
+ if (this.role === 'leader') return;
72
+
73
+ if (Date.now() >= this.electionDeadline) {
74
+ void this.startElection().catch(error => this.deps.handleBackgroundError(error));
75
+ }
76
+ }, 250);
77
+ }
78
+
79
+ stopElectionLoop(): void {
80
+ if (this.electionTimer) {
81
+ clearInterval(this.electionTimer);
82
+ this.electionTimer = null;
83
+ }
84
+ this.electionInFlight = false;
85
+ }
86
+
87
+ async startElection(): Promise<void> {
88
+ if (!this.config.enabled || this.electionInFlight || !this.deps.isActive()) return;
89
+
90
+ this.electionInFlight = true;
91
+ const electionTerm = this.currentTerm + 1;
92
+
93
+ this.currentTerm = electionTerm;
94
+ this.role = 'candidate';
95
+ this.votedFor = this.config.node_id;
96
+ this.leaderId = null;
97
+ this.resetElectionDeadline();
98
+ this.persistRaftState();
99
+ this.appendDurableEntry('election_start', {
100
+ term: electionTerm,
101
+ candidate_id: this.config.node_id,
102
+ });
103
+
104
+ let votes = 1;
105
+
106
+ try {
107
+ await Promise.all(
108
+ this.config.peers
109
+ .filter(peer => peer.id !== this.config.node_id)
110
+ .map(async peer => {
111
+ const response = await this.deps.postJson<VoteResponse>(
112
+ peer,
113
+ '/cluster/v1/election/request-vote',
114
+ {
115
+ term: electionTerm,
116
+ candidate_id: this.config.node_id,
117
+ } satisfies VoteRequest
118
+ );
119
+
120
+ if (!response) return;
121
+
122
+ if (response.term > this.currentTerm) {
123
+ this.stepDown(response.term, response.leader_id);
124
+ return;
125
+ }
126
+
127
+ if (
128
+ this.role === 'candidate' &&
129
+ this.currentTerm === electionTerm &&
130
+ response.vote_granted
131
+ ) {
132
+ votes += 1;
133
+ }
134
+ })
135
+ );
136
+
137
+ if (
138
+ this.role === 'candidate' &&
139
+ this.currentTerm === electionTerm &&
140
+ votes >= this.quorum()
141
+ ) {
142
+ this.role = 'leader';
143
+ this.leaderId = this.config.node_id;
144
+ this.votedFor = null;
145
+ this.persistRaftState();
146
+ this.appendDurableEntry('election_won', {
147
+ term: electionTerm,
148
+ votes,
149
+ quorum: this.quorum(),
150
+ leader_id: this.config.node_id,
151
+ });
152
+ }
153
+ } finally {
154
+ this.electionInFlight = false;
155
+ }
156
+ }
157
+
158
+ handleVoteRequest(body: unknown): VoteResponse {
159
+ const request = body as Partial<VoteRequest>;
160
+ const term = Number(request.term || 0);
161
+ const candidateId = typeof request.candidate_id === 'string' ? request.candidate_id : '';
162
+
163
+ if (!candidateId) {
164
+ return { term: this.currentTerm, vote_granted: false, leader_id: this.leaderId };
165
+ }
166
+
167
+ if (term < this.currentTerm) {
168
+ return { term: this.currentTerm, vote_granted: false, leader_id: this.leaderId };
169
+ }
170
+
171
+ if (term > this.currentTerm) {
172
+ this.stepDown(term, null);
173
+ }
174
+
175
+ const canVote = this.votedFor === null || this.votedFor === candidateId;
176
+ if (canVote) {
177
+ this.votedFor = candidateId;
178
+ this.resetElectionDeadline();
179
+ this.persistRaftState();
180
+ this.appendDurableEntry('vote_granted', {
181
+ term: this.currentTerm,
182
+ candidate_id: candidateId,
183
+ });
184
+ return { term: this.currentTerm, vote_granted: true, leader_id: this.leaderId };
185
+ }
186
+
187
+ return { term: this.currentTerm, vote_granted: false, leader_id: this.leaderId };
188
+ }
189
+
190
+ stepDown(term: number, leaderId: string | null): void {
191
+ const previousRole = this.role;
192
+ const previousTerm = this.currentTerm;
193
+
194
+ this.currentTerm = term;
195
+ this.role = 'follower';
196
+ this.votedFor = null;
197
+ this.leaderId = leaderId;
198
+ this.resetElectionDeadline();
199
+ this.persistRaftState();
200
+
201
+ this.appendDurableEntry('state_transition', {
202
+ previous_role: previousRole,
203
+ previous_term: previousTerm,
204
+ current_term: this.currentTerm,
205
+ leader_id: leaderId,
206
+ });
207
+ }
208
+
209
+ quorum(): number {
210
+ const nodes = this.config.peers.length + 1;
211
+ return Math.floor(nodes / 2) + 1;
212
+ }
213
+
214
+ resetElectionDeadline(): void {
215
+ const min = this.config.election_timeout_min_ms;
216
+ const max = Math.max(min, this.config.election_timeout_max_ms);
217
+ const spread = max - min;
218
+ const jitter = spread === 0 ? 0 : Math.floor(Math.random() * spread);
219
+ this.electionDeadline = Date.now() + min + jitter;
220
+ }
221
+
222
+ persistRaftState(): void {
223
+ if (!this.raftStore) return;
224
+
225
+ this.raftStore.setState({
226
+ current_term: this.currentTerm,
227
+ voted_for: this.votedFor,
228
+ leader_id: this.leaderId,
229
+ });
230
+ }
231
+
232
+ appendDurableEntry(type: DurableLogEntryType, metadata: Record<string, unknown>): void {
233
+ if (!this.raftStore) return;
234
+
235
+ try {
236
+ this.raftStore.appendEntry({ type, term: this.currentTerm, metadata });
237
+ } catch (error) {
238
+ const err = error as NodeJS.ErrnoException;
239
+ if (err.code === 'ENOENT') return;
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ appendClusterEventsToDurableLog(events: import('./replication.js').ClusterEvent[]): number {
245
+ if (!this.raftStore) return 0;
246
+
247
+ const appended = this.raftStore.appendClusterEvents(events, this.currentTerm);
248
+ this.persistRaftState();
249
+ return appended;
250
+ }
251
+
252
+ getRaftStoreState(): {
253
+ commit_index: number;
254
+ last_applied: number;
255
+ last_log_index: number;
256
+ } | null {
257
+ return this.raftStore?.getState() ?? null;
258
+ }
259
+
260
+ getLeaderUrl(): string | null {
261
+ if (!this.leaderId) return null;
262
+ if (this.leaderId === this.config.node_id) return this.config.public_url;
263
+
264
+ const peer = this.config.peers.find(item => item.id === this.leaderId);
265
+ return peer?.url || null;
266
+ }
267
+ }