Imported from Chalet-Labs/portu (
AGENTS.md). Install upstream withnpx skills add Chalet-Labs/portu. Copyright stays with the author.
Agent Instructions
Project
Portu is a native macOS SwiftUI crypto portfolio dashboard. It is local-first, has no backend, and has no telemetry. It aggregates holdings from Zapper, CoinGecko, and exchange APIs such as Kraken, Binance, and Coinbase.
Build And Run
- This is a native macOS SwiftUI app generated by XcodeGen from
project.yml. - Always regenerate
Portu.xcodeprojafter modifyingproject.yml. - Regenerate
Portu.xcodeprojbefore building when project files may be stale. The repo run script already does this. - Prefer
./script/build_and_run.sh --verifyto build, launch, and confirm thePortuprocess is running. - Use
./script/build_and_run.sh --logsor--telemetrywhen runtime logs are needed. - Use
./script/build_and_run.sh --debugto build, open LLDB, and start the app under the debugger. - Debug signing: read
docs/agents/debug-signing.mdbefore changing signing settings inproject.ymlorConfigs/, or when macOS asks for keychain access after a rebuild.
Common commands:
just generate
just build
just release
just test-packages
just test
just lint
just lint-strict
just lint-fix
just format
just format-check
just clean
CI runs just format-check and just lint-strict. Plain just lint is not strict, so a local pass can still fail that gate.
XcodeBuildMCP
- The repo declares
XcodeBuildMCPin.mcp.jsonand enables themacosworkflow in.xcodebuildmcp/config.yaml. - If native MCP tools are loaded, prefer the macOS workflow for build/test/discovery.
- If native MCP tools are not loaded, use the CLI directly:
xcodebuildmcp macos build-and-run --project-path Portu.xcodeproj --scheme Portu --configuration Debug --derived-data-path .build/DerivedData --extra-args -skipMacroValidation
xcodebuildmcp macos test --project-path Portu.xcodeproj --scheme Portu --configuration Debug --derived-data-path .build/DerivedData --extra-args -skipMacroValidation
Architecture
Three SwiftPM packages have clean boundaries. Do not introduce cross-package imports that violate this layering:
PortuUI (views, theme, components)
-> PortuCore (models, DTOs, keychain, protocols)
PortuNetwork (API clients, providers, price service)
-> PortuCore
The app target in Sources/Portu/ imports all three packages and contains features, app state, and sync orchestration.
Swift And Concurrency
- Swift 6.2 with strict concurrency (
SWIFT_STRICT_CONCURRENCY: complete). - PortuUI uses
defaultIsolation(MainActor.self). - The app target does not use default MainActor isolation because it is incompatible with TCA.
- PortuNetwork providers are
actortypes and run off the main thread. - DTOs and enums must be plain
Sendablefor cross-isolation safety. - SwiftData
@Modelinstances are read only on the actor that owns theirModelContext(the main actor for views, the@ModelActorfor background fetchers).
Testing
- Use Swift Testing (
import Testing), not XCTest. - Use
@Test,#expect(),#require(), and async test methods. - Use
@Suite(.serialized)only when tests share mutable state. Do not add plain@Suitewithout traits. - Tests live in each package's
Tests/directory and inTests/PortuTests/. - For behavior changes, run focused tests first, then the full Xcode scheme before calling the work complete.
- When testing initializer default parameter behavior, call the initializer without those arguments instead of explicitly passing the default values.
Full Xcode scheme test command:
xcodebuild -project Portu.xcodeproj -scheme Portu -configuration Debug -derivedDataPath .build/DerivedData -skipMacroValidation test
TDD Rules
- Never write implementation code without a failing test first.
- Run tests after every change. Use XcodeBuildMCP when available.
- Full cycle: RED (failing test), GREEN (minimal implementation), REFACTOR (clean up).
- No implementation plans with code snippets. Tests are the spec.
- When a test fails unexpectedly, diagnose before changing the test.
- Run the full test suite before considering behavior complete.
- Read specs from
openspec/changes/before generating tests. - Refactor only when tests are green. Do not refactor and change behavior simultaneously.
Code Style
- Views:
*View,*Sheet,*Sidebar,*Panel. - Types:
*DTO,*Provider,*Service,*Error. - State:
*State,*Status. - Enums use descriptive names, such as
AccountKind,ExchangeType, andPositionType. - For composite lookup or deduplication keys, prefer a flat
Hashablestruct over nested dictionaries. - When passing URL-derived paths to
FileManagerAPIs that take string paths, useurl.path(percentEncoded: false). - When displayed percentages must sum to 100%, use largest-remainder rounding via
ListShares.rounded(_:fractionDigits:)in PortuCore; never add the whole residual to one row. - For chart data built from grouped dictionaries or other unordered sources, use deterministic multi-key sorting, such as date then category.
SwiftData Models
Models use cascade delete rules and inverse relationships. When adding new models, follow the existing pattern in Packages/PortuCore/Sources/Models/.
Never use #Predicate with optional chaining through relationships, such as $0.position?.account?.isActive. CoreData generates unsupported TERNARY SQL and crashes at runtime. Use @Query without a filter and filter in Swift instead.
- Avoid redundant SwiftData sort descriptors. If a query filters a field to a single value, do not add a secondary sort on that same field.
- When deduplicating snapshot-like records, include every field needed for identity in the key, such as
day,accountId, andassetId.
Formatting
SwiftFormat is used for code formatting and runs automatically via hooks on file edits.
SwiftFormat 0.64.0 or later is required: .swiftformat sets --minversion 0.64.0, and CI pins 0.64.0 in .github/workflows/ci.yml.
Agent skills
Issue tracker
Issues are tracked in GitHub Issues for Chalet-Labs/portu. See docs/agents/issue-tracker.md.
Triage labels
Triage uses the five default canonical labels — see docs/agents/triage-labels.md for the mapping and which of them exist in this repo's tracker today.
Domain docs
Domain documentation uses a single-context layout. See docs/agents/domain.md.
