Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ by `./sync-agents`.
## Targets

- **Package products** ([`Package.swift`](Package.swift)) — **StuffCore** ([`Shared/StuffCore/Sources/`](Shared/StuffCore/Sources/)), **LifecycleKit** ([`Shared/LifecycleKit/Sources/`](Shared/LifecycleKit/Sources/)), **LogKit** ([`Shared/LogKit/Sources/`](Shared/LogKit/Sources/), the logging facade), **LogViewerUI** ([`Shared/LogViewerUI/Sources/`](Shared/LogViewerUI/Sources/), the generic SwiftUI log viewer), and **SwiftDataInspector** ([`Shared/SwiftDataInspector/Sources/`](Shared/SwiftDataInspector/Sources/), the generic SwiftData browser) under [`Shared/`](Shared/); **WhereCore** / **WhereUI** / **WhereTesting** under [`Where/`](Where/); **ForemanCore** ([`Foreman/ForemanCore/Sources/`](Foreman/ForemanCore/Sources/), the only **macOS-only** package library, which processes a generated-symbol `Resources/Localizable.xcstrings`) under [`Foreman/`](Foreman/).
- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)), **RegionViewer** ([`Where/RegionViewer/`](Where/RegionViewer/), a thin standalone **Mac Catalyst** host for the WhereUI region-map developer tool — the only target with a `.macCatalyst` destination), **Foreman** ([`Foreman/Foreman/`](Foreman/Foreman/), the **native-macOS** menu bar app that runs Cursor local agent workers per repo; its user-facing copy is a generated-symbol `Resources/Localizable.xcstrings` via `STRING_CATALOG_GENERATE_SYMBOLS`), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), hosted **\*Tests** bundles (**StuffCoreTests**, **LifecycleKitTests**, **LogKitTests**, **LogViewerUITests**, **SwiftDataInspectorTests**, **WhereCoreTests**, **WhereUITests**) that depend on **StuffTestHost** + **WhereTesting** + the relevant package product, and the hostless macOS bundle **ForemanCoreTests**.
- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)), **RegionViewer** ([`Where/RegionViewer/`](Where/RegionViewer/), a thin standalone **Mac Catalyst** host for the WhereUI region-map developer tool — the only target with a `.macCatalyst` destination), **Foreman** ([`Foreman/Foreman/`](Foreman/Foreman/), the **native-macOS** menu bar app that runs Cursor local agent workers per repo; its user-facing copy is a generated-symbol `Resources/Localizable.xcstrings` via `STRING_CATALOG_GENERATE_SYMBOLS`; it also serves a local unix-domain control socket for the `foreman-mcp` companion — see below), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), hosted **\*Tests** bundles (**StuffCoreTests**, **LifecycleKitTests**, **LogKitTests**, **LogViewerUITests**, **SwiftDataInspectorTests**, **WhereCoreTests**, **WhereUITests**) that depend on **StuffTestHost** + **WhereTesting** + the relevant package product, and the hostless macOS bundle **ForemanCoreTests**.
- Add SPM library targets in `Package.swift` and wire apps/tests in `Project.swift` (see existing `unitTests` helper; macOS test bundles are declared directly, like `ForemanCoreTests`). A new module also ships a root `README.md` and `AGENTS.md` — see [Per-module docs](#per-module-docs).
- **Non-Swift module**: **foreman-mcp** ([`Foreman/foreman-mcp/`](Foreman/foreman-mcp/)) is a Node/TypeScript **stdio MCP server** (the repo's only non-Swift module, outside Tuist/SwiftPM). It lets a Cursor agent spin up `git worktree`/`clone` copies of its repo and have Foreman start a worker on them, talking to Foreman over the control socket above (wire protocol mirrored from `ForemanCore/Sources/ControlProtocol.swift`). Build/test with `npm` in that directory; registered in `~/.cursor/mcp.json`. See its [`README.md`](Foreman/foreman-mcp/README.md).
- **CI schemes**: the workspace mixes iOS and macOS targets, which no single xcodebuild destination can build, so CI runs two explicit shared schemes — **Stuff-iOS-Tests** (all iOS bundles) and **Foreman-macOS-Tests** (Foreman app + ForemanCoreTests). New iOS test bundles must be added to the `Stuff-iOS-Tests` scheme in `Project.swift` or CI won't run them.

## Deployment
Expand Down
13 changes: 13 additions & 0 deletions Foreman/Foreman/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ formatting, global conventions) and ForemanCore's
- **Stop-on-quit lifecycle.** The app delegate calls `stopAllWorkers()` in
`applicationWillTerminate` — Foreman owns its worker processes and must
never leave orphans.
- **The control socket lives here, the protocol doesn't.** `ControlServer` is
the only socket-owning code (raw POSIX unix-domain socket): it accepts on a
serial queue, serves each connection on a concurrent queue (with `SO_RCVTIMEO`
so a stalled peer can't wedge it), and delegates all framing/decoding/encoding
to ForemanCore's `ControlConnection` (which dispatches through
`ControlRequestHandler` **on the main actor**). Started in
`applicationDidFinishLaunching` and torn down in `applicationWillTerminate`,
both via `ForemanSession`
(`startControlServer()` / `stopControlServer()`). All request→intent mapping
belongs in ForemanCore's `ControlRequestHandler`, not here. The Remove-copy UI
action goes through `ForemanSession.removeCopy(_:)`, which surfaces failures on
`actionError` (an alert) rather than throwing into a view; copy provenance for
the sidebar badge / detail is presented via `CopyProvenanceDisplay`.
- New worker CLI flags start in ForemanCore (`WorkerOptions` field + argv
rendering, with tests), then surface in `WorkerOptionsView`'s draft.
- Feature-level todos live in [`../TODOs.md`](../TODOs.md) (conventional
Expand Down
25 changes: 23 additions & 2 deletions Foreman/Foreman/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ closed. Closing the window just hides it — the app keeps running until Quit.
between groups as you toggle or favorite them. Each row shows a status dot
(gray stopped, yellow stopping/restarting, green running, red failed —
flipping a stopping worker back on queues a restart for when the old process
exits), the worker's on/off switch, and a star on favorited repos.
exits), the worker's on/off switch, and a star on favorited repos. Repos that
Foreman created as copies (via the MCP — see below) carry a small
worktree/clone badge whose tooltip names the parent repo and branch.
Right-click a row to favorite or unfavorite it.
- **Detail pane** (select a repo) — the worker's status, pid and live uptime,
the failure reason when it died, the repo path, the exact `cursor-agent`
Expand All @@ -41,7 +43,11 @@ closed. Closing the window just hides it — the app keeps running until Quit.
status row offers **Retry** when an enabled worker failed and **Restart**
while it's running (a fresh process with the saved options); neither
changes the on/off switch. A toolbar star favorites the repo (mirrors the
sidebar's right-click toggle).
sidebar's right-click toggle). For a copy Foreman created, a **Copy** section
shows how it was made (worktree vs clone, the parent repo, the branch) and a
**Remove Copy…** button that — after a confirmation — stops the worker and
removes the worktree (git) or moves the clone to the Trash; failures surface
in an alert.
- **Worker options** — mirrors the `cursor agent worker` CLI flags: display
name, pool mode + pool name, `key=value` labels, idle release timeout, and
verbose startup logs. Editable while the worker is stopped — options apply
Expand Down Expand Up @@ -69,6 +75,8 @@ small editor sheet that commits only on **Save** (Cancel or Escape discards).
- On launch, Foreman restores the saved configuration and restarts the workers
that were enabled last time. With *Launch Foreman at login* on, this happens
automatically after you log in.
- The MCP control socket is started after launch and closed (its file removed)
on quit.
- Quitting stops every worker (stop-on-quit: the app owns its processes and
never leaves orphans).
- The repo list refreshes every time the window is opened or focused, and on
Expand All @@ -79,6 +87,19 @@ small editor sheet that commits only on **Save** (Cancel or Escape discards).
from the current scan directory are pruned; settings for other scan
directories are kept and re-apply when you switch back.

## MCP control socket

On launch Foreman starts a small local control server on a unix-domain socket
at `~/Library/Application Support/com.stuff.foreman/control.sock` (JSON-lines),
and tears it down on quit — both wired through `ForemanSession`. The
[`foreman-mcp`](../foreman-mcp) server connects to it so a Cursor agent can spin
up a worktree/clone copy of its repo and have Foreman start a worker on it; the
copy then appears in the sidebar like any other repo. The transport-agnostic
protocol and the intents behind it (`describe` / `adopt` / `removeCopy`) live in
[`ForemanCore`](../ForemanCore/README.md#mcp-control); this target only owns the
socket. A stale socket file (e.g. after a hard kill) is unlinked on the next
start, so a leftover file never blocks binding.

## Localization

All of Foreman's on-screen copy lives in
Expand Down
168 changes: 168 additions & 0 deletions Foreman/Foreman/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,174 @@
}
}
},
"action.error.title" : {
"comment" : "Title of the alert shown when a user-triggered action (e.g. Remove Copy) fails.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Couldn't complete the action"
}
}
}
},
"common.ok" : {
"comment" : "Button that dismisses an alert.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "OK"
}
}
}
},
"detail.copy.branchLabel" : {
"comment" : "Label for the branch a repo copy is checked out on.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Branch"
}
}
}
},
"detail.copy.header" : {
"comment" : "Section header for a repository copy's provenance and removal action.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copy"
}
}
}
},
"detail.copy.kindLabel" : {
"comment" : "Label for whether a copy is a worktree or a clone.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Type"
}
}
}
},
"detail.copy.parentLabel" : {
"comment" : "Label for the repository a copy was made from.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copied from"
}
}
}
},
"detail.removeCopy" : {
"comment" : "Button that removes a repository copy Foreman created.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Remove Copy…"
}
}
}
},
"detail.removeCopy.confirmButton" : {
"comment" : "Confirmation button that carries out removing a copy.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Remove Copy"
}
}
}
},
"detail.removeCopy.confirmMessage" : {
"comment" : "Confirmation dialog body for removing a copy. The placeholder is the copy's name.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Foreman stops the worker, then removes “%(name)@”. A worktree is removed with git; a full clone is moved to the Trash."
}
}
}
},
"detail.removeCopy.confirmTitle" : {
"comment" : "Confirmation dialog title for removing a copy.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Remove this copy?"
}
}
}
},
"detail.removeCopy.help" : {
"comment" : "Tooltip for the Remove Copy button.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Stop the worker and remove this copy."
}
}
}
},
"provenance.clone" : {
"comment" : "Copy kind: a full git clone.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Clone"
}
}
}
},
"provenance.worktree" : {
"comment" : "Copy kind: a git worktree.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Worktree"
}
}
}
},
"row.copy.help" : {
"comment" : "Tooltip on the copy badge in a sidebar row. First placeholder is the kind (Worktree/Clone), second the parent repo name, third the branch.",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%(kind)@ of %(parent)@ on branch %(branch)@"
}
}
}
},
"detail.empty.description" : {
"comment" : "Description of the empty detail pane shown when no repo is selected.",
"extractionState" : "manual",
Expand Down
Loading
Loading