hungry-ghost-hive 0.47.2 → 0.47.3
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/cli/commands/auth.js +4 -4
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/auth.test.js +42 -0
- package/dist/cli/commands/auth.test.js.map +1 -1
- package/dist/cli/commands/pr.d.ts.map +1 -1
- package/dist/cli/commands/pr.js +0 -22
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/commands/pr.test.js +28 -0
- package/dist/cli/commands/pr.test.js.map +1 -1
- package/dist/cli/wizard/init-wizard.js +1 -1
- package/dist/cli/wizard/init-wizard.js.map +1 -1
- package/dist/cli/wizard/init-wizard.test.js +10 -4
- package/dist/cli/wizard/init-wizard.test.js.map +1 -1
- package/dist/config/schema.d.ts +19 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +4 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/context-files/index.test.js +1 -1
- package/dist/context-files/index.test.js.map +1 -1
- package/dist/utils/auto-merge.d.ts +8 -0
- package/dist/utils/auto-merge.d.ts.map +1 -1
- package/dist/utils/auto-merge.js +138 -0
- package/dist/utils/auto-merge.js.map +1 -1
- package/dist/utils/auto-merge.test.js +145 -1
- package/dist/utils/auto-merge.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/auth.test.ts +51 -0
- package/src/cli/commands/auth.ts +4 -4
- package/src/cli/commands/pr.test.ts +48 -0
- package/src/cli/commands/pr.ts +0 -30
- package/src/cli/wizard/init-wizard.test.ts +10 -4
- package/src/cli/wizard/init-wizard.ts +1 -1
- package/src/config/schema.ts +4 -0
- package/src/context-files/index.test.ts +1 -1
- package/src/utils/auto-merge.test.ts +213 -1
- package/src/utils/auto-merge.ts +179 -0
|
@@ -27,7 +27,7 @@ vi.mock('../connectors/project-management/operations.js', () => ({
|
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
29
|
import { loadConfig } from '../config/loader.js';
|
|
30
|
-
import { autoMergeApprovedPRs } from './auto-merge.js';
|
|
30
|
+
import { autoMergeApprovedPRs, checkPreexistingCIFailures } from './auto-merge.js';
|
|
31
31
|
|
|
32
32
|
const mockLoadConfig = vi.mocked(loadConfig);
|
|
33
33
|
|
|
@@ -380,5 +380,217 @@ describe('auto-merge functionality', () => {
|
|
|
380
380
|
expect(result).toBe(0);
|
|
381
381
|
expect(getPullRequestById(db, pr.id)?.status).toBe('approved');
|
|
382
382
|
});
|
|
383
|
+
|
|
384
|
+
it('should bypass BLOCKED status when all CI failures are pre-existing on base branch', async () => {
|
|
385
|
+
const pr = createPullRequest(db, {
|
|
386
|
+
storyId,
|
|
387
|
+
teamId,
|
|
388
|
+
branchName: 'feature/preexisting-ci',
|
|
389
|
+
githubPrNumber: 555,
|
|
390
|
+
});
|
|
391
|
+
updatePullRequest(db, pr.id, { status: 'approved' });
|
|
392
|
+
|
|
393
|
+
mockLoadConfig.mockReturnValue({
|
|
394
|
+
integrations: {
|
|
395
|
+
autonomy: { level: 'full', allow_preexisting_ci_failures: true },
|
|
396
|
+
source_control: { provider: 'github' },
|
|
397
|
+
project_management: { provider: 'none' },
|
|
398
|
+
},
|
|
399
|
+
} as any);
|
|
400
|
+
|
|
401
|
+
const mockExecSync = vi.fn();
|
|
402
|
+
// 1. gh pr view → BLOCKED but MERGEABLE
|
|
403
|
+
mockExecSync.mockReturnValueOnce(
|
|
404
|
+
JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BLOCKED' })
|
|
405
|
+
);
|
|
406
|
+
// 2. gh pr checks → one failing check
|
|
407
|
+
mockExecSync.mockReturnValueOnce(
|
|
408
|
+
JSON.stringify([
|
|
409
|
+
{ name: 'build', state: 'SUCCESS' },
|
|
410
|
+
{ name: 'lint', state: 'FAIL' },
|
|
411
|
+
])
|
|
412
|
+
);
|
|
413
|
+
// 3. gh pr view baseRefName
|
|
414
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ baseRefName: 'main' }));
|
|
415
|
+
// 4. gh api base branch check-runs → same check failing
|
|
416
|
+
mockExecSync.mockReturnValueOnce(
|
|
417
|
+
JSON.stringify([
|
|
418
|
+
{ name: 'build', conclusion: 'success' },
|
|
419
|
+
{ name: 'lint', conclusion: 'failure' },
|
|
420
|
+
])
|
|
421
|
+
);
|
|
422
|
+
// 5. gh pr merge --admin → success
|
|
423
|
+
mockExecSync.mockReturnValueOnce(undefined);
|
|
424
|
+
|
|
425
|
+
vi.doMock('child_process', () => ({ execSync: mockExecSync }));
|
|
426
|
+
|
|
427
|
+
const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
|
|
428
|
+
const result = await autoMergeApprovedPRs('/mock/root', dbClient);
|
|
429
|
+
|
|
430
|
+
expect(result).toBe(1);
|
|
431
|
+
expect(getPullRequestById(db, pr.id)?.status).toBe('merged');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should not bypass BLOCKED status when PR has new CI failures not on base branch', async () => {
|
|
435
|
+
const pr = createPullRequest(db, {
|
|
436
|
+
storyId,
|
|
437
|
+
teamId,
|
|
438
|
+
branchName: 'feature/new-ci-failure',
|
|
439
|
+
githubPrNumber: 556,
|
|
440
|
+
});
|
|
441
|
+
updatePullRequest(db, pr.id, { status: 'approved' });
|
|
442
|
+
|
|
443
|
+
mockLoadConfig.mockReturnValue({
|
|
444
|
+
integrations: {
|
|
445
|
+
autonomy: { level: 'full', allow_preexisting_ci_failures: true },
|
|
446
|
+
source_control: { provider: 'github' },
|
|
447
|
+
project_management: { provider: 'none' },
|
|
448
|
+
},
|
|
449
|
+
} as any);
|
|
450
|
+
|
|
451
|
+
const mockExecSync = vi.fn();
|
|
452
|
+
// 1. gh pr view → BLOCKED but MERGEABLE
|
|
453
|
+
mockExecSync.mockReturnValueOnce(
|
|
454
|
+
JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BLOCKED' })
|
|
455
|
+
);
|
|
456
|
+
// 2. gh pr checks → two failing checks
|
|
457
|
+
mockExecSync.mockReturnValueOnce(
|
|
458
|
+
JSON.stringify([
|
|
459
|
+
{ name: 'build', state: 'FAIL' },
|
|
460
|
+
{ name: 'lint', state: 'FAIL' },
|
|
461
|
+
])
|
|
462
|
+
);
|
|
463
|
+
// 3. gh pr view baseRefName
|
|
464
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ baseRefName: 'main' }));
|
|
465
|
+
// 4. gh api base branch check-runs → only lint fails on base
|
|
466
|
+
mockExecSync.mockReturnValueOnce(
|
|
467
|
+
JSON.stringify([
|
|
468
|
+
{ name: 'build', conclusion: 'success' },
|
|
469
|
+
{ name: 'lint', conclusion: 'failure' },
|
|
470
|
+
])
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
vi.doMock('child_process', () => ({ execSync: mockExecSync }));
|
|
474
|
+
|
|
475
|
+
const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
|
|
476
|
+
const result = await autoMergeApprovedPRs('/mock/root', dbClient);
|
|
477
|
+
|
|
478
|
+
// Should not merge — 'build' is a new failure
|
|
479
|
+
expect(result).toBe(0);
|
|
480
|
+
// PR should be reset to approved (ci_blocked outcome)
|
|
481
|
+
expect(getPullRequestById(db, pr.id)?.status).toBe('approved');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should skip BLOCKED PR when allow_preexisting_ci_failures is false', async () => {
|
|
485
|
+
const pr = createPullRequest(db, {
|
|
486
|
+
storyId,
|
|
487
|
+
teamId,
|
|
488
|
+
branchName: 'feature/ci-blocked-no-bypass',
|
|
489
|
+
githubPrNumber: 557,
|
|
490
|
+
});
|
|
491
|
+
updatePullRequest(db, pr.id, { status: 'approved' });
|
|
492
|
+
|
|
493
|
+
mockLoadConfig.mockReturnValue({
|
|
494
|
+
integrations: {
|
|
495
|
+
autonomy: { level: 'full', allow_preexisting_ci_failures: false },
|
|
496
|
+
source_control: { provider: 'github' },
|
|
497
|
+
project_management: { provider: 'none' },
|
|
498
|
+
},
|
|
499
|
+
} as any);
|
|
500
|
+
|
|
501
|
+
const mockExecSync = vi.fn();
|
|
502
|
+
// gh pr view → BLOCKED but MERGEABLE
|
|
503
|
+
mockExecSync.mockReturnValueOnce(
|
|
504
|
+
JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BLOCKED' })
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
vi.doMock('child_process', () => ({ execSync: mockExecSync }));
|
|
508
|
+
|
|
509
|
+
const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
|
|
510
|
+
const result = await autoMergeApprovedPRs('/mock/root', dbClient);
|
|
511
|
+
|
|
512
|
+
expect(result).toBe(0);
|
|
513
|
+
expect(getPullRequestById(db, pr.id)?.status).toBe('approved');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('checkPreexistingCIFailures', () => {
|
|
518
|
+
it('should return bypassed=true when all PR failures exist on base branch', () => {
|
|
519
|
+
const mockExecSync = vi.fn();
|
|
520
|
+
// gh pr checks
|
|
521
|
+
mockExecSync.mockReturnValueOnce(
|
|
522
|
+
JSON.stringify([
|
|
523
|
+
{ name: 'build', state: 'SUCCESS' },
|
|
524
|
+
{ name: 'lint', state: 'FAIL' },
|
|
525
|
+
{ name: 'test', state: 'FAIL' },
|
|
526
|
+
])
|
|
527
|
+
);
|
|
528
|
+
// gh pr view baseRefName
|
|
529
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ baseRefName: 'main' }));
|
|
530
|
+
// gh api check-runs
|
|
531
|
+
mockExecSync.mockReturnValueOnce(
|
|
532
|
+
JSON.stringify([
|
|
533
|
+
{ name: 'build', conclusion: 'success' },
|
|
534
|
+
{ name: 'lint', conclusion: 'failure' },
|
|
535
|
+
{ name: 'test', conclusion: 'failure' },
|
|
536
|
+
])
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const result = checkPreexistingCIFailures(
|
|
540
|
+
123,
|
|
541
|
+
'/repo',
|
|
542
|
+
'',
|
|
543
|
+
'owner/repo',
|
|
544
|
+
mockExecSync as any
|
|
545
|
+
);
|
|
546
|
+
expect(result.bypassed).toBe(true);
|
|
547
|
+
expect(result.bypassedChecks).toEqual(expect.arrayContaining(['lint', 'test']));
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should return bypassed=false when PR has unique failures', () => {
|
|
551
|
+
const mockExecSync = vi.fn();
|
|
552
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify([{ name: 'build', state: 'FAIL' }]));
|
|
553
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ baseRefName: 'main' }));
|
|
554
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify([{ name: 'build', conclusion: 'success' }]));
|
|
555
|
+
|
|
556
|
+
const result = checkPreexistingCIFailures(
|
|
557
|
+
123,
|
|
558
|
+
'/repo',
|
|
559
|
+
'',
|
|
560
|
+
'owner/repo',
|
|
561
|
+
mockExecSync as any
|
|
562
|
+
);
|
|
563
|
+
expect(result.bypassed).toBe(false);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should return bypassed=false when no PR checks are failing', () => {
|
|
567
|
+
const mockExecSync = vi.fn();
|
|
568
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify([{ name: 'build', state: 'SUCCESS' }]));
|
|
569
|
+
|
|
570
|
+
const result = checkPreexistingCIFailures(
|
|
571
|
+
123,
|
|
572
|
+
'/repo',
|
|
573
|
+
'',
|
|
574
|
+
'owner/repo',
|
|
575
|
+
mockExecSync as any
|
|
576
|
+
);
|
|
577
|
+
expect(result.bypassed).toBe(false);
|
|
578
|
+
expect(result.bypassedChecks).toEqual([]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should return bypassed=false when execSync throws', () => {
|
|
582
|
+
const mockExecSync = vi.fn().mockImplementation(() => {
|
|
583
|
+
throw new Error('gh not found');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const result = checkPreexistingCIFailures(
|
|
587
|
+
123,
|
|
588
|
+
'/repo',
|
|
589
|
+
'',
|
|
590
|
+
'owner/repo',
|
|
591
|
+
mockExecSync as any
|
|
592
|
+
);
|
|
593
|
+
expect(result.bypassed).toBe(false);
|
|
594
|
+
});
|
|
383
595
|
});
|
|
384
596
|
});
|
package/src/utils/auto-merge.ts
CHANGED
|
@@ -44,9 +44,83 @@ interface ClaimedPR {
|
|
|
44
44
|
pr: PullRequestRow;
|
|
45
45
|
repoCwd: string;
|
|
46
46
|
repoFlag: string;
|
|
47
|
+
repoSlug: string | null;
|
|
47
48
|
teamId: string | null;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Compare CI check failures between a PR and its base branch.
|
|
53
|
+
* Returns bypassed=true when every failing check on the PR also fails on the base branch.
|
|
54
|
+
*/
|
|
55
|
+
export function checkPreexistingCIFailures(
|
|
56
|
+
prNumber: number,
|
|
57
|
+
repoCwd: string,
|
|
58
|
+
repoFlag: string,
|
|
59
|
+
repoSlug: string | null,
|
|
60
|
+
execSyncFn: typeof import('child_process').execSync
|
|
61
|
+
): { bypassed: boolean; bypassedChecks: string[] } {
|
|
62
|
+
try {
|
|
63
|
+
// Get failing checks on the PR
|
|
64
|
+
const prChecksRaw = execSyncFn(`gh pr checks ${prNumber} --json name,state${repoFlag}`, {
|
|
65
|
+
stdio: 'pipe',
|
|
66
|
+
cwd: repoCwd,
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
timeout: PR_STATE_CHECK_TIMEOUT_MS,
|
|
69
|
+
}) as string;
|
|
70
|
+
const prChecks: Array<{ name: string; state: string }> = JSON.parse(prChecksRaw);
|
|
71
|
+
const prFailingNames = new Set(prChecks.filter(c => c.state === 'FAIL').map(c => c.name));
|
|
72
|
+
|
|
73
|
+
if (prFailingNames.size === 0) return { bypassed: false, bypassedChecks: [] };
|
|
74
|
+
|
|
75
|
+
// Get the base branch ref
|
|
76
|
+
const baseRefRaw = execSyncFn(`gh pr view ${prNumber} --json baseRefName${repoFlag}`, {
|
|
77
|
+
stdio: 'pipe',
|
|
78
|
+
cwd: repoCwd,
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
timeout: PR_STATE_CHECK_TIMEOUT_MS,
|
|
81
|
+
}) as string;
|
|
82
|
+
const { baseRefName } = JSON.parse(baseRefRaw);
|
|
83
|
+
|
|
84
|
+
// Determine repo slug for API call
|
|
85
|
+
let slug = repoSlug;
|
|
86
|
+
if (!slug) {
|
|
87
|
+
try {
|
|
88
|
+
slug = (
|
|
89
|
+
execSyncFn('gh repo view --json nameWithOwner -q .nameWithOwner', {
|
|
90
|
+
stdio: 'pipe',
|
|
91
|
+
cwd: repoCwd,
|
|
92
|
+
encoding: 'utf-8',
|
|
93
|
+
timeout: PR_STATE_CHECK_TIMEOUT_MS,
|
|
94
|
+
}) as string
|
|
95
|
+
).trim();
|
|
96
|
+
} catch {
|
|
97
|
+
return { bypassed: false, bypassedChecks: [] };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get check runs on the base branch
|
|
102
|
+
const baseChecksRaw = execSyncFn(
|
|
103
|
+
`gh api repos/${slug}/commits/${baseRefName}/check-runs --jq '.check_runs'`,
|
|
104
|
+
{ stdio: 'pipe', cwd: repoCwd, encoding: 'utf-8', timeout: PR_STATE_CHECK_TIMEOUT_MS }
|
|
105
|
+
) as string;
|
|
106
|
+
const baseCheckRuns: Array<{ name: string; conclusion: string }> = JSON.parse(baseChecksRaw);
|
|
107
|
+
const baseFailingNames = new Set(
|
|
108
|
+
baseCheckRuns.filter(c => c.conclusion === 'failure').map(c => c.name)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Check if all PR failures also fail on base
|
|
112
|
+
const prOnlyFailures = [...prFailingNames].filter(name => !baseFailingNames.has(name));
|
|
113
|
+
|
|
114
|
+
if (prOnlyFailures.length === 0) {
|
|
115
|
+
return { bypassed: true, bypassedChecks: [...prFailingNames] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { bypassed: false, bypassedChecks: [] };
|
|
119
|
+
} catch {
|
|
120
|
+
return { bypassed: false, bypassedChecks: [] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
50
124
|
/**
|
|
51
125
|
* Auto-merge all approved PRs that are ready to merge.
|
|
52
126
|
* Can be called immediately after PR approval or from manager daemon.
|
|
@@ -130,6 +204,7 @@ export async function autoMergeApprovedPRs(
|
|
|
130
204
|
pr,
|
|
131
205
|
repoCwd,
|
|
132
206
|
repoFlag: repoSlug ? ` -R ${repoSlug}` : '',
|
|
207
|
+
repoSlug,
|
|
133
208
|
teamId,
|
|
134
209
|
});
|
|
135
210
|
}
|
|
@@ -154,6 +229,8 @@ export async function autoMergeApprovedPRs(
|
|
|
154
229
|
| { type: 'branch_updated' }
|
|
155
230
|
| { type: 'already_closed'; prState: GitHubPRState }
|
|
156
231
|
| { type: 'conflicts' }
|
|
232
|
+
| { type: 'ci_blocked' }
|
|
233
|
+
| { type: 'ci_bypassed'; bypassedChecks: string[] }
|
|
157
234
|
| { type: 'unknown_state' }
|
|
158
235
|
| { type: 'merge_failed'; error: Error };
|
|
159
236
|
}
|
|
@@ -207,6 +284,44 @@ export async function autoMergeApprovedPRs(
|
|
|
207
284
|
continue;
|
|
208
285
|
}
|
|
209
286
|
|
|
287
|
+
// Handle BLOCKED mergeStateStatus (CI checks failing)
|
|
288
|
+
if (prState.mergeStateStatus === 'BLOCKED') {
|
|
289
|
+
if (config.integrations.autonomy.allow_preexisting_ci_failures) {
|
|
290
|
+
const ciResult = checkPreexistingCIFailures(
|
|
291
|
+
pr.github_pr_number!,
|
|
292
|
+
repoCwd,
|
|
293
|
+
repoFlag,
|
|
294
|
+
claimed.repoSlug,
|
|
295
|
+
execSync
|
|
296
|
+
);
|
|
297
|
+
if (ciResult.bypassed) {
|
|
298
|
+
// All CI failures also exist on the base branch — attempt merge with admin bypass
|
|
299
|
+
try {
|
|
300
|
+
execSync(
|
|
301
|
+
`gh pr merge ${pr.github_pr_number} --squash --delete-branch --admin${repoFlag}`,
|
|
302
|
+
{ stdio: 'pipe', cwd: repoCwd, timeout: PR_MERGE_TIMEOUT_MS }
|
|
303
|
+
);
|
|
304
|
+
results.push({
|
|
305
|
+
claimed,
|
|
306
|
+
outcome: { type: 'ci_bypassed', bypassedChecks: ciResult.bypassedChecks },
|
|
307
|
+
});
|
|
308
|
+
} catch (mergeErr) {
|
|
309
|
+
results.push({
|
|
310
|
+
claimed,
|
|
311
|
+
outcome: {
|
|
312
|
+
type: 'merge_failed',
|
|
313
|
+
error: mergeErr instanceof Error ? mergeErr : new Error(String(mergeErr)),
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// New CI failures or config disabled — skip
|
|
321
|
+
results.push({ claimed, outcome: { type: 'ci_blocked' } });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
210
325
|
// Attempt merge
|
|
211
326
|
try {
|
|
212
327
|
execSync(`gh pr merge ${pr.github_pr_number} --auto --squash --delete-branch${repoFlag}`, {
|
|
@@ -337,6 +452,70 @@ export async function autoMergeApprovedPRs(
|
|
|
337
452
|
break;
|
|
338
453
|
}
|
|
339
454
|
|
|
455
|
+
case 'ci_blocked': {
|
|
456
|
+
await withTransaction(
|
|
457
|
+
phaseDb.db,
|
|
458
|
+
() => {
|
|
459
|
+
updatePullRequest(phaseDb.db, pr.id, { status: 'approved' });
|
|
460
|
+
createLog(phaseDb.db, {
|
|
461
|
+
agentId: 'manager',
|
|
462
|
+
storyId: pr.story_id || undefined,
|
|
463
|
+
eventType: 'PR_MERGE_SKIPPED',
|
|
464
|
+
status: 'warn',
|
|
465
|
+
message: `Skipped auto-merge of PR #${pr.github_pr_number}: CI checks are failing (not pre-existing on base branch)`,
|
|
466
|
+
metadata: { pr_id: pr.id },
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
() => phaseDb.save()
|
|
470
|
+
);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
case 'ci_bypassed': {
|
|
475
|
+
const storyId = pr.story_id;
|
|
476
|
+
const bypassedChecks = result.outcome.bypassedChecks;
|
|
477
|
+
await withTransaction(
|
|
478
|
+
phaseDb.db,
|
|
479
|
+
() => {
|
|
480
|
+
updatePullRequest(phaseDb.db, pr.id, { status: 'merged' });
|
|
481
|
+
if (storyId) {
|
|
482
|
+
updateStory(phaseDb.db, storyId, { status: 'merged' });
|
|
483
|
+
const story = getStoryById(phaseDb.db, storyId);
|
|
484
|
+
if (story?.assigned_agent_id) {
|
|
485
|
+
const agent = getAgentById(phaseDb.db, story.assigned_agent_id);
|
|
486
|
+
if (agent && agent.current_story_id === storyId) {
|
|
487
|
+
updateAgent(phaseDb.db, agent.id, { currentStoryId: null, status: 'idle' });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
createLog(phaseDb.db, {
|
|
491
|
+
agentId: 'manager',
|
|
492
|
+
storyId,
|
|
493
|
+
eventType: 'STORY_MERGED',
|
|
494
|
+
message: `Story auto-merged from GitHub PR #${pr.github_pr_number} (bypassed pre-existing CI failures: ${bypassedChecks.join(', ')})`,
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
createLog(phaseDb.db, {
|
|
498
|
+
agentId: 'manager',
|
|
499
|
+
eventType: 'PR_MERGED',
|
|
500
|
+
message: `PR ${pr.id} auto-merged (GitHub PR #${pr.github_pr_number}, bypassed pre-existing CI failures: ${bypassedChecks.join(', ')})`,
|
|
501
|
+
metadata: { pr_id: pr.id },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
() => phaseDb.save()
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
mergedCount++;
|
|
509
|
+
|
|
510
|
+
if (storyId) {
|
|
511
|
+
postLifecycleComment(phaseDb.db, paths.hiveDir, config, storyId, 'merged').catch(() => {
|
|
512
|
+
/* non-fatal */
|
|
513
|
+
});
|
|
514
|
+
syncStatusForStory(root, phaseDb.db, storyId, 'merged');
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
340
519
|
case 'merged': {
|
|
341
520
|
const storyId = pr.story_id;
|
|
342
521
|
await withTransaction(
|