@shepai/cli 1.56.0 → 1.58.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 (195) 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/add-repository-node/add-repository-button.d.ts.map +1 -1
  26. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.js +4 -2
  27. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.d.ts +2 -0
  28. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.d.ts.map +1 -1
  29. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.js +6 -0
  30. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  31. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +6 -0
  32. package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
  33. package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +9 -0
  34. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.d.ts.map +1 -1
  35. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.js +3 -3
  36. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts +0 -1
  37. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts.map +1 -1
  38. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.js +1 -31
  39. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.d.ts.map +1 -1
  40. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +5 -1
  41. package/dist/src/presentation/web/dev-server.js +11 -0
  42. package/dist/tsconfig.build.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/web/.next/BUILD_ID +1 -1
  45. package/web/.next/build-manifest.json +2 -2
  46. package/web/.next/cache/.previewinfo +1 -1
  47. package/web/.next/cache/.rscinfo +1 -1
  48. package/web/.next/cache/.tsbuildinfo +1 -1
  49. package/web/.next/cache/config.json +3 -3
  50. package/web/.next/fallback-build-manifest.json +2 -2
  51. package/web/.next/prerender-manifest.json +3 -3
  52. package/web/.next/required-server-files.js +1 -1
  53. package/web/.next/required-server-files.json +1 -1
  54. package/web/.next/server/app/_global-error/page.js +1 -1
  55. package/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  56. package/web/.next/server/app/_global-error.html +2 -2
  57. package/web/.next/server/app/_global-error.rsc +1 -1
  58. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  59. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  60. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  61. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  62. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  63. package/web/.next/server/app/_not-found/page/server-reference-manifest.json +17 -1
  64. package/web/.next/server/app/_not-found/page.js +2 -2
  65. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  66. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  67. package/web/.next/server/app/api/dialog/pick-files/route.js +1 -1
  68. package/web/.next/server/app/api/dialog/pick-files/route.js.nft.json +1 -1
  69. package/web/.next/server/app/page/server-reference-manifest.json +41 -41
  70. package/web/.next/server/app/page.js +2 -3
  71. package/web/.next/server/app/page.js.nft.json +1 -1
  72. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/web/.next/server/app/tools/page/server-reference-manifest.json +17 -1
  74. package/web/.next/server/app/tools/page.js +2 -2
  75. package/web/.next/server/app/tools/page.js.nft.json +1 -1
  76. package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  77. package/web/.next/server/app/version/page/server-reference-manifest.json +17 -1
  78. package/web/.next/server/app/version/page.js +2 -2
  79. package/web/.next/server/app/version/page.js.nft.json +1 -1
  80. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  81. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
  82. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -1
  83. package/web/.next/server/chunks/{[root-of-the-server]__332c8d91._.js → [root-of-the-server]__a6bf88cc._.js} +2 -2
  84. package/web/.next/server/chunks/ssr/[root-of-the-server]__08ba9bd3._.js +4 -0
  85. package/web/.next/server/chunks/ssr/[root-of-the-server]__08ba9bd3._.js.map +1 -0
  86. package/web/.next/server/chunks/ssr/[root-of-the-server]__6b17a22d._.js +4 -0
  87. package/web/.next/server/chunks/ssr/[root-of-the-server]__6b17a22d._.js.map +1 -0
  88. package/web/.next/server/chunks/ssr/[root-of-the-server]__804c006d._.js +4 -0
  89. package/web/.next/server/chunks/ssr/[root-of-the-server]__804c006d._.js.map +1 -0
  90. package/web/.next/server/chunks/ssr/[root-of-the-server]__86c7bba5._.js +9 -0
  91. package/web/.next/server/chunks/ssr/[root-of-the-server]__86c7bba5._.js.map +1 -0
  92. package/web/.next/server/chunks/ssr/[root-of-the-server]__9add7c3a._.js +12 -0
  93. package/web/.next/server/chunks/ssr/[root-of-the-server]__9add7c3a._.js.map +1 -0
  94. package/web/.next/server/chunks/ssr/{[root-of-the-server]__dbd220d2._.js → [root-of-the-server]__b79b0516._.js} +2 -2
  95. package/web/.next/server/chunks/ssr/{[root-of-the-server]__dbd220d2._.js.map → [root-of-the-server]__b79b0516._.js.map} +1 -1
  96. package/web/.next/server/chunks/ssr/{[root-of-the-server]__757c7912._.js → [root-of-the-server]__f7f90528._.js} +2 -2
  97. package/web/.next/server/chunks/ssr/{[root-of-the-server]__757c7912._.js.map → [root-of-the-server]__f7f90528._.js.map} +1 -1
  98. package/web/.next/server/chunks/ssr/[root-of-the-server]__fbc89707._.js +1 -1
  99. package/web/.next/server/chunks/ssr/{_0e2c2b4f._.js → _027596cf._.js} +2 -2
  100. package/web/.next/server/chunks/ssr/_027596cf._.js.map +1 -0
  101. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  102. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  103. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  104. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  105. package/web/.next/server/pages/500.html +2 -2
  106. package/web/.next/server/server-reference-manifest.js +1 -1
  107. package/web/.next/server/server-reference-manifest.json +63 -42
  108. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  109. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  110. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  111. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  112. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error/page.js +1 -1
  113. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  114. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  115. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  116. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  117. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  118. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  119. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  120. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  121. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page/server-reference-manifest.json +17 -1
  122. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js +2 -2
  123. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  124. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  125. package/web/.next/standalone/src/presentation/web/.next/server/app/api/dialog/pick-files/route.js +1 -1
  126. package/web/.next/standalone/src/presentation/web/.next/server/app/api/dialog/pick-files/route.js.nft.json +1 -1
  127. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +41 -41
  128. package/web/.next/standalone/src/presentation/web/.next/server/app/page.js +2 -3
  129. package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
  130. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  131. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page/server-reference-manifest.json +17 -1
  132. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page.js +2 -2
  133. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page.js.nft.json +1 -1
  134. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  135. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page/server-reference-manifest.json +17 -1
  136. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js +2 -2
  137. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js.nft.json +1 -1
  138. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  139. package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
  140. package/web/.next/standalone/src/presentation/web/.next/server/chunks/{[root-of-the-server]__332c8d91._.js → [root-of-the-server]__a6bf88cc._.js} +2 -2
  141. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__08ba9bd3._.js +4 -0
  142. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6b17a22d._.js +4 -0
  143. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__804c006d._.js +4 -0
  144. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__86c7bba5._.js +9 -0
  145. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__9add7c3a._.js +12 -0
  146. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{[root-of-the-server]__dbd220d2._.js → [root-of-the-server]__b79b0516._.js} +2 -2
  147. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{[root-of-the-server]__757c7912._.js → [root-of-the-server]__f7f90528._.js} +2 -2
  148. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__fbc89707._.js +1 -1
  149. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{_0e2c2b4f._.js → _027596cf._.js} +2 -2
  150. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  151. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  152. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  153. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  154. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +63 -42
  155. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-button.tsx +24 -15
  156. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.tsx +12 -0
  157. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +6 -0
  158. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +10 -0
  159. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx +0 -41
  160. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +2 -7
  161. package/web/.next/standalone/src/presentation/web/components/layouts/app-shell/app-shell.tsx +6 -0
  162. package/web/.next/standalone/src/presentation/web/dev-server.ts +15 -0
  163. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  164. package/web/.next/static/chunks/3f5de337d3fc604f.js +1 -0
  165. package/web/.next/static/chunks/60bbbe7ff679eb4c.css +2 -0
  166. package/web/.next/static/chunks/{ef237b70209d9853.js → 67459433cc866344.js} +1 -1
  167. package/web/.next/static/chunks/eb0e60434c992fc1.js +10 -0
  168. package/web/.next/trace +1 -1
  169. package/web/.next/trace-build +1 -1
  170. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +0 -9
  171. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +0 -1
  172. package/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js +0 -12
  173. package/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js.map +0 -1
  174. package/web/.next/server/chunks/ssr/[root-of-the-server]__950afd5e._.js +0 -3
  175. package/web/.next/server/chunks/ssr/[root-of-the-server]__950afd5e._.js.map +0 -1
  176. package/web/.next/server/chunks/ssr/_0e2c2b4f._.js.map +0 -1
  177. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app__not-found_page_actions_a836bb2a.js +0 -3
  178. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app__not-found_page_actions_a836bb2a.js.map +0 -1
  179. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_f1dd7791.js +0 -3
  180. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_f1dd7791.js.map +0 -1
  181. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_version_page_actions_ea925032.js +0 -3
  182. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_version_page_actions_ea925032.js.map +0 -1
  183. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +0 -9
  184. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js +0 -12
  185. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__950afd5e._.js +0 -3
  186. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app__not-found_page_actions_a836bb2a.js +0 -3
  187. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_f1dd7791.js +0 -3
  188. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_version_page_actions_ea925032.js +0 -3
  189. package/web/.next/static/chunks/4651f20be1a3b7d7.css +0 -2
  190. package/web/.next/static/chunks/62eba5d9a5e28761.js +0 -10
  191. package/web/.next/static/chunks/ca4bb930b4a3f3a9.js +0 -1
  192. /package/web/.next/server/chunks/{[root-of-the-server]__332c8d91._.js.map → [root-of-the-server]__a6bf88cc._.js.map} +0 -0
  193. /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → e7Q45G0MI6cgci9Gn2J-i}/_buildManifest.js +0 -0
  194. /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → e7Q45G0MI6cgci9Gn2J-i}/_clientMiddlewareManifest.json +0 -0
  195. /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → e7Q45G0MI6cgci9Gn2J-i}/_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":"add-repository-button.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/add-repository-node/add-repository-button.tsx"],"names":[],"mappings":"AAMA,UAAU,wBAAwB;IAChC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,EAAE,wBAAwB,2CAgCzE"}
1
+ {"version":3,"file":"add-repository-button.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/add-repository-node/add-repository-button.tsx"],"names":[],"mappings":"AAQA,UAAU,wBAAwB;IAChC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,EAAE,wBAAwB,2CAuCzE"}
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
- import { Plus, Loader2 } from 'lucide-react';
4
+ import { FolderPlus, Loader2 } from 'lucide-react';
5
+ import { Button } from '../../ui/button.js';
6
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip.js';
5
7
  import { pickFolder } from './pick-folder.js';
6
8
  export function AddRepositoryButton({ onSelect }) {
7
9
  const [loading, setLoading] = useState(false);
@@ -19,5 +21,5 @@ export function AddRepositoryButton({ onSelect }) {
19
21
  setLoading(false);
20
22
  }
21
23
  }
22
- return (_jsxs("button", { type: "button", "data-testid": "add-repository-button", onClick: handleClick, disabled: loading, className: "border-muted-foreground/30 flex cursor-pointer items-center gap-2 rounded-full border-2 border-dashed px-3 py-1.5 text-xs transition-colors hover:border-blue-500 hover:text-blue-500 disabled:cursor-wait disabled:opacity-60", children: [loading ? (_jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin" })) : (_jsx(Plus, { className: "h-3.5 w-3.5 shrink-0" })), _jsx("span", { className: "font-medium", children: loading ? 'Opening...' : 'Add Repository' })] }));
24
+ return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Add Repository", "data-testid": "add-repository-button", onClick: handleClick, disabled: loading, children: loading ? (_jsx(Loader2, { className: "h-5 w-5 animate-spin" })) : (_jsx(FolderPlus, { className: "h-5 w-5" })) }) }), _jsx(TooltipContent, { children: "Add Repository" })] }) }));
23
25
  }
@@ -5,4 +5,6 @@ export default meta;
5
5
  type Story = StoryObj<AddRepositoryNodeData>;
6
6
  export declare const Default: Story;
7
7
  export declare const WithCallback: Story;
8
+ /** Add Repository button as it appears in the top bar alongside other toggles */
9
+ export declare const TopBarButton: StoryObj;
8
10
  //# sourceMappingURL=add-repository-node.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"add-repository-node.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/add-repository-node/add-repository-node.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,KAAK,EAAE,qBAAqB,EAAyB,MAAM,8BAA8B,CAAC;AAgCjG,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,qBAAqB,CAOrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,qBAAqB,CAAC,CAAC;AAE7C,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAK1B,CAAC"}
1
+ {"version":3,"file":"add-repository-node.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/add-repository-node/add-repository-node.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,KAAK,EAAE,qBAAqB,EAAyB,MAAM,8BAA8B,CAAC;AAgCjG,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,qBAAqB,CAOrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,qBAAqB,CAAC,CAAC;AAE7C,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAK1B,CAAC;AAIF,iFAAiF;AACjF,eAAO,MAAM,YAAY,EAAE,QAM1B,CAAC"}
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from 'react';
3
3
  import { ReactFlowProvider, ReactFlow } from '@xyflow/react';
4
4
  import { AddRepositoryNode } from './add-repository-node.js';
5
+ import { AddRepositoryButton } from './add-repository-button.js';
5
6
  const nodeTypes = { addRepositoryNode: AddRepositoryNode };
6
7
  function AddRepositoryNodeCanvas({ data, style = { width: 500, height: 250 }, }) {
7
8
  const nodes = useMemo(() => [{ id: 'node-1', type: 'addRepositoryNode', position: { x: 0, y: 0 }, data }], [data]);
@@ -25,3 +26,8 @@ export const WithCallback = {
25
26
  },
26
27
  render: (args) => _jsx(AddRepositoryNodeCanvas, { data: args }),
27
28
  };
29
+ // --- Add Repository Button (top bar icon) stories ---
30
+ /** Add Repository button as it appears in the top bar alongside other toggles */
31
+ export const TopBarButton = {
32
+ render: () => (_jsx("div", { className: "flex gap-1", children: _jsx(AddRepositoryButton, { onSelect: () => undefined }) })),
33
+ };
@@ -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. */
@@ -1 +1 @@
1
- {"version":3,"file":"control-center-inner.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/control-center/control-center-inner.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAgB1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAY5E,UAAU,uBAAuB;IAC/B,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,YAAY,EAAE,IAAI,EAAE,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,uBAAuB,2CAgazF"}
1
+ {"version":3,"file":"control-center-inner.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/control-center/control-center-inner.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAgB1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAY5E,UAAU,uBAAuB;IAC/B,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,YAAY,EAAE,IAAI,EAAE,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,uBAAuB,2CA0azF"}
@@ -236,6 +236,15 @@ export function ControlCenterInner({ initialNodes, initialEdges }) {
236
236
  window.addEventListener('shep:open-create-drawer', handler);
237
237
  return () => window.removeEventListener('shep:open-create-drawer', handler);
238
238
  }, [handleAddFeature]);
239
+ // Listen for global "add repository" events from the top bar button
240
+ useEffect(() => {
241
+ const handler = (e) => {
242
+ const path = e.detail.path;
243
+ handleAddRepository(path);
244
+ };
245
+ window.addEventListener('shep:add-repository', handler);
246
+ return () => window.removeEventListener('shep:add-repository', handler);
247
+ }, [handleAddRepository]);
239
248
  // Listen for notification "Review" clicks to open the relevant drawer
240
249
  useEffect(() => {
241
250
  const handler = (e) => {
@@ -1 +1 @@
1
- {"version":3,"file":"features-canvas.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/features-canvas/features-canvas.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKlE,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,kCAAkC,CAAC;AAEzF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAE9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAGrF,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,kBAAkB,GAAG,qBAAqB,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,cAAc,CAAC,EAAE,KAAK,IAAI,CAAC;IAChE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACtE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAChD,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,kBAAkB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7C,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,KAAK,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,OAAO,EACP,UAAU,GACX,EAAE,mBAAmB,2CAwHrB"}
1
+ {"version":3,"file":"features-canvas.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/features-canvas/features-canvas.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKlE,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,kCAAkC,CAAC;AAEzF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAE9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAGrF,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,kBAAkB,GAAG,qBAAqB,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,cAAc,CAAC,EAAE,KAAK,IAAI,CAAC;IAChE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACtE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAChD,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,kBAAkB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7C,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,KAAK,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,OAAO,EACP,UAAU,GACX,EAAE,mBAAmB,2CAmHrB"}
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useMemo } from 'react';
4
- import { ReactFlow, Background, Controls, Panel, ReactFlowProvider } from '@xyflow/react';
4
+ import { ReactFlow, Background, Controls, ReactFlowProvider } from '@xyflow/react';
5
5
  import { Plus } from 'lucide-react';
6
6
  import { Button } from '../../ui/button.js';
7
7
  import { EmptyState } from '../../common/empty-state/index.js';
8
8
  import { FeatureNode } from '../../common/feature-node/index.js';
9
9
  import { RepositoryNode } from '../../common/repository-node/index.js';
10
- import { AddRepositoryButton, AddRepositoryNode } from '../../common/add-repository-node/index.js';
10
+ import { AddRepositoryNode } from '../../common/add-repository-node/index.js';
11
11
  import { DependencyEdge } from './dependency-edge.js';
12
12
  export function FeaturesCanvas({ nodes, edges, onNodesChange, onAddFeature, onNodeAction, onNodeSettings, onConnect, onNodeClick, onPaneClick, onRepositoryAdd, onRepositoryDelete, onRepositorySelect, toolbar, emptyState, }) {
13
13
  const nodeTypes = useMemo(() => ({
@@ -63,5 +63,5 @@ export function FeaturesCanvas({ nodes, edges, onNodesChange, onAddFeature, onNo
63
63
  }
64
64
  return (_jsx("div", { "data-testid": "features-canvas-empty", children: _jsx(EmptyState, { title: "No features yet", description: "Get started by creating your first feature.", action: _jsxs(Button, { onClick: onAddFeature, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Feature"] }) }) }));
65
65
  }
66
- return (_jsx("div", { "data-testid": "features-canvas", className: "h-full w-full", children: _jsx(ReactFlowProvider, { children: _jsxs(ReactFlow, { nodes: enrichedNodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, isValidConnection: isValidConnection, onConnect: onConnect, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, defaultViewport: { x: 30, y: 30, zoom: 0.85 }, nodesDraggable: false, nodesConnectable: false, elementsSelectable: false, children: [_jsx(Background, {}), _jsx(Controls, {}), onRepositorySelect ? (_jsx(Panel, { position: "top-right", className: "mr-16", children: _jsx(AddRepositoryButton, { onSelect: onRepositorySelect }) })) : null, toolbar] }) }) }));
66
+ return (_jsx("div", { "data-testid": "features-canvas", className: "h-full w-full", children: _jsx(ReactFlowProvider, { children: _jsxs(ReactFlow, { nodes: enrichedNodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, isValidConnection: isValidConnection, onConnect: onConnect, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, defaultViewport: { x: 30, y: 30, zoom: 0.85 }, nodesDraggable: false, nodesConnectable: false, elementsSelectable: false, children: [_jsx(Background, {}), _jsx(Controls, {}), toolbar] }) }) }));
67
67
  }
@@ -14,5 +14,4 @@ export declare const SingleRepositoryMultipleFeatures: Story;
14
14
  export declare const MultipleRepositories: Story;
15
15
  export declare const MultipleRepositoriesMixedConnections: Story;
16
16
  export declare const InteractiveWithRepository: Story;
17
- export declare const AddRepositoryButtonClearanceCheck: Story;
18
17
  //# sourceMappingURL=features-canvas.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"features-canvas.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAOnD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAcrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAwKnC,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAKxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAK3B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAK5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAWzB,CAAC;AA4DF,eAAO,MAAM,mCAAmC,EAAE,KAOjD,CAAC;AAkCF,eAAO,MAAM,gCAAgC,EAAE,KAS9C,CAAC;AAgEF,eAAO,MAAM,gCAAgC,EAAE,KAW9C,CAAC;AAuDF,eAAO,MAAM,oBAAoB,EAAE,KAUlC,CAAC;AA2GF,eAAO,MAAM,oCAAoC,EAAE,KAOlD,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,KASvC,CAAC;AAKF,eAAO,MAAM,iCAAiC,EAAE,KAoC/C,CAAC"}
1
+ {"version":3,"file":"features-canvas.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAOnD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAcrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAwKnC,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAKxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAK3B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAK5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAWzB,CAAC;AA4DF,eAAO,MAAM,mCAAmC,EAAE,KAOjD,CAAC;AAkCF,eAAO,MAAM,gCAAgC,EAAE,KAS9C,CAAC;AAgEF,eAAO,MAAM,gCAAgC,EAAE,KAW9C,CAAC;AAuDF,eAAO,MAAM,oBAAoB,EAAE,KAUlC,CAAC;AA2GF,eAAO,MAAM,oCAAoC,EAAE,KAOlD,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,KASvC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { FeaturesCanvas } from './features-canvas.js';
3
3
  import { layoutWithDagre } from '../../../lib/layout-with-dagre.js';
4
4
  const meta = {
@@ -562,33 +562,3 @@ export const InteractiveWithRepository = {
562
562
  onRepositoryAdd: () => undefined,
563
563
  },
564
564
  };
565
- // Story to visually verify the Add Repository button does not overlap the ThemeToggle.
566
- // The simulated ThemeToggle (top-3 right-3, 40×40 px) is rendered as an overlay so
567
- // reviewers can confirm the mr-16 (64 px) gap keeps the Panel clear of the toggle zone.
568
- export const AddRepositoryButtonClearanceCheck = {
569
- decorators: [
570
- (Story) => (_jsxs("div", { style: { height: '100vh', position: 'relative' }, children: [_jsx(Story, {}), _jsx("div", { style: {
571
- position: 'absolute',
572
- top: 12,
573
- right: 12,
574
- width: 40,
575
- height: 40,
576
- background: 'rgba(255, 0, 0, 0.35)',
577
- border: '2px dashed red',
578
- borderRadius: 6,
579
- display: 'flex',
580
- alignItems: 'center',
581
- justifyContent: 'center',
582
- fontSize: 10,
583
- pointerEvents: 'none',
584
- zIndex: 50,
585
- }, children: "Theme" })] })),
586
- ],
587
- args: {
588
- nodes: repoFeatureNodes,
589
- edges: repoFeatureEdges,
590
- onNodeAction: () => undefined,
591
- onNodeSettings: () => undefined,
592
- onRepositorySelect: () => undefined,
593
- },
594
- };
@@ -1 +1 @@
1
- {"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/layouts/app-shell/app-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAQpD,UAAU,aAAa;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AA2BD,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,2CAMnD"}
1
+ {"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/layouts/app-shell/app-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AASpD,UAAU,aAAa;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAgCD,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,2CAMnD"}
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback } from 'react';
4
4
  import { SidebarProvider, SidebarInset } from '../../ui/sidebar.js';
5
5
  import { AppSidebar } from '../../layouts/app-sidebar/index.js';
6
+ import { AddRepositoryButton } from '../../common/add-repository-node/index.js';
6
7
  import { ThemeToggle } from '../../common/theme-toggle/index.js';
7
8
  import { SoundToggle } from '../../common/sound-toggle/index.js';
8
9
  import { AgentEventsProvider } from '../../../hooks/agent-events-provider.js';
@@ -14,7 +15,10 @@ function AppShellInner({ children }) {
14
15
  const handleNewFeature = useCallback(() => {
15
16
  window.dispatchEvent(new CustomEvent('shep:open-create-drawer'));
16
17
  }, []);
17
- return (_jsxs(SidebarProvider, { children: [_jsx(AppSidebar, { features: [], onNewFeature: handleNewFeature }), _jsx(SidebarInset, { children: _jsxs("div", { className: "relative h-full", children: [_jsxs("div", { className: "absolute top-3 right-3 z-50 flex gap-1", children: [_jsx(SoundToggle, {}), _jsx(ThemeToggle, {})] }), _jsx("main", { className: "h-full", children: children })] }) })] }));
18
+ const handleRepositorySelect = useCallback((path) => {
19
+ window.dispatchEvent(new CustomEvent('shep:add-repository', { detail: { path } }));
20
+ }, []);
21
+ return (_jsxs(SidebarProvider, { children: [_jsx(AppSidebar, { features: [], onNewFeature: handleNewFeature }), _jsx(SidebarInset, { children: _jsxs("div", { className: "relative h-full", children: [_jsxs("div", { className: "absolute top-3 right-3 z-50 flex gap-1", children: [_jsx(AddRepositoryButton, { onSelect: handleRepositorySelect }), _jsx(SoundToggle, {}), _jsx(ThemeToggle, {})] }), _jsx("main", { className: "h-full", children: children })] }) })] }));
18
22
  }
19
23
  export function AppShell({ children }) {
20
24
  return (_jsx(AgentEventsProvider, { children: _jsx(AppShellInner, { children: children }) }));
@@ -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())),