@shepai/cli 1.56.0 → 1.57.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 (99) hide show
  1. package/apis/json-schema/NotificationEventConfig.yaml +20 -0
  2. package/apis/json-schema/NotificationEventType.yaml +4 -0
  3. package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts +22 -0
  4. package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts.map +1 -1
  5. package/dist/packages/core/src/domain/factories/settings-defaults.factory.d.ts.map +1 -1
  6. package/dist/packages/core/src/domain/factories/settings-defaults.factory.js +4 -0
  7. package/dist/packages/core/src/domain/generated/output.d.ts +21 -1
  8. package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
  9. package/dist/packages/core/src/domain/generated/output.js +4 -0
  10. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.d.ts +4 -0
  11. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.d.ts.map +1 -1
  12. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.js +8 -0
  13. package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
  14. package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.js +18 -0
  15. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts +3 -1
  16. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts.map +1 -1
  17. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.js +28 -0
  18. package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts.map +1 -1
  19. package/dist/packages/core/src/infrastructure/services/notifications/notification.service.js +4 -0
  20. package/dist/packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.d.ts +61 -0
  21. package/dist/packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.d.ts.map +1 -0
  22. package/dist/packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.js +286 -0
  23. package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
  24. package/dist/src/presentation/cli/commands/ui.command.js +6 -0
  25. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  26. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +6 -0
  27. package/dist/src/presentation/web/dev-server.js +11 -0
  28. package/dist/tsconfig.build.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/web/.next/BUILD_ID +1 -1
  31. package/web/.next/build-manifest.json +2 -2
  32. package/web/.next/cache/.previewinfo +1 -1
  33. package/web/.next/cache/.rscinfo +1 -1
  34. package/web/.next/cache/.tsbuildinfo +1 -1
  35. package/web/.next/cache/config.json +3 -3
  36. package/web/.next/fallback-build-manifest.json +2 -2
  37. package/web/.next/prerender-manifest.json +3 -3
  38. package/web/.next/required-server-files.js +1 -1
  39. package/web/.next/required-server-files.json +1 -1
  40. package/web/.next/server/app/_global-error.html +2 -2
  41. package/web/.next/server/app/_global-error.rsc +1 -1
  42. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  43. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  48. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  49. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  51. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  52. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
  53. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -1
  54. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  55. package/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js +2 -2
  56. package/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js.map +1 -1
  57. package/web/.next/server/chunks/ssr/[root-of-the-server]__fbc89707._.js +1 -1
  58. package/web/.next/server/chunks/ssr/_0e2c2b4f._.js +1 -1
  59. package/web/.next/server/chunks/ssr/_0e2c2b4f._.js.map +1 -1
  60. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  61. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  62. package/web/.next/server/pages/500.html +2 -2
  63. package/web/.next/server/server-reference-manifest.js +1 -1
  64. package/web/.next/server/server-reference-manifest.json +15 -15
  65. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  66. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  67. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  68. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  69. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  70. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  71. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  73. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  77. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  78. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  79. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  80. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  81. package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
  82. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  83. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js +2 -2
  84. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__fbc89707._.js +1 -1
  85. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_0e2c2b4f._.js +1 -1
  86. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  87. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  88. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  89. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  90. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +6 -0
  91. package/web/.next/standalone/src/presentation/web/dev-server.ts +15 -0
  92. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  93. package/web/.next/static/chunks/{62eba5d9a5e28761.js → 225de19e886a62ab.js} +2 -2
  94. package/web/.next/static/chunks/{ca4bb930b4a3f3a9.js → a1f2befd62b321c0.js} +1 -1
  95. package/web/.next/trace +1 -1
  96. package/web/.next/trace-build +1 -1
  97. /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → V2579MPjpJ_hqKjPsmMlR}/_buildManifest.js +0 -0
  98. /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → V2579MPjpJ_hqKjPsmMlR}/_clientMiddlewareManifest.json +0 -0
  99. /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → V2579MPjpJ_hqKjPsmMlR}/_ssgManifest.js +0 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * PR Sync Watcher Service
3
+ *
4
+ * Polls GitHub PR status and CI status for features in the Review lifecycle
5
+ * stage, updating feature records and emitting notifications when transitions
6
+ * are detected. Follows the NotificationWatcherService polling pattern.
7
+ *
8
+ * Maintains in-memory tracking of last-known PR and CI status per feature
9
+ * to avoid duplicate updates and notifications. Features are grouped by
10
+ * repositoryPath for batch `gh pr list` queries.
11
+ */
12
+ import { SdlcLifecycle, PrStatus, CiStatus, AgentRunStatus, NotificationEventType, NotificationSeverity, } from '../../../domain/generated/output.js';
13
+ const DEFAULT_POLL_INTERVAL_MS = 3000;
14
+ const TAG = '[PrSyncWatcher]';
15
+ const CI_STATUS_MAP = {
16
+ success: CiStatus.Success,
17
+ failure: CiStatus.Failure,
18
+ pending: CiStatus.Pending,
19
+ };
20
+ export class PrSyncWatcherService {
21
+ featureRepo;
22
+ agentRunRepo;
23
+ gitPrService;
24
+ notificationService;
25
+ pollIntervalMs;
26
+ trackedFeatures = new Map();
27
+ skippedRepos = new Set();
28
+ intervalId = null;
29
+ constructor(featureRepo, agentRunRepo, gitPrService, notificationService, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
30
+ this.featureRepo = featureRepo;
31
+ this.agentRunRepo = agentRunRepo;
32
+ this.gitPrService = gitPrService;
33
+ this.notificationService = notificationService;
34
+ this.pollIntervalMs = pollIntervalMs;
35
+ }
36
+ isRunning() {
37
+ return this.intervalId !== null;
38
+ }
39
+ start() {
40
+ if (this.intervalId !== null)
41
+ return;
42
+ // eslint-disable-next-line no-console
43
+ console.log(`${TAG} Starting (poll every ${this.pollIntervalMs}ms)`);
44
+ // Run first poll immediately
45
+ void this.poll();
46
+ this.intervalId = setInterval(() => {
47
+ void this.poll();
48
+ }, this.pollIntervalMs);
49
+ }
50
+ stop() {
51
+ if (this.intervalId !== null) {
52
+ clearInterval(this.intervalId);
53
+ this.intervalId = null;
54
+ // eslint-disable-next-line no-console
55
+ console.log(`${TAG} Stopped`);
56
+ }
57
+ }
58
+ async poll() {
59
+ try {
60
+ const allFeatures = await this.featureRepo.list({ lifecycle: SdlcLifecycle.Review });
61
+ // Include features with a valid repositoryPath (with or without PR data)
62
+ const features = allFeatures.filter((f) => f.repositoryPath);
63
+ if (features.length === 0) {
64
+ this.trackedFeatures.clear();
65
+ return;
66
+ }
67
+ // Group features by repositoryPath for batch queries
68
+ const byRepo = new Map();
69
+ for (const feature of features) {
70
+ const group = byRepo.get(feature.repositoryPath) ?? [];
71
+ group.push(feature);
72
+ byRepo.set(feature.repositoryPath, group);
73
+ }
74
+ // Process each repository
75
+ for (const [repoPath, repoFeatures] of byRepo) {
76
+ await this.processRepository(repoPath, repoFeatures);
77
+ }
78
+ // Prune features no longer in Review
79
+ const currentFeatureIds = new Set(features.map((f) => f.id));
80
+ for (const trackedId of this.trackedFeatures.keys()) {
81
+ if (!currentFeatureIds.has(trackedId)) {
82
+ this.trackedFeatures.delete(trackedId);
83
+ }
84
+ }
85
+ }
86
+ catch (error) {
87
+ const msg = error instanceof Error ? error.message : String(error);
88
+ // eslint-disable-next-line no-console
89
+ console.warn(`${TAG} Poll failed: ${msg}`);
90
+ }
91
+ }
92
+ async processRepository(repoPath, features) {
93
+ if (this.skippedRepos.has(repoPath))
94
+ return;
95
+ // Skip repos without a git remote — gh pr list will always fail
96
+ try {
97
+ if (!(await this.gitPrService.hasRemote(repoPath))) {
98
+ // eslint-disable-next-line no-console
99
+ console.log(`${TAG} Skipping ${repoPath} (no git remote)`);
100
+ this.skippedRepos.add(repoPath);
101
+ return;
102
+ }
103
+ }
104
+ catch {
105
+ return;
106
+ }
107
+ let prStatuses;
108
+ try {
109
+ prStatuses = await this.gitPrService.listPrStatuses(repoPath);
110
+ }
111
+ catch (error) {
112
+ const msg = error instanceof Error ? error.message : String(error);
113
+ // eslint-disable-next-line no-console
114
+ console.warn(`${TAG} listPrStatuses failed for ${repoPath}: ${msg}`);
115
+ return;
116
+ }
117
+ // Build lookups by PR number and by head branch name.
118
+ // Multiple PRs can share the same headRefName (e.g. old merged PR + new open PR).
119
+ // Prefer Open PRs so we don't match stale merged/closed ones.
120
+ const statusByNumber = new Map();
121
+ const statusByBranch = new Map();
122
+ for (const pr of prStatuses) {
123
+ statusByNumber.set(pr.number, pr);
124
+ if (pr.headRefName) {
125
+ const existing = statusByBranch.get(pr.headRefName);
126
+ if (!existing || (existing.state !== PrStatus.Open && pr.state === PrStatus.Open)) {
127
+ statusByBranch.set(pr.headRefName, pr);
128
+ }
129
+ }
130
+ }
131
+ for (const feature of features) {
132
+ await this.processFeature(feature, statusByNumber, statusByBranch);
133
+ }
134
+ }
135
+ async processFeature(feature, statusByNumber, statusByBranch) {
136
+ // If feature has no PR data, try to discover an Open PR by matching branch name.
137
+ // Only link Open PRs — merged/closed PRs with the same branch are likely stale
138
+ // from a previous iteration. The watcher will catch Open→Merged transitions
139
+ // on subsequent polls once the PR is linked.
140
+ if (!feature.pr) {
141
+ const matchedPr = statusByBranch.get(feature.branch);
142
+ if (matchedPr?.state !== PrStatus.Open)
143
+ return;
144
+ // eslint-disable-next-line no-console
145
+ console.log(`${TAG} Discovered PR #${matchedPr.number} for "${feature.name}" via branch "${feature.branch}"`);
146
+ feature.pr = {
147
+ url: matchedPr.url,
148
+ number: matchedPr.number,
149
+ status: PrStatus.Open,
150
+ };
151
+ this.trackedFeatures.set(feature.id, {
152
+ prStatus: PrStatus.Open,
153
+ ciStatus: undefined,
154
+ featureName: feature.name,
155
+ });
156
+ await this.featureRepo.update(feature);
157
+ return;
158
+ }
159
+ const pr = feature.pr;
160
+ const prStatusInfo = statusByNumber.get(pr.number);
161
+ let needsUpdate = false;
162
+ // Initialize or retrieve tracked state
163
+ const prevState = this.trackedFeatures.get(feature.id);
164
+ if (!prevState) {
165
+ // First time seeing this feature — initialize tracking
166
+ this.trackedFeatures.set(feature.id, {
167
+ prStatus: pr.status,
168
+ ciStatus: pr.ciStatus,
169
+ featureName: feature.name,
170
+ });
171
+ }
172
+ const tracked = this.trackedFeatures.get(feature.id);
173
+ // Check PR status transition
174
+ if (prStatusInfo && prStatusInfo.state !== tracked.prStatus) {
175
+ const newPrStatus = prStatusInfo.state;
176
+ // eslint-disable-next-line no-console
177
+ console.log(`${TAG} PR #${pr.number} status changed: ${tracked.prStatus} -> ${newPrStatus} for "${feature.name}"`);
178
+ feature.pr = { ...pr, status: newPrStatus };
179
+ if (newPrStatus === PrStatus.Merged) {
180
+ feature.lifecycle = SdlcLifecycle.Maintain;
181
+ await this.completeAgentRun(feature);
182
+ this.emitNotification(NotificationEventType.PrMerged, feature.id, feature.agentRunId ?? '', feature.name, `PR #${pr.number} merged for ${feature.name}`, NotificationSeverity.Success);
183
+ }
184
+ else if (newPrStatus === PrStatus.Closed) {
185
+ this.emitNotification(NotificationEventType.PrClosed, feature.id, feature.agentRunId ?? '', feature.name, `PR #${pr.number} closed for ${feature.name}`, NotificationSeverity.Warning);
186
+ }
187
+ tracked.prStatus = newPrStatus;
188
+ needsUpdate = true;
189
+ }
190
+ // Check CI status transition
191
+ try {
192
+ const ciResult = await this.gitPrService.getCiStatus(feature.repositoryPath, feature.branch);
193
+ const newCiStatus = CI_STATUS_MAP[ciResult.status] ?? CiStatus.Pending;
194
+ if (newCiStatus !== tracked.ciStatus) {
195
+ // eslint-disable-next-line no-console
196
+ console.log(`${TAG} CI status changed: ${tracked.ciStatus ?? 'none'} -> ${newCiStatus} for "${feature.name}"`);
197
+ feature.pr = { ...(feature.pr ?? pr), ciStatus: newCiStatus };
198
+ if (newCiStatus === CiStatus.Success) {
199
+ this.emitNotification(NotificationEventType.PrChecksPassed, feature.id, feature.agentRunId ?? '', feature.name, `CI checks passed for ${feature.name}`, NotificationSeverity.Success);
200
+ }
201
+ else if (newCiStatus === CiStatus.Failure) {
202
+ this.emitNotification(NotificationEventType.PrChecksFailed, feature.id, feature.agentRunId ?? '', feature.name, `CI checks failed for ${feature.name}`, NotificationSeverity.Error);
203
+ }
204
+ tracked.ciStatus = newCiStatus;
205
+ needsUpdate = true;
206
+ }
207
+ }
208
+ catch (error) {
209
+ const msg = error instanceof Error ? error.message : String(error);
210
+ // eslint-disable-next-line no-console
211
+ console.warn(`${TAG} getCiStatus failed for "${feature.name}": ${msg}`);
212
+ }
213
+ if (needsUpdate) {
214
+ await this.featureRepo.update(feature);
215
+ }
216
+ }
217
+ /** Mark associated agent run as completed so the UI reflects "done" state. */
218
+ async completeAgentRun(feature) {
219
+ if (!feature.agentRunId)
220
+ return;
221
+ try {
222
+ await this.agentRunRepo.updateStatus(feature.agentRunId, AgentRunStatus.completed);
223
+ }
224
+ catch (error) {
225
+ const msg = error instanceof Error ? error.message : String(error);
226
+ // eslint-disable-next-line no-console
227
+ console.warn(`${TAG} Failed to complete agent run ${feature.agentRunId}: ${msg}`);
228
+ }
229
+ }
230
+ emitNotification(eventType, featureId, agentRunId, featureName, message, severity) {
231
+ const event = {
232
+ eventType,
233
+ featureId,
234
+ agentRunId,
235
+ featureName,
236
+ message,
237
+ severity,
238
+ timestamp: new Date().toISOString(),
239
+ };
240
+ this.notificationService.notify(event);
241
+ }
242
+ }
243
+ // --- Singleton accessors (follows NotificationWatcherService pattern) ---
244
+ let watcherInstance = null;
245
+ /**
246
+ * Initialize the PR sync watcher singleton.
247
+ * Must be called once during web server startup.
248
+ *
249
+ * @throws Error if the watcher is already initialized
250
+ */
251
+ export function initializePrSyncWatcher(featureRepo, agentRunRepo, gitPrService, notificationService, pollIntervalMs) {
252
+ if (watcherInstance !== null) {
253
+ throw new Error('PR sync watcher already initialized. Cannot re-initialize.');
254
+ }
255
+ watcherInstance = new PrSyncWatcherService(featureRepo, agentRunRepo, gitPrService, notificationService, pollIntervalMs);
256
+ }
257
+ /**
258
+ * Get the PR sync watcher singleton.
259
+ *
260
+ * @returns The PR sync watcher service
261
+ * @throws Error if the watcher hasn't been initialized yet
262
+ */
263
+ export function getPrSyncWatcher() {
264
+ if (watcherInstance === null) {
265
+ throw new Error('PR sync watcher not initialized. Call initializePrSyncWatcher() during web server startup.');
266
+ }
267
+ return watcherInstance;
268
+ }
269
+ /**
270
+ * Check if the PR sync watcher has been initialized.
271
+ */
272
+ export function hasPrSyncWatcher() {
273
+ return watcherInstance !== null;
274
+ }
275
+ /**
276
+ * Reset the PR sync watcher singleton (for testing purposes only).
277
+ * Stops the watcher if running before resetting.
278
+ *
279
+ * @internal
280
+ */
281
+ export function resetPrSyncWatcher() {
282
+ if (watcherInstance !== null) {
283
+ watcherInstance.stop();
284
+ }
285
+ watcherInstance = null;
286
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ui.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/ui.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AA0B1D;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CA4EzC"}
1
+ {"version":3,"file":"ui.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/ui.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AA+B1D;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAkFzC"}
@@ -20,6 +20,7 @@ import { container } from '../../../../packages/core/src/infrastructure/di/conta
20
20
  import { setVersionEnvVars } from '../../../../packages/core/src/infrastructure/services/version.service.js';
21
21
  import { resolveWebDir } from '../../../../packages/core/src/infrastructure/services/web-server.service.js';
22
22
  import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
23
+ import { initializePrSyncWatcher, getPrSyncWatcher, } from '../../../../packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.js';
23
24
  import { BrowserOpenerService } from '../../../../packages/core/src/infrastructure/services/browser-opener.service.js';
24
25
  import { colors, fmt, messages } from '../ui/index.js';
25
26
  function parsePort(value) {
@@ -63,6 +64,10 @@ Examples:
63
64
  const notificationService = container.resolve('INotificationService');
64
65
  initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
65
66
  getNotificationWatcher().start();
67
+ // Start PR sync watcher to detect PR/CI status transitions on GitHub
68
+ const gitPrService = container.resolve('IGitPrService');
69
+ initializePrSyncWatcher(featureRepo, runRepo, gitPrService, notificationService);
70
+ getPrSyncWatcher().start();
66
71
  const url = `http://localhost:${port}`;
67
72
  messages.success(`Server ready at ${fmt.code(url)}`);
68
73
  messages.info('Press Ctrl+C to stop');
@@ -84,6 +89,7 @@ Examples:
84
89
  // Force exit after 5s if graceful shutdown stalls
85
90
  const forceExit = setTimeout(() => process.exit(0), 5000);
86
91
  forceExit.unref();
92
+ getPrSyncWatcher().stop();
87
93
  getNotificationWatcher().stop();
88
94
  await service.stop();
89
95
  process.exit(0);
@@ -1 +1 @@
1
- {"version":3,"file":"derive-feature-state.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/derive-feature-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAIL,qBAAqB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,gBAAgB,CA4C9F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAYvD;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,GAAG,gBAAgB,CAYtF;AAYD,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,qBAAqB,GAAG,SAAS,CAGnC"}
1
+ {"version":3,"file":"derive-feature-state.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/derive-feature-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAIL,qBAAqB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,gBAAgB,CA4C9F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAYvD;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,GAAG,gBAAgB,CAkBtF;AAYD,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,qBAAqB,GAAG,SAAS,CAGnC"}
@@ -84,7 +84,13 @@ export function mapEventTypeToState(eventType) {
84
84
  case NotificationEventType.AgentCompleted:
85
85
  return 'done';
86
86
  case NotificationEventType.AgentFailed:
87
+ case NotificationEventType.PrChecksFailed:
87
88
  return 'error';
89
+ case NotificationEventType.PrMerged:
90
+ case NotificationEventType.PrChecksPassed:
91
+ return 'done';
92
+ case NotificationEventType.PrClosed:
93
+ return 'action-required';
88
94
  }
89
95
  }
90
96
  /** Maps an SSE event phaseName to a FeatureLifecyclePhase. Mirrors page.tsx nodeToLifecyclePhase. */
@@ -18,6 +18,7 @@ import { initializeContainer, container } from '../../../packages/core/src/infra
18
18
  import { InitializeSettingsUseCase } from '../../../packages/core/src/application/use-cases/settings/initialize-settings.use-case.js';
19
19
  import { initializeSettings } from '../../../packages/core/src/infrastructure/services/settings.service.js';
20
20
  import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
21
+ import { initializePrSyncWatcher, getPrSyncWatcher, } from '../../../packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.js';
21
22
  const DEFAULT_PORT = 3000;
22
23
  async function isPortAvailable(port) {
23
24
  return new Promise((resolve) => {
@@ -61,6 +62,10 @@ async function main() {
61
62
  const notificationService = container.resolve('INotificationService');
62
63
  initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
63
64
  getNotificationWatcher().start();
65
+ // Start PR sync watcher to detect PR/CI status transitions on GitHub
66
+ const gitPrService = container.resolve('IGitPrService');
67
+ initializePrSyncWatcher(featureRepo, runRepo, gitPrService, notificationService);
68
+ getPrSyncWatcher().start();
64
69
  }
65
70
  catch (error) {
66
71
  console.warn('[dev-server] DI initialization failed — features will be empty:', error);
@@ -106,6 +111,12 @@ async function main() {
106
111
  catch {
107
112
  /* not initialized */
108
113
  }
114
+ try {
115
+ getPrSyncWatcher().stop();
116
+ }
117
+ catch {
118
+ /* not initialized */
119
+ }
109
120
  server.closeAllConnections();
110
121
  await Promise.all([
111
122
  new Promise((resolve) => server.close(() => resolve())),