diff --git a/.cursor/rules/verify-before-claiming-fixed.mdc b/.cursor/rules/verify-before-claiming-fixed.mdc new file mode 100644 index 00000000..602bf59e --- /dev/null +++ b/.cursor/rules/verify-before-claiming-fixed.mdc @@ -0,0 +1,13 @@ +--- +description: Prove a fix with the failing path before saying it works +alwaysApply: true +--- + +# Verify before claiming a fix + +Do not treat a code change as fixed because it compiles or because the new logic looks right. + +- Read the live logs or the failing output first. +- Add or run a test that fails on the broken behavior and passes after the change. +- Run that test. If the bug is in a running app, build the checkout that actually launches and exercise that path. +- Say what was verified and what was not. diff --git a/packages/HostUI/Sources/HostUI/HostUIMarkdown.swift b/packages/HostUI/Sources/HostUI/HostUIMarkdown.swift index 9a77b56b..9a971900 100644 --- a/packages/HostUI/Sources/HostUI/HostUIMarkdown.swift +++ b/packages/HostUI/Sources/HostUI/HostUIMarkdown.swift @@ -4,8 +4,8 @@ import SwiftUI /// Shared markdown + `$profile` token coloring for HostUI message bodies. public enum HostUIMarkdown { - /// Dark green for `$handle` and `[Derrick:handle]`. Darker than system green on white bubbles. - public static let profileTokenColor = Color(red: 0.0, green: 0.42, blue: 0.18) + /// Medium green for `$handle` and `[Derrick:$handle]` on white bubbles. + public static let profileTokenColor = Color(red: 0.18, green: 0.62, blue: 0.32) public static func attributed(_ text: String) -> AttributedString { var options = AttributedString.MarkdownParsingOptions() @@ -16,7 +16,7 @@ public enum HostUIMarkdown { return attributed } - /// Paints `$handle` and `[Derrick:handle]` on already-rendered text. Later copies of the same token are included. + /// Paints `$handle` and `[Derrick:$handle]` on already-rendered text. Later copies of the same token are included. public static func colorProfileTokens(_ attributed: inout AttributedString) { let source = String(attributed.characters) var search = attributed.startIndex @@ -29,7 +29,7 @@ public enum HostUIMarkdown { } } -/// Plain text with `$handle` and `[Derrick:handle]` in `HostUIMarkdown.profileTokenColor`. +/// Plain text with `$handle` and `[Derrick:$handle]` in `HostUIMarkdown.profileTokenColor`. public struct HostUIProfileTokenText: View { private let text: String private let base: Color diff --git a/packages/HostUI/Sources/HostUI/HostUIMessagingControls.swift b/packages/HostUI/Sources/HostUI/HostUIMessagingControls.swift index 3e632f1f..830a8aa6 100644 --- a/packages/HostUI/Sources/HostUI/HostUIMessagingControls.swift +++ b/packages/HostUI/Sources/HostUI/HostUIMessagingControls.swift @@ -427,7 +427,7 @@ public struct HostUIComposer: View { id: "wx", sender: "derrick", body: """ - [Derrick:orchestrator] Searching the web… + [Derrick:$orchestrator] Searching the web… In **Northvale, NJ 07647**, Weather Underground reported **62°F** at **7:27 AM EDT on September 19, 2026**. Its listed daily temp **71°F / 63°F**. diff --git a/packages/HostUI/Sources/HostUI/HostUINodeView.swift b/packages/HostUI/Sources/HostUI/HostUINodeView.swift index bdb4a8ba..87a15fec 100644 --- a/packages/HostUI/Sources/HostUI/HostUINodeView.swift +++ b/packages/HostUI/Sources/HostUI/HostUINodeView.swift @@ -387,7 +387,7 @@ private struct HostUIInboxPreviewHost: View { id: "r", sender: "derrick", body: """ - [Derrick:orchestrator] Searching the web… + [Derrick:$orchestrator] Searching the web… In **Northvale, NJ 07647**, Weather Underground reported **62°F** at **7:27 AM EDT on September 19, 2026**. Its listed daily temp **71°F / 63°F**. diff --git a/packages/HostUI/Tests/HostUITests/HostUIProfileTokenColorTests.swift b/packages/HostUI/Tests/HostUITests/HostUIProfileTokenColorTests.swift index e4360b89..509e480d 100644 --- a/packages/HostUI/Tests/HostUITests/HostUIProfileTokenColorTests.swift +++ b/packages/HostUI/Tests/HostUITests/HostUIProfileTokenColorTests.swift @@ -3,8 +3,8 @@ import Testing @MainActor @Suite struct HostUIProfileTokenColorTests { - @Test func dollarHandleAndSenderPrefixShareDarkGreen() { - let text = "$orchestrator then [Derrick:developer] done" + @Test func dollarHandleAndSenderPrefixShareMediumGreen() { + let text = "$orchestrator then [Derrick:$developer] done" let attributed = HostUIMarkdown.attributed(text) let snippets = attributed.runs.compactMap { run -> String? in guard attributed[run.range].foregroundColor == HostUIMarkdown.profileTokenColor else { @@ -12,12 +12,12 @@ import Testing } return String(attributed[run.range].characters) } - #expect(snippets == ["$orchestrator", "[Derrick:developer]"]) + #expect(snippets == ["$orchestrator", "[Derrick:$developer]"]) } @Test func plainPreviewKeepsTheSameGreenOnTheSenderPrefix() { let attributed = HostUIProfileTokenText.attributed( - "[Derrick:orchestrator] Searching the web…", + "[Derrick:$orchestrator] Searching the web…", base: .secondary ) let green = attributed.runs.compactMap { run -> String? in @@ -26,6 +26,6 @@ import Testing } return String(attributed[run.range].characters) } - #expect(green == ["[Derrick:orchestrator]"]) + #expect(green == ["[Derrick:$orchestrator]"]) } } diff --git a/packages/Structure/Sources/AgentRuntime/AgentProfile.swift b/packages/Structure/Sources/AgentRuntime/AgentProfile.swift index cb90be7d..b467cace 100644 --- a/packages/Structure/Sources/AgentRuntime/AgentProfile.swift +++ b/packages/Structure/Sources/AgentRuntime/AgentProfile.swift @@ -143,16 +143,19 @@ public enum AgentProfileTokenParser { } } -/// Ranges of `$shortName` tokens and the full `[Derrick:handle]` sender prefix for UI highlighting. +/// Ranges of `$shortName` tokens and the full `[Derrick:$handle]` sender prefix for UI highlighting. public enum AgentProfileTokenHighlight { public static func ranges( in text: String, productName: String = DerrickAppSupport.hostAppProductName ) -> [Range] { - var found: [Range] = [] - found.append(contentsOf: dollarHandleRanges(in: text)) - found.append(contentsOf: productPrefixedHandleRanges(in: text, productName: productName)) - return found.sorted { $0.lowerBound < $1.lowerBound } + let prefixed = productPrefixedHandleRanges(in: text, productName: productName) + let dollars = dollarHandleRanges(in: text).filter { dollar in + !prefixed.contains { prefix in + prefix.lowerBound <= dollar.lowerBound && dollar.upperBound <= prefix.upperBound + } + } + return (dollars + prefixed).sorted { $0.lowerBound < $1.lowerBound } } public static func nsRanges( @@ -195,7 +198,10 @@ public enum AgentProfileTokenHighlight { let prefix = text.range(of: needle, range: searchFrom.. [DaemonRegistrationStep] { + let sessionNeedsRepair = !sessionJobLoaded || sessionProgramStale + guard sessionNeedsRepair else { + return [.kickstartSessionAgent] + } + var steps: [DaemonRegistrationStep] = [] + if serviceManagementOwnsMachEndpoint { + steps.append(.unregisterServiceManagement) + } + steps.append(.bootoutLaunchdJobs) + steps.append(.installSessionAgent) + return steps + } + /// Extra processes of the expected binary steal the Mach service and lock SQLite. /// Keep the newest; evict the rest. public static func duplicateExpectedDaemonPIDsToEvict( diff --git a/packages/Structure/Sources/Plugin/Messaging/ConnectorMentionRouting.swift b/packages/Structure/Sources/Plugin/Messaging/ConnectorMentionRouting.swift index f8e810ae..f5b52364 100644 --- a/packages/Structure/Sources/Plugin/Messaging/ConnectorMentionRouting.swift +++ b/packages/Structure/Sources/Plugin/Messaging/ConnectorMentionRouting.swift @@ -95,7 +95,7 @@ public enum ConnectorMentionParser: Sendable { if handle.isEmpty { return botReplyPrefix } - return "[\(DerrickAppSupport.hostAppProductName):\(handle)]" + return "[\(DerrickAppSupport.hostAppProductName):$\(handle)]" } /// Reply under the inbound message, or stay in an existing thread. @@ -160,7 +160,11 @@ public enum ConnectorMentionParser: Sendable { else { return nil } - return AgentProfileHandle.normalize(String(trimmed[prefix.endIndex.. Bool { - loadedLaunchdDomain() != nil + isSessionLaunchdJobLoaded() } private static func loadedLaunchdLabel() -> String? { @@ -298,51 +271,15 @@ public enum JobServiceLoginAgent { } private static func loadedLaunchdDomain() -> String? { - let uid = getuid() - let names = [ - DerrickServiceID.daemonSessionLaunchdLabel, - label, - DerrickServiceID.jobKeepAlive.rawValue, - ] - for name in names { - let domain = "gui/\(uid)/\(name)" - if isLaunchdDomainLoaded(domain) { - return domain - } + let domain = "gui/\(getuid())/\(DerrickServiceID.daemonSessionLaunchdLabel)" + if isLaunchdDomainLoaded(domain) { + return domain } return nil } - private static func isDaemonProcessRunning() -> Bool { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep") - process.arguments = ["-f", "LoginItems/JobKeepAlive.app/Contents/MacOS/JobKeepAlive"] - process.standardOutput = Pipe() - process.standardError = Pipe() - do { - try process.run() - process.waitUntilExit() - return process.terminationStatus == 0 - } catch { - return false - } - } - - /// Start the SM helper through launchd so it can check in the Mach service. - /// Do not `open` the helper app: that process holds the singleton lock but never - /// receives `VUSK4B2YKQ.derrick.shared.daemon`, and the UI hangs on connect. - /// Do not `launchctl print gui/`: the dump hangs the sandboxed UI. - private static func demandStartSMDaemon() { - let uid = getuid() - for name in DerrickServiceID.demandStartLaunchdLabels { - let domain = "gui/\(uid)/\(name)" - let status = runLaunchctlAllowFail(["kickstart", domain]) - if status == 0 { - fputs("[derrickd] demand-start ok \(domain)\n", stderr) - return - } - } - fputs("[derrickd] demand-start missed — Mach XPC launch-on-demand should start derrickd\n", stderr) + private static func isSessionLaunchdJobLoaded() -> Bool { + loadedLaunchdDomain() != nil } private static func isLaunchdDomainLoaded(_ domain: String) -> Bool { @@ -425,57 +362,6 @@ public enum JobServiceLoginAgent { ) } - // MARK: - SMAppService - - private static func registerViaSMAppService() throws -> Result? { - let status = smAgent.status - switch status { - case .enabled: - // Re-register so BTM tracks this DerivedData bundle without prompting again. - try? smAgent.register() - return Result( - method: .smAppService, - statusDescription: describe(status), - isRunningOrEnabled: true, - detail: "SMAppService enabled" - ) - case .requiresApproval: - // Do not open Settings here. BTM can report approval-needed while the - // nested helper still starts when the host app opens it. - return Result( - method: .smAppService, - statusDescription: describe(status), - isRunningOrEnabled: false, - detail: "Approve in System Settings → Login Items" - ) - case .notRegistered, .notFound: - try? smAgent.unregister() - do { - try smAgent.register() - } catch { - fputs( - "[derrickd] SM register error: \((error as NSError).domain) \((error as NSError).code) \(error.localizedDescription)\n", - stderr - ) - return nil - } - let after = smAgent.status - if after == .enabled || after == .requiresApproval { - return Result( - method: .smAppService, - statusDescription: describe(after), - isRunningOrEnabled: after == .enabled, - detail: after == .enabled - ? "SMAppService registered and enabled" - : "SMAppService registered; needs Login Items approval" - ) - } - return nil - @unknown default: - return nil - } - } - // MARK: - Helper install /// Installer writes the session plist and exits. Do not use `open -W`: diff --git a/ui/ui/AgentProfiles/AgentProfileHighlightedText.swift b/ui/ui/AgentProfiles/AgentProfileHighlightedText.swift index 896a5494..ff8a1fbc 100644 --- a/ui/ui/AgentProfiles/AgentProfileHighlightedText.swift +++ b/ui/ui/AgentProfiles/AgentProfileHighlightedText.swift @@ -3,6 +3,6 @@ import Structure import SwiftUI enum AgentProfileTokenColor { - /// Darker than system green so `$profile` tokens stay readable on light bubbles. - static let darkGreen = Color(red: 0.0, green: 0.42, blue: 0.18) + /// Medium green shared with `$handle` and `[Derrick:$handle]` labels. + static let darkGreen = Color(red: 0.18, green: 0.62, blue: 0.32) } diff --git a/ui/ui/Views/ContentView.swift b/ui/ui/Views/ContentView.swift index 373ec09d..ac7628c6 100644 --- a/ui/ui/Views/ContentView.swift +++ b/ui/ui/Views/ContentView.swift @@ -920,7 +920,7 @@ struct ContentView: View { @MainActor private func connectLaunchDaemon() async throws -> ServiceHealthReport { bootstrapStatus.update(phase: .connectingHelper, message: "Connecting to Derrick daemon…") - try? await Task.detached(priority: .userInitiated) { + try await Task.detached(priority: .userInitiated) { try await JobServiceLoginAgent.ensureRegistered() JobServiceLoginAgent.ensureHelperProcessRunning() }.value diff --git a/ui/uiTests/MessagingNavigationTests.swift b/ui/uiTests/MessagingNavigationTests.swift index 0792e936..e8dfed48 100644 --- a/ui/uiTests/MessagingNavigationTests.swift +++ b/ui/uiTests/MessagingNavigationTests.swift @@ -388,8 +388,8 @@ import Testing } @Test func profileTokenHighlightMarksDollarHandlesGreenCandidates() { - let text = "$orchestrator tell me about yourself then [Derrick:developer] replied" + let text = "$orchestrator tell me about yourself then [Derrick:$developer] replied" let tokens = AgentProfileTokenHighlight.ranges(in: text).map { String(text[$0]) } - #expect(tokens == ["$orchestrator", "[Derrick:developer]"]) + #expect(tokens == ["$orchestrator", "[Derrick:$developer]"]) } }