 
 	/// Sends each event that occurs to a project underneath the receiver (or
 	/// the receiver itself).
-	public let projectEvents: HotSignal<ProjectEvent>
-	private let _projectEventsSink: SinkOf<ProjectEvent>
+	public let projectEvents: Signal<ProjectEvent, NoError>
+	private let _projectEventsObserver: Signal<ProjectEvent, NoError>.Observer
 
 	public init(directoryURL: NSURL) {
 		precondition(directoryURL.fileURL)
 
-		let (signal, sink) = HotSignal<ProjectEvent>.pipe()
+		let (signal, observer) = Signal<ProjectEvent, NoError>.pipe()
 		projectEvents = signal
-		_projectEventsSink = sink
+		_projectEventsObserver = observer
 
 		self.directoryURL = directoryURL
+	}
+
+	deinit {
+		sendCompleted(_projectEventsObserver)
 	}
 
 	/// Caches versions to avoid expensive lookups, and unnecessary


	/// Attempts to load Cartfile or Cartfile.private from the given directory,
	/// merging their dependencies.
	public func loadCombinedCartfile() -> ColdSignal<Cartfile> {
		let cartfileURL = directoryURL.URLByAppendingPathComponent(CarthageProjectCartfilePath, isDirectory: false)
		let privateCartfileURL = directoryURL.URLByAppendingPathComponent(CarthageProjectPrivateCartfilePath, isDirectory: false)

		let isNoSuchFileError = { (error: NSError) -> Bool in
			return error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoSuchFileError
		}

		let cartfile = ColdSignal.lazy {
				.fromResult(Cartfile.fromFile(cartfileURL))
			}
			.catch { error -> ColdSignal<Cartfile> in
				if isNoSuchFileError(error) && NSFileManager.defaultManager().fileExistsAtPath(privateCartfileURL.path!) {
					return .single(Cartfile())
				}

				return .error(error)
			}

		let privateCartfile = ColdSignal.lazy {
				.fromResult(Cartfile.fromFile(privateCartfileURL))
			}
			.catch { error -> ColdSignal<Cartfile> in
				if isNoSuchFileError(error) {
					return .single(Cartfile())
				}

				return .error(error)
		}

		return cartfile.zipWith(privateCartfile)
			.tryMap { (var cartfile, privateCartfile) -> Result<Cartfile> in
				let duplicateDeps = cartfile.duplicateProjects().map { DuplicateDependency(project: $0, locations: ["\(CarthageProjectCartfilePath)"]) }
					+ privateCartfile.duplicateProjects().map { DuplicateDependency(project: $0, locations: ["\(CarthageProjectPrivateCartfilePath)"]) }
					+ duplicateProjectsInCartfiles(cartfile, privateCartfile).map { DuplicateDependency(project: $0, locations: ["\(CarthageProjectCartfilePath)", "\(CarthageProjectPrivateCartfilePath)"]) }

	///
	/// This will fetch dependency repositories as necessary, but will not check
	/// them out into the project's working directory.
	public func updatedResolvedCartfile() -> SignalProducer<ResolvedCartfile, CarthageError> {
		let resolver = Resolver(versionsForDependency: versionsForProject, cartfileForDependency: cartfileForDependency, resolvedGitReference: resolvedGitReference)

		return loadCombinedCartfile()
			|> joinMap(.Merge) { cartfile in resolver.resolveDependenciesInCartfile(cartfile) }
			|> reduce([]) { $0 + [ $1 ] }
			|> map { ResolvedCartfile(dependencies: $0) }
	}

	/// Updates the dependencies of the project to the latest version. The
	/// changes will be reflected in the working directory checkouts and
	/// Cartfile.resolved.
	public func updateDependencies() -> SignalProducer<(), CarthageError> {
		return updatedResolvedCartfile()
			|> tryMap { resolvedCartfile -> Result<(), CarthageError> in
				return self.writeResolvedCartfile(resolvedCartfile)
			}
			|> then(checkoutResolvedDependencies())
	}

	/// Installs binaries for the given project, if available.
	///
	/// Sends a boolean indicating whether binaries were installed.
	private func installBinariesForProject(project: ProjectIdentifier, atRevision revision: String) -> SignalProducer<Bool, CarthageError> {
		return SignalProducer.try {
				return success(self.useBinaries)
			}
			|> joinMap(.Merge) { useBinaries -> SignalProducer<Bool, CarthageError> in
				if useBinaries {
					return SignalProducer(value: false)
				}

				let checkoutDirectoryURL = self.directoryURL.URLByAppendingPathComponent(project.relativePath, isDirectory: true)

				switch project {
				case let .GitHub(repository):
					return self.downloadMatchingBinariesForProject(project, atRevision: revision, fromRepository: repository, withCredentials: nil)
						|> catch { error in
							switch error {
							case let .NetworkError(underlyingError):
								// If we were unable to fetch releases, try loading credentials from Git.
								if underlyingError.domain == NSURLErrorDomain {
									return GitHubCredentials.loadFromGit()
										|> joinMap(.Merge) { credentials in
											if let credentials = credentials {
												return self.downloadMatchingBinariesForProject(project, atRevision: revision, fromRepository: repository, withCredentials: credentials)
											} else {
												return SignalProducer(error: error)
											}
										}
								}

								fallthrough

							default:
								return SignalProducer(error: error)
							}
						}
						|> joinMap(.Concat, unzipArchiveToTemporaryDirectory)
						|> joinMap(.Concat) { directoryURL in
							return frameworksInDirectory(directoryURL)
								|> joinMap(.Merge, self.copyFrameworkToBuildFolder)
								|> on(completed: {
									_ = NSFileManager.defaultManager().trashItemAtURL(checkoutDirectoryURL, resultingItemURL: nil, error: nil)
								})
								|> then(SignalProducer(value: directoryURL))
						}
						|> tryMap { (temporaryDirectoryURL: NSURL) -> Result<Bool, CarthageError> in
							var error: NSError?
							if NSFileManager.defaultManager().removeItemAtURL(temporaryDirectoryURL, error: &error) {
								return success(true)
							} else {
								return failure(.WriteFailed(temporaryDirectoryURL, error))
							}
						}
						|> concat(SignalProducer(value: false))
						|> take(1)

				case .Git:
					return SignalProducer(value: false)
				}
			}
	}

	/// Downloads any binaries that may be able to be used instead of a
