diff --git a/AGENTS.md b/AGENTS.md index 6c3d454..19d9265 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Read only the guides relevant to the task: - [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) -- [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) - [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) - [Documentation](AgentGuidelines/Guidelines/Documentation.md) - [Logging](AgentGuidelines/Guidelines/Logging.md) diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md b/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md new file mode 100644 index 0000000..1f33702 --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md @@ -0,0 +1,60 @@ +--- +name: agent-guidelines-audit +description: Audit completed repository work against the consumer's applicable agent-guidelines, local AGENTS.md instructions, requested scope, and declared validation workflow. Use after implementing changes and before claiming completion, handing work to the user, preparing, opening, or updating a pull request, declaring merge readiness, or preparing a release. Do not use for simple answers, read-only exploration, or work that is still actively being implemented. +--- + +# Agent Guidelines Audit + +Perform a final, evidence-based compliance pass. Treat the applicable guidelines and local instructions as the source of truth; do not duplicate their full content in this skill. + +## Establish the audit scope + +1. Re-read the user request and list every requested outcome and explicit constraint. +2. Locate the repository root and every applicable `AGENTS.md` from the current directory to that root. +3. Read the shared guides referenced by those instructions that apply to the changed files and workflow. +4. Inspect `git status`, the complete diff, and relevant untracked files. Preserve unrelated user changes. +5. Check the consumer's `AgentGuidelines/VERSION` and provenance when the task changes or depends on the synchronized subtree. Do not update it implicitly. + +## Audit the implementation + +Review the actual change rather than only checking whether files exist: + +- Confirm every requested outcome is implemented and no material behavior was dropped. +- Confirm physical folders, familiar domain grouping, filenames, declaration order, type ownership, namespacing, documentation, and `MARK` organization follow the applicable guides. Distinguish values that describe data from tools that primarily execute algorithms or accumulate behavior. +- For Redux applications, trace actions, state, reducers, middleware, services, tools, presentation models, views, and side-effect results through the complete data flow. Confirm each Redux component folder contains only that component type. +- Check that framework objects, persistence, logging, and asynchronous work remain in their allowed boundaries. +- Check SwiftUI composition, narrow inputs, local versus durable state, localization, accessibility, and safe deterministic previews where applicable. +- Check tests for the required framework, mirrored paths, shared tags, Given/When/Then structure, deterministic seams, and coverage of changed behavior and failure paths. +- Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, and noise controls when logging changed. +- Check durable documentation, package configuration, CI/CD, Xcode project configuration, security-sensitive changes, and physical-device limitations when they are in scope. Compare documented Swift and concurrency settings with the effective application and test-target settings; flag both redundant isolation annotations and missing annotations at compiler-verified boundaries. +- Search for stale type names, superseded files, direct APIs forbidden by the new architecture, empty folders, and references to removed behavior. + +## Validate the evidence + +Run the repository's declared non-destructive checks in proportion to the change: + +- formatter and strict lint; +- focused tests, followed by the declared broader test plan when warranted; +- relevant builds or package validation; +- repository-specific validators; +- `git diff --check`. + +Use fresh successful evidence already produced in the same task instead of rerunning expensive checks without reason. Distinguish automated compilation and simulator evidence from hardware, signing, deployment, or manual validation that automation cannot prove. + +## Resolve findings + +- When the user authorized implementation, fix safe in-scope findings and rerun the affected checks. +- For review-only work, report findings without modifying code. +- Do not broaden the feature, rewrite unrelated files, edit a synchronized `AgentGuidelines/` subtree, or perform commits, pushes, pull requests, merges, tags, or releases without the required authority. +- Treat an unresolved required guideline violation or missing relevant validation as a blocker to claiming completion. + +## Hand off + +Summarize: + +- the instruction and guideline areas audited; +- findings fixed during the audit; +- validation commands and outcomes; +- any deliberate deviations, unavailable evidence, or remaining blockers. + +Do not say the work is done merely because the audit ran. Say it is ready only when the requested outcome is complete and the relevant evidence passes. diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml b/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml new file mode 100644 index 0000000..dc4aab7 --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Agent Guidelines Audit" + short_description: "Audit completed work against shared guidelines" + default_prompt: "Use $agent-guidelines-audit to audit this completed change before handoff." diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md index 7c39de6..c6eef34 100644 --- a/AgentGuidelines/AGENTS.md +++ b/AgentGuidelines/AGENTS.md @@ -19,6 +19,7 @@ This public repository is the versioned source of truth for reusable ThatFactory - Keep examples generic and concise. - Use relative Markdown links inside this repository. - Update `README.md` when adding, moving, or removing a guide. +- Keep the README guideline catalog sorted alphabetically by link label. - Update `CHANGELOG.md` and `VERSION` for a release. - When releasing a new version, update the version in both the README installation command and the README consumer-update command. Keep both commands aligned with the new release, for example: diff --git a/AgentGuidelines/CHANGELOG.md b/AgentGuidelines/CHANGELOG.md index 995b21d..e622148 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/AgentGuidelines/CHANGELOG.md @@ -2,6 +2,77 @@ All notable changes to this project are documented in this file. +## [0.0.16] - 2026-08-13 + +### Changed + +- Required SwiftUI dynamic properties to precede ordinary stored properties and clarified deterministic preview expectations. +- Required one top-level type per file, focused function decomposition, logical enum grouping, and consistent declaration-modifier and multiline-signature layout. +- Documented which declaration layout conventions remain review-guided because swift-format cannot enforce them without broad source reflow. + +## [0.0.15] - 2026-07-27 + +### Added + +- A shared agent-workflow guide for bounded grouping of independent repository inspections, with dependency, ordering, scope, and output-size safeguards. +- A versioned global Codex instruction template that bootstraps discovery of repository-local guidance without duplicating engineering policy. + +### Changed + +- Linked the workflow guide from the consumer template, documented the manual global Codex setup, and required alphabetical ordering of the README guideline catalog. +- Clarified that Codex review requests are automatic by default and must not be triggered manually without an explicit user request. + +## [0.0.14] - 2026-07-27 + +### Changed + +- Clarify Store/Middleware @MainActor usage. +- Removed workaround for a resolved Xcode issue. + +## [0.0.13] - 2026-07-26 + +### Added + +- A reusable `agent-guidelines-audit` skill and mandatory completion gate before handoff, pull requests, merge readiness, and releases. +- A canonical Redux Store template plus dependency-container and middleware-composition guidance. +- Consumer Stack guidance for recording toolchain, platform, strict-concurrency, and actor-isolation settings. + +### Changed + +- Clarified Redux folder ownership, familiar domain grouping, model-versus-tool classification, service-local helpers, presentation models, and one-component-per-file organization. +- Required documentation for new Swift declarations, meaningful `MARK` sections, one meaningful SwiftUI view per file, and deterministic previews where possible. +- Clarified when target isolation defaults replace explicit annotations and when compiler-verified boundaries still require them. +- Enabled conditional-import sorting and expanded validation for Swift templates, the audit skill, Stack guidance, and formatting policy. + +## [0.0.12] - 2026-07-25 + +### Added + +- Login-shell guidance for using explicitly authorized `gh` credentials exported by local shell startup configuration without exposing token values. + +## [0.0.11] - 2026-07-25 + +### Added + +- Pre-compilation Xcode build-phase guidance and a reusable `format-and-lint` command for human and agent workflows. +- An easy-to-find record of Xcode-aligned layout settings, enabled rule overrides, and deliberate non-adoptions. +- Pull-request guidance that prevents duplicate manual Codex requests when automatic review is enabled. + +### Changed + +- Enabled empty-array literals, force-try rejection, brace whitespace cleanup, `where` clauses in eligible loops, and documentation-comment validation. + +## [0.0.10] - 2026-07-24 + +### Added + +- Shared Xcode-aligned swift-format and EditorConfig configuration. +- Reusable format, warning-lint, and strict-lint commands for Swift consumers. + +### Changed + +- Replaced SwiftLint guidance with toolchain-native swift-format guidance. + ## [0.0.9] - 2026-07-23 ### Added diff --git a/AgentGuidelines/Configurations/Swift/.editorconfig b/AgentGuidelines/Configurations/Swift/.editorconfig new file mode 100644 index 0000000..f3faacc --- /dev/null +++ b/AgentGuidelines/Configurations/Swift/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*.swift] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 120 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/AgentGuidelines/Configurations/Swift/.swift-format b/AgentGuidelines/Configurations/Swift/.swift-format new file mode 100644 index 0000000..ee5586f --- /dev/null +++ b/AgentGuidelines/Configurations/Swift/.swift-format @@ -0,0 +1,81 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentBlankLines" : false, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "indentation" : { + "spaces" : 4 + }, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineBreakBetweenDeclarationAttributes" : false, + "lineLength" : 120, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "multilineTrailingCommaBehavior" : "keptAsWritten", + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "orderedImports" : { + "includeConditionalImports" : true, + "shouldGroupImports" : true + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "reflowMultilineStringLiterals" : "never", + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : true, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "AvoidRetroactiveConformances" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : true, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyLinesOpeningClosingBraces" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : true, + "ValidateDocumentationComments" : true + }, + "spacesAroundRangeFormationOperators" : false, + "spacesBeforeEndOfLineComments" : 2, + "tabWidth" : 4, + "version" : 1 +} diff --git a/AgentGuidelines/Guidelines/AgentWorkflow.md b/AgentGuidelines/Guidelines/AgentWorkflow.md new file mode 100644 index 0000000..ebf6f13 --- /dev/null +++ b/AgentGuidelines/Guidelines/AgentWorkflow.md @@ -0,0 +1,49 @@ +# Agent Workflow + +Use this guide for repository investigation and tool execution. It governs how work is explored and coordinated; language, architecture, testing, and development requirements remain in their respective guides. + +This guidance is motivated by high token consumption from unnecessary model and tool cycles during read-heavy investigation, as described in [openai/codex#35050](https://github.com/openai/codex/issues/35050). It aims to avoid unnecessary cycles while preserving coverage and correctness; it does not guarantee a particular reduction in token usage. + +## Bounded investigation + +Investigate in bounded stages based on the current task. + +Within a stage, group independent, already-known read-only operations when the available tools support doing so efficiently. Examples include targeted searches, reads of already-identified files, independent metadata checks, and inspection of separate tests or call sites. + +Use an appropriate supported mechanism for grouped or concurrent execution. A current implementation might use batched tool calls, concurrent shell operations, `Promise.allSettled`, or an equivalent approach, but no particular API is required. + +Inspect every result relevant to the conclusion. Account for failed, incomplete, and contradictory results rather than treating execution as successful merely because it was grouped. + +## Dependency and ordering + +Keep operations sequential when a result determines the next step or when ordering is observable. + +This includes: + +- adaptive investigation; +- approval-sensitive operations; +- related or conflicting mutations; +- edits followed by compilation or validation; +- diagnostics whose result determines the next change; +- stateful external operations; +- waits and resumptions. + +Architecture-specific ordering requirements remain authoritative. For example, follow the Redux guide for dispatch and side-effect ordering rather than inferring that investigation-level concurrency permits runtime concurrency. + +Do not group operations merely because concurrency is available. + +## Scope and output + +Keep each stage narrowly scoped to the request. + +Prefer targeted searches, relevant line ranges, focused diagnostics, and specific log sections over broad repository, file, or log dumps. + +Bound the combined output of grouped operations so that every result can be inspected reliably. When evidence is incomplete or truncated, retrieve only the missing portion rather than repeating the full investigation. + +Do not expand the investigation merely because additional operations can be executed concurrently. + +## Efficiency + +Avoid unnecessary repeated model and tool cycles when several independent operations are already known. + +Efficiency must not reduce required coverage, bypass validation, conceal failures, or introduce unrelated work. diff --git a/AgentGuidelines/Guidelines/Architecture/Redux.md b/AgentGuidelines/Guidelines/Architecture/Redux.md index 56ab6f3..4a28847 100644 --- a/AgentGuidelines/Guidelines/Architecture/Redux.md +++ b/AgentGuidelines/Guidelines/Architecture/Redux.md @@ -11,7 +11,7 @@ Use this guide for applications that explicitly adopt the ThatFactory Redux arch - Middleware performs asynchronous work and other side effects. - Services wrap external frameworks, packages, persistence, clocks, APIs, and system capabilities. - Selectors derive shared domain information from state. -- Render-ready view state and view-only projections live beside their consuming views. +- Render-ready value models live under `Model/`; SwiftUI `View` types stay under `View/`. - Every side-effect result returns to the store as an action before it changes state. ## Data flow @@ -41,15 +41,7 @@ The store reduces the original action first, then awaits middleware and sequenti ## Store -Use one observable store as the source of truth and inject it at the application root. A store implementation may expose aliases like these: - -```swift -typealias AppStore = Store -typealias StateType = Equatable & Codable -typealias ActionType = Equatable -typealias Reducer = (State, Action) -> State -typealias Middleware = (State, Action) async -> Action? -``` +Use one observable store as the source of truth and inject it at the application root. The canonical Store requires `Default Actor Isolation` set to `MainActor` and `nonisolated(nonsending) By Default` set to `Yes` in every application and test target that compiles or exercises it. New projects copy [the Store template](../../Templates/Store.swift) as is; do not add redundant isolation annotations or change its dispatch ordering, observation exclusions, or documentation. Dispatch is asynchronous and ordered: @@ -61,6 +53,31 @@ Dispatch is asynchronous and ordered: Use only `await store.dispatch(_:)`. Do not add a fire-and-forget dispatch API. +## Dependency composition + +Create one application-owned `DependencyContainer` that constructs and retains services, persistence, providers, and other side-effect dependencies. Create the container before the store, restore synchronous initial state through its dependencies, and pass the container to `makeMiddlewares(_:)`. + +```swift +@main +struct ExampleApp: App { + @State private var dependencies: DependencyContainer + @State private var store: AppStore + + init() { + let dependencies = DependencyContainer() + let store = AppStore( + initialState: dependencies.restoredAppState(), + middlewares: makeMiddlewares(dependencies), + reducer: appReducer + ) + _dependencies = State(initialValue: dependencies) + _store = State(initialValue: store) + } +} +``` + +Keep application bootstrap responsible for composition, not feature behavior. Do not construct individual services directly in the app after a dependency container exists. + ## Canonical physical folders These are filesystem folders, not Xcode groups. New single-application repositories use this structure by default: @@ -79,7 +96,6 @@ These are filesystem folders, not Xcode groups. New single-application repositor |-- Services/ |-- Tools/ |-- View/ -| `-- / `-- Resources/ Tests/ @@ -94,7 +110,6 @@ These are filesystem folders, not Xcode groups. New single-application repositor |-- Services/ |-- Tools/ `-- View/ - `-- / ``` A multi-target application may use a shared source root such as `Shared/Redux/` and target-specific roots such as `/View/`. Its root `AGENTS.md` must provide a concrete path map: @@ -119,7 +134,18 @@ Put application bootstrap, app delegates, scene definitions, store construction, ### Model -Put reusable domain values in `Model/`. Keep each important type in a focused file. Do not hide response models, payloads, or domain values inside action or service files merely because only one caller currently uses them. +Put domain and presentation values in `Model/`. Models describe data, state, configuration, categories, or render-ready values; their primary responsibility is not executing an algorithm or coordinating side effects. Keep each important type in a focused file. Do not hide response models, payloads, logging categories, levels, or other values inside action or service folders merely because only one caller currently uses them. + +When several models are familiar parts of one domain, group them by that domain: + +```text +Model/ +|-- Camera/ +|-- Face/ +`-- Logging/ +``` + +Use names that help a reader reason about the domain. Keep `Model/` flat while a domain has only one file; do not create a folder for every type. ### Action @@ -134,6 +160,10 @@ enum AppAction: Equatable { Name actions after what happened or what the user requested. Keep cases in the order required by the project's Swift style guide. +Declare `AppAction` and each domain action in separate files. `AppAction.swift` contains the root routing action only; do not append logging models, categories, feature actions, or unrelated supporting declarations to it. + +Every production file under `Redux/Action/` must define an action. Values carried by actions, including categories, levels, payloads, and capability descriptions, belong in `Model/`. + ### State Put the root state and domain sub-states in `Redux/State/`. Prefer focused value types with compiler-synthesized conformances. Add a new sub-state for a durable domain instead of folding unrelated values into an existing feature. @@ -142,6 +172,8 @@ State stores durable facts. Avoid storing values that are cheap, deterministic d Sub-states should conform to `Equatable` and `Codable`; add `Sendable` when their values and concurrency boundaries require it. Keep root state and root actions for genuine cross-domain behavior. Keep domain action cases descriptive of intent or outcomes and route them through the root action. +Declare `AppState` and each domain sub-state in separate files. `AppState.swift` contains the root state only. + ### Reducer Put reducer functions in `Redux/Reducer/`. A reducer receives state and an action and returns new state. It must not: @@ -155,13 +187,17 @@ Put reducer functions in `Redux/Reducer/`. A reducer receives state and an actio Use the smallest state and action inputs that correctly express the transition. Root reducers compose domain reducers. +Declare the root reducer and each domain reducer in separate files. `AppReducer.swift` contains only root composition. Every production file under `Redux/Reducer/` must define a reducer; move events, capability values, policies, and other supporting domain types to `Model/` or their own appropriate component. + ### Middleware Put middleware in `Redux/Middleware/`. Middleware may call injected services and return a follow-up action. It must not mutate store state directly. Inject services, providers, managers, clocks, and identifier generators through parameters so middleware tests remain deterministic. Register middleware in one root composition file such as `AppMiddlewares.swift`. Reducers own every state mutation. -Create a feature subfolder when a domain has multiple middleware files: +Every production file under `Redux/Middleware/` must define or compose middleware. A helper, closure signature, or type alias used only by one middleware stays in that middleware file and should be private when its test seam and call sites allow it. Do not create a standalone middleware file for a declaration that is not middleware. + +Create a feature subfolder only when a domain has multiple middleware files: ```text Redux/Middleware/Account/ @@ -178,32 +214,33 @@ Do not put SwiftUI types, colors, images, localized display strings, or render-r ### Services -Put focused external-boundary abstractions in `Services//`. Services wrap APIs, persistence, packages, frameworks, sensors, system features, and other impure operations. Middleware calls services; views and reducers do not. +Put focused external-boundary abstractions in `Services/`. Services wrap APIs, persistence, packages, frameworks, sensors, system features, and other impure operations. Keep this folder flat while a capability has only one file; introduce a familiar capability folder such as `Services/FaceService/` or `Services/CalibrationService/` when that capability genuinely requires several related files. Middleware calls services; views and reducers do not. Prefer a protocol or otherwise injectable contract when a service must be replaced in tests. Keep transport-specific details behind the service boundary. +Keep a supporting delegate, adapter, or helper beside its service when only that capability uses it. Local ownership is clearer than promoting a service-private framework bridge to a global `Tools/` folder. + Views dispatch actions; middleware calls services. Views never call a service directly for Redux-owned behavior. ### Tools -Put genuinely cross-cutting implementation utilities in `Tools/`. This is not a miscellaneous folder. Feature-only formatters, helpers, constants, or factories stay beside that feature. Promote them to `Tools/` only after they have a clear cross-feature role. +Put specialized algorithms, accumulators, framework adapters, and genuinely cross-cutting implementation utilities in `Tools/`. This is not a miscellaneous folder. A type belongs here when its primary responsibility is performing computation or implementing technical behavior rather than describing values or owning an external capability. Feature-only helpers stay beside that feature. Keep `Tools/` flat until one familiar topic requires several files, then group them under a domain folder such as `Tools/Face/`. ### View -Put SwiftUI screens and components in `View//`. A new view belongs to the feature it renders, not in Redux. Reusable visual components may use `View/Generic/` or another explicitly declared shared-view folder. +Put SwiftUI screens and components in `View/`. A new view belongs to the feature it renders, not in Redux. Reusable visual components may use `View/Generic/` or another explicitly declared shared-view folder. Keep `View/` flat while it has only a few files; introduce `View//` when a familiar feature genuinely has several views. -Render-facing view-state types and projections live beside the consuming view: +Render-facing value types that do not conform to `View` are presentation models and live under `Model//`: ```text +Model/Account/ +`-- AccountViewState.swift + View/Account/ -|-- AccountView.swift -|-- AccountViewState.swift -`-- AccountViewStateProjection.swift +`-- AccountView.swift ``` -If a projection exists only to render one screen, it is view-layer code even when its input is `AppState`. - -Projection tests mirror the production view path under the test target. +Keep a tiny private projection beside its consuming view only when it is an implementation detail rather than a named value type. ### Resources @@ -213,10 +250,12 @@ Put catalogs, assets, preview assets, configuration resources, and test plans in - Prefer one primary concern per file. - When a feature has several files of one Redux component, introduce a feature subfolder under that component. +- Group several related models, services, or tools by a familiar domain or capability so readers can reason about them together. - Keep root routing and composition at the component root; keep feature implementations below it. - File names match their primary type or clearly describe their primary pure function. - Do not introduce artificial enum namespaces solely to satisfy filename lint rules. - Mirror production organization in tests so components are easy to locate. +- Do not keep empty component folders. Add `Selector/`, `Tools/`, feature folders, or mirrored test folders only when they contain a real implementation. ## SwiftUI connection @@ -230,18 +269,18 @@ Prefer narrow view inputs or a focused view-state projection. This aligns SwiftU | Step | Change | Default destination | |---|---|---| -| 1 | Define domain models | `Model//` | +| 1 | Define domain models | `Model/` or `Model//` when several are familiar | | 2 | Define feature state | `Redux/State/State.swift` | | 3 | Add it to root state | `Redux/State/AppState.swift` | | 4 | Define feature actions | `Redux/Action/Action.swift` | | 5 | Route them through the root action | `Redux/Action/AppAction.swift` | | 6 | Implement the reducer | `Redux/Reducer/Reducer.swift` | | 7 | Compose the reducer | `Redux/Reducer/AppReducer.swift` | -| 8 | Add side effects if needed | `Redux/Middleware//` | +| 8 | Add side effects if needed | `Redux/Middleware/` or a feature folder when several | | 9 | Register middleware | `Redux/Middleware/AppMiddlewares.swift` | -| 10 | Add external boundaries if needed | `Services//` | +| 10 | Add external boundaries if needed | `Services/` or `Services//` when several | | 11 | Add shared domain selectors if needed | `Redux/Selector//` | -| 12 | Build the feature UI | `View//` | +| 12 | Build the feature UI | `View/` or `View//` when several are familiar | | 13 | Mirror tests | `Tests/` | Skip components that provide no value. A state-only transition needs no middleware; a screen-only projection does not need a Redux selector. @@ -252,7 +291,7 @@ Skip components that provide no value. A state-only transition needs no middlewa - Selector tests provide state and assert the derived domain result. - Middleware tests inject mocks, execute an action, and assert the returned follow-up action. - Service tests exercise the external boundary without involving views. -- View-state projection tests live under the matching `Tests/View//` folder, or the consumer-mapped test root. +- Presentation-model tests live under the matching `Tests/Model//` folder, or the consumer-mapped test root. - Test mocks and fixture data live under the test target's `Mocks/` folder. Follow [Unit testing](../Testing/UnitTesting.md) for framework and concurrency conventions. diff --git a/AgentGuidelines/Guidelines/Development.md b/AgentGuidelines/Guidelines/Development.md index afce49f..8f93d90 100644 --- a/AgentGuidelines/Guidelines/Development.md +++ b/AgentGuidelines/Guidelines/Development.md @@ -19,6 +19,12 @@ AgentGuidelines/** linguist-generated Keep each subtree update in its own commit. In the pull-request description, state the old and new guideline versions and link to the central release or pull request where the guideline changes were reviewed. Continue validating the checked-in subtree in CI. Because generated-file diffs are collapsed by default, never edit the subtree locally; make shared changes in the source repository and consume a tagged release. +## Completion audit + +Before claiming implementation is complete, handing work to the user, preparing, opening, or updating a pull request, declaring merge readiness, or preparing a release, invoke `$agent-guidelines-audit`. + +If the skill is not discoverable in a subtree consumer, read and follow its [SKILL.md](../.agents/skills/agent-guidelines-audit/SKILL.md) directly. The audit is a final verification gate, not a substitute for reading and applying the relevant guidelines during implementation. Resolve in-scope findings and rerun affected checks before handoff. Do not broaden the requested scope merely to satisfy the audit. + ## Logging Applications own their orchestration, lifecycle, and product-domain diagnostics. Follow the shared [logging guide](Logging.md) and rely on each dependency to log its own implementation. Do not duplicate or reformat package-internal operations in the application log. diff --git a/AgentGuidelines/Guidelines/Documentation.md b/AgentGuidelines/Guidelines/Documentation.md index 2a0d327..4cc60c5 100644 --- a/AgentGuidelines/Guidelines/Documentation.md +++ b/AgentGuidelines/Guidelines/Documentation.md @@ -6,7 +6,8 @@ ## Code-level documentation -- Document structs, classes, enums, protocols, actors, and other significant types with focused `///` DocC comments. +- Document every new struct, class, enum, protocol, actor, and function with focused `///` DocC comments. +- Use `// MARK: -` pragmas to separate meaningful logical sections so source files remain easy to scan and navigate. - Update documentation when changing a documented API, parameter, behavior, or invariant. - End documentation sentences with periods. - Explain intent, contracts, units, side effects, isolation, and non-obvious constraints; do not restate syntax. diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/AgentGuidelines/Guidelines/Git/Repositories.md index bff7933..04e66ce 100644 --- a/AgentGuidelines/Guidelines/Git/Repositories.md +++ b/AgentGuidelines/Guidelines/Git/Repositories.md @@ -40,3 +40,13 @@ When `gh` authentication appears inconsistent: 5. Use SSH for Git transport only when the CLI remains unavailable after retry and the operation is specifically a Git fetch, commit, or push. Continue using `gh` for GitHub API operations whenever it is working. An environment mismatch is not evidence that the user's GitHub account or token is invalid. Record the failed command and exact non-secret error, retry after the authentication check, and report the blocker only after repeated attempts fail. + +### Login-shell credentials + +Some developer environments export `GITHUB_TOKEN` from a shell startup file rather than from the non-interactive process that launched the agent. When the user has explicitly authorized using that local configuration, retry `gh` in a login shell that sources the user's startup configuration: + +```sh +zsh -lc 'source "$HOME/.zshrc"; gh auth status' +``` + +Run the required `gh` operation in that same shell after authentication succeeds. Never print, inspect, copy, or persist the token value; suppress unrelated startup output when practical, and do not source a startup file merely to bypass a credential or permission boundary without the user's authorization. diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/AgentGuidelines/Guidelines/GitHub/PullRequests.md index 3535d36..3a2c349 100644 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ b/AgentGuidelines/Guidelines/GitHub/PullRequests.md @@ -8,6 +8,7 @@ Use this guide whenever creating, reviewing, updating, or merging a GitHub pull - Follow the repository's pull-request template and local contribution instructions. - Run the relevant local validation and document anything that could not be run. - Open the pull request without auto-merge and keep it unmerged while automated or agent review is pending. Use draft state only when configured reviewers also run on drafts. +- When automatic Codex review is enabled, opening the pull request schedules the review. Do not also post `@codex review` or make another manual request; duplicate reviews waste review capacity and tokens. Do not request a Codex review manually unless the user explicitly asks for one. ## Consumer subtree review scope diff --git a/AgentGuidelines/Guidelines/Swift/Swift.md b/AgentGuidelines/Guidelines/Swift/Swift.md index 2f6305e..6ae523d 100644 --- a/AgentGuidelines/Guidelines/Swift/Swift.md +++ b/AgentGuidelines/Guidelines/Swift/Swift.md @@ -22,9 +22,11 @@ ## State and isolation - Treat actor isolation as part of an API's contract. -- Mark UI-bound reference models `@MainActor` unless the target's default actor isolation already provides it. -- Avoid adding `@MainActor` to tests or domain types merely to silence a diagnostic. Resolve the actual isolation boundary. +- When application and test targets use MainActor default isolation, infer isolated conformances, and `nonisolated(nonsending)` by default, omit annotations that merely restate those effective settings. Verify every affected target before removing annotations. +- `nonisolated(nonsending)` by default governs how nonisolated asynchronous functions run; it does not make synchronous types or conformances nonisolated. Keep explicit `nonisolated` where a value conformance must satisfy a `Sendable` generic contract, a synchronous API is called from a `@Sendable` closure, or another compiler-verified actor boundary requires it. +- Keep an explicit isolation annotation when a declaration intentionally differs from the target default, crosses an actor boundary, belongs to reusable code compiled under different defaults, or implements a documented compiler workaround. - Use `Sendable` where values cross concurrency domains and their stored values support it. +- Avoid adding `@MainActor` to tests or domain types merely to silence a diagnostic. Resolve the actual isolation boundary. ## C-family interoperability diff --git a/AgentGuidelines/Guidelines/Swift/SwiftFormat.md b/AgentGuidelines/Guidelines/Swift/SwiftFormat.md new file mode 100644 index 0000000..b5060fb --- /dev/null +++ b/AgentGuidelines/Guidelines/Swift/SwiftFormat.md @@ -0,0 +1,56 @@ +# Swift Format + +## Workflow + +- Treat formatting and lint rules as readability and correctness tools, not as architecture. +- Use the shared configuration under `Configurations/Swift/`; consumers expose it through root `.swift-format` and `.editorconfig` symlinks so Xcode, local commands, and CI agree. Configuration discovery is hierarchical, while an explicit `--configuration` path is unconditional. +- In Xcode, use **Editor > Structure > Format File with 'swift-format'** (or the corresponding selection command) when you want to rewrite source. +- After changing Swift source, humans and agents run `AgentGuidelines/Scripts/swift_format.sh format-and-lint ` before handoff. Do this even when a later build would provide the same safety net. +- Run `AgentGuidelines/Scripts/swift_format.sh format ` when only rewriting source is required. +- Run `AgentGuidelines/Scripts/swift_format.sh lint ` for non-blocking local warnings and `lint-strict` for errors that block CI. +- Fix findings introduced by a change. Formatter-supported rules are corrected by `format`; linter-only rules require a source change. + +## Xcode build integration + +- Add a **Swift Format** run-script phase to every independently buildable app or test target that compiles Swift source. Place it before **Compile Sources** so compilation consumes the formatted files. +- Skip the phase when `CI=true`; CI must remain non-mutating and run `lint-strict` in one dedicated job. +- Invoke `AgentGuidelines/Scripts/swift_format.sh format-and-lint` only over source folders compiled by that target, including shared folders it consumes. Exclude unrelated app and test sources so an invalid file outside the selected build cannot block compilation. +- Run the phase on every build rather than using dependency analysis. A no-op formatting pass is intentionally cheaper than allowing locally generated formatting debt. +- Source mutation requires either declared source inputs and outputs or disabling Xcode's **User Script Sandboxing** for the affected configurations. Record and review that choice locally; never disable sandboxing without the formatting phase requiring it. +- Validate the integration in Xcode with an open, deliberately misformatted file. Confirm formatting happens before compilation and that editor saving, cursor state, and undo behavior remain acceptable. + +## Shared customizations + +The checked-in configuration starts from the exhaustive Xcode toolchain dump. These deliberate overrides are the shared policy and must be reapplied when the toolchain changes. + +### Xcode-aligned layout + +- `indentation`: 4 spaces +- `tabWidth`: 4 +- `lineLength`: 120 +- `indentSwitchCaseLabels`: `false` +- Swift-only EditorConfig settings mirror indentation, line length, LF newlines, final newlines, and trailing-whitespace cleanup. + +### Rules enabled beyond the dumped defaults + +- `AlwaysUseLiteralForEmptyCollectionInit`: keeps empty arrays concise and replaces the relevant SwiftLint array/empty-collection checks. +- `NeverUseForceTry`: retains a production safety check; swift-format exempts supported test code. +- `NoEmptyLinesOpeningClosingBraces`: replaces SwiftLint's opening- and closing-brace vertical-whitespace checks. +- `UseWhereClausesInForLoops`: preserves the former SwiftLint `for_where` behavior. +- `ValidateDocumentationComments`: validates documentation already present, including parameter coverage after signature changes, without requiring every declaration to be documented. +- `includeConditionalImports`: sorts imports inside conditional-compilation blocks together with ordinary imports. + +Rules not listed here retain the exhaustive Xcode dump values. In particular, universal public documentation, force-unwrap rejection, implicit-return rewriting, early-exit rewriting, leading-underscore rejection, and implicitly unwrapped optional rejection remain disabled until adopted deliberately. swift-format has no equivalent for repository-specific import bans or sorted enum cases. + +Declaration layout rules from [Swift style](SwiftStyle.md), including keeping modifiers on the declaration line and preserving an intentionally multiline signature, remain review-guided. The formatter preserves a correctly authored layout, but it has no focused rule that forces those shapes; disabling `respectsExistingLineBreaks` would broadly reflow otherwise intentional source formatting. + +## Focused exceptions + +- Prefer a focused `// swift-format-ignore: RuleName` immediately before the affected declaration or statement when a rule conflicts with required semantics. Add a short preceding comment explaining why. +- Do not ignore a whole file or disable a shared rule to avoid fixing one occurrence. + +## Toolchain updates + +- When the supported Xcode toolchain changes, regenerate the exhaustive configuration with `xcrun swift-format dump-configuration`, reapply the documented Xcode-aligned values, review the resulting policy change, and release it centrally before consumer adoption. + +See swift-format's [configuration](https://github.com/swiftlang/swift-format/blob/main/Documentation/Configuration.md), [rule](https://github.com/swiftlang/swift-format/blob/main/Documentation/RuleDocumentation.md), and [focused suppression](https://github.com/swiftlang/swift-format/blob/main/Documentation/IgnoringSource.md) documentation for the underlying behavior. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftLint.md b/AgentGuidelines/Guidelines/Swift/SwiftLint.md deleted file mode 100644 index c37477b..0000000 --- a/AgentGuidelines/Guidelines/Swift/SwiftLint.md +++ /dev/null @@ -1,8 +0,0 @@ -# SwiftLint - -- Treat lint rules as readability and correctness tools, not as architecture. -- Fix warnings introduced by a change. -- Do not add enum namespaces, empty wrapper types, or other artificial structures solely to satisfy filename rules for pure-function files such as reducers, selectors, or middleware. -- Prefer a focused local disable with a short reason when a rule conflicts with the intended design. -- Do not disable a rule repository-wide to avoid fixing one occurrence. -- Keep the lint configuration aligned with the physical folder organization and generated-file exclusions of the consumer repository. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md b/AgentGuidelines/Guidelines/Swift/SwiftStyle.md index 49a5aed..6277537 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md +++ b/AgentGuidelines/Guidelines/Swift/SwiftStyle.md @@ -8,9 +8,14 @@ - Keep enum cases alphabetical unless ordering communicates behavior or a local lint suppression documents the exception. - Use `// MARK: -` to separate meaningful sections. - Use `// MARK: - Private` when separating private implementation from non-private declarations in the same file. +- Break branching or multi-step implementation into small, focused functions whose names make the caller read as a sequence of intentions. Keep orchestration concise, move implementation details below `// MARK: - Private`, and avoid extracting trivial expressions that are clearer inline. - Do not add Xcode boilerplate filename, author, or creation-date headers. -- Prefer one primary type or concern per file. +- Keep each top-level type in its own file, even when multiple types are closely related. Nest a supporting type only when it is private to one primary type and the relationship forms a natural namespace. - Match a type file's name to its primary type. +- Put the declaration named by the file immediately after imports and file-level directives. Opening `EffectAssetLoader.swift`, for example, must reveal `EffectAssetLoader` before supporting declarations. A shared canonical template may retain type aliases that its documented layout deliberately places first. +- Keep declaration modifiers such as `nonisolated` on the same line as the declaration they modify. For a multiline function signature, keep the opening brace on the return-type line. +- Separate groups of enum cases with blank lines when the groups represent distinct operations, phases, or workflows. Keep cases consistently ordered within each group; meaningful workflow order may override alphabetical order. +- Keep physical folders flat until one topic genuinely contains several files. When grouping becomes useful, organize related models, services, tools, views, and Redux components by a familiar domain, feature, or capability so readers can reason about them together. Example: @@ -23,3 +28,30 @@ withAnimation { isPresented = true } ``` + +Namespaced supporting types keep their ownership visible: + +```swift +struct Measurement { + // ... +} + +// MARK: - Errors + +extension Measurement { + enum ValidationError: Error { + case invalidValue + } +} +``` + +Multiline declarations keep their modifiers and braces attached to the declaration: + +```swift +nonisolated func reduce( + _ state: State, + _ action: Action +) -> State { + // ... +} +``` diff --git a/AgentGuidelines/Guidelines/Swift/SwiftUI.md b/AgentGuidelines/Guidelines/Swift/SwiftUI.md index 4b39758..cbc3185 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftUI.md +++ b/AgentGuidelines/Guidelines/Swift/SwiftUI.md @@ -4,8 +4,10 @@ Use official Apple documentation and Xcode's current SwiftUI skills for API-spec ## View structure +- Put SwiftUI dynamic properties such as `@Environment`, `@Query`, `@State`, and `@Binding` before ordinary stored `let` and `var` properties. Keep injected environment dependencies before locally owned state when both are present. - Keep a parent view focused on composition. - Model meaningful sections such as headers, lists, metadata, sidebars, and footers as separate `View` types with narrow inputs. +- Keep each independently meaningful `View` in its own file, including private supporting views. Give every view its own deterministic preview when the required dependencies can be represented safely; when they cannot, document the concrete limitation in the handoff. - Do not extract sections into computed `some View` properties merely to shorten `body`; computed properties remain in the parent's invalidation boundary. - Tiny fragments reused within one body may use a small helper when they have no independent state, input, or invalidation story. - Keep view initializers cheap. Do not decode data, access files, build large structures, or allocate formatters in `init`. diff --git a/AgentGuidelines/README.md b/AgentGuidelines/README.md index 7a801a1..e29c651 100644 --- a/AgentGuidelines/README.md +++ b/AgentGuidelines/README.md @@ -9,9 +9,9 @@ # Agent Guidelines -`agent-guidelines` is ThatFactory's public, versioned source of truth for reusable instructions given to coding agents. It centralizes stable decisions about Swift development, Redux architecture, testing, documentation, logging, packages, CI/CD, localization, and Xcode tooling while leaving product context and exceptions in each consuming repository. +`agent-guidelines` is ThatFactory's public, versioned source of truth for reusable instructions and development configuration. It centralizes stable decisions about Swift development, Redux architecture, testing, documentation, logging, packages, CI/CD, localization, and Xcode tooling while leaving product context and exceptions in each consuming repository. -The repository contains documentation, not a Swift product. Consumers install a tagged release as a Git subtree at `AgentGuidelines/`, so every agent sees ordinary version-controlled files at predictable paths. +The repository contains documentation and supporting configuration, not a Swift product. Consumers install a tagged release as a Git subtree at `AgentGuidelines/`, so every agent and supported tool sees ordinary version-controlled files at predictable paths. ## How it fits together @@ -25,7 +25,7 @@ The repository contains documentation, not a Swift product. Consumers install a git subtree add/pull | v -+---------------- Consumer project or package ----------------+ ++---------------- Consumer project or package -----------------+ | | | AGENTS.md | | |-- local product/package context | @@ -35,11 +35,12 @@ The repository contains documentation, not a Swift product. Consumers install a | | | | AgentGuidelines/ | | | |-- VERSION | | +| |-- Configurations/ | | | `-- Guidelines/ <----------------------------------+ | -| |-- Architecture/Redux.md | -| |-- Swift/SwiftUI.md | -| |-- Testing/UnitTesting.md | -| `-- Xcode/MCP.md | +| |-- Architecture/Redux.md | +| |-- Swift/SwiftUI.md | +| |-- Testing/UnitTesting.md | +| `-- Xcode/MCP.md | | | | Sources and project files | +----------------------------+---------------------------------+ @@ -58,24 +59,25 @@ The subtree does not automatically import every guide into an agent's context. A ## Guideline catalog +- [Agent workflow and tool execution](Guidelines/AgentWorkflow.md) +- [CI/CD](Guidelines/CICD.md) +- [Development and reusability](Guidelines/Development.md) +- [Documentation](Guidelines/Documentation.md) +- [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) +- [GitHub pull requests](Guidelines/GitHub/PullRequests.md) +- [Localization](Guidelines/Swift/Localization.md) +- [Logging](Guidelines/Logging.md) - [Redux architecture and physical folder organization](Guidelines/Architecture/Redux.md) - [Swift](Guidelines/Swift/Swift.md) +- [Swift format](Guidelines/Swift/SwiftFormat.md) +- [Swift packages](Guidelines/Packages.md) - [Swift style](Guidelines/Swift/SwiftStyle.md) - [SwiftUI](Guidelines/Swift/SwiftUI.md) -- [SwiftLint](Guidelines/Swift/SwiftLint.md) -- [Localization](Guidelines/Swift/Localization.md) - [Unit and integration testing](Guidelines/Testing/UnitTesting.md) -- [Documentation](Guidelines/Documentation.md) -- [Logging](Guidelines/Logging.md) -- [Swift packages](Guidelines/Packages.md) -- [Development and reusability](Guidelines/Development.md) -- [CI/CD](Guidelines/CICD.md) -- [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) -- [GitHub pull requests](Guidelines/GitHub/PullRequests.md) - [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) - [Xcode security audits](Guidelines/Xcode/Security.md) -Only reference the guides that apply. A UI-agnostic package normally uses Swift, style, testing, documentation, logging, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. +Only reference the guides that apply. Agent workflow normally applies to both applications and packages. A UI-agnostic package normally also uses Swift, style, testing, documentation, logging, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. ## Add to a consumer @@ -85,8 +87,15 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.9 \ - --squash + 0.0.16 \ + --squash +``` + +Swift consumers that adopt the shared formatter expose its configuration at the repository root so Xcode and other tools discover it: + +```sh +ln -s AgentGuidelines/Configurations/Swift/.swift-format .swift-format +ln -s AgentGuidelines/Configurations/Swift/.editorconfig .editorconfig ``` Keep the subtree tracked, but add this to the consumer's tracked `.gitattributes` so GitHub collapses synchronized guideline files in pull-request diffs by default: @@ -98,6 +107,24 @@ AgentGuidelines/** linguist-generated Copy and adapt [the consumer template](Templates/AGENTS.md). Keep the consumer file small: describe the product or package, map its concrete physical folders, point to the applicable shared guides, and state only genuine exceptions. +### Configure global Codex instructions + +Copy the contents of [`Templates/GlobalCodexInstructions.md`](Templates/GlobalCodexInstructions.md) into the user's global Codex instructions. + +These instructions only bootstrap discovery of repository-local `AGENTS.md` files and shared guides. Repository engineering policy remains versioned in this repository rather than duplicated in each user's global configuration. + +Review this template when upgrading `agent-guidelines`, because the recommended global bootstrap instructions may change between releases. Installing or updating the Git subtree does not update a user's global Codex configuration. + +Redux applications also copy [the canonical Store](Templates/Store.swift) as is, following the composition and placement rules in [Redux architecture](Guidelines/Architecture/Redux.md). + +Expose the completion-audit skill at the consumer repository root so Codex can discover it: + +```sh +mkdir -p .agents/skills +ln -s ../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ + .agents/skills/agent-guidelines-audit +``` + ## Update a consumer Review the target release's changelog, then pull it deliberately: @@ -106,8 +133,8 @@ Review the target release's changelog, then pull it deliberately: git subtree pull \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.9 \ - --squash + 0.0.16 \ + --squash ``` Confirm `AgentGuidelines/VERSION`, ensure the `.gitattributes` rule above is present, review the subtree diff, validate local `AGENTS.md` pointers, and run the consumer's relevant tests. Keep the subtree update in its own commit, and identify the old and new versions plus the central release or pull request in the consumer pull-request description. Updates are intentionally not automatic: one guideline release cannot silently change every project. diff --git a/AgentGuidelines/Scripts/swift_format.sh b/AgentGuidelines/Scripts/swift_format.sh new file mode 100755 index 0000000..7ed65b1 --- /dev/null +++ b/AgentGuidelines/Scripts/swift_format.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "Usage: $0 ..." >&2 +} + +if [[ $# -lt 2 ]]; then + usage + exit 64 +fi + +mode="$1" +shift + +script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +configuration="$script_directory/../Configurations/Swift/.swift-format" + +if command -v xcrun >/dev/null 2>&1 && xcrun --find swift-format >/dev/null 2>&1; then + formatter=(xcrun swift-format) +elif command -v swift-format >/dev/null 2>&1; then + formatter=(swift-format) +elif command -v swift >/dev/null 2>&1; then + formatter=(swift format) +else + echo "error: swift-format is unavailable; install or select a Swift 6 toolchain." >&2 + exit 127 +fi + +common_arguments=( + --configuration "$configuration" + --recursive + --parallel +) + +format_sources() { + "${formatter[@]}" format --in-place "${common_arguments[@]}" "$@" +} + +lint_sources() { + "${formatter[@]}" lint "${common_arguments[@]}" "$@" +} + +case "$mode" in + format) + format_sources "$@" + ;; + format-and-lint) + format_sources "$@" + lint_sources "$@" + ;; + lint) + lint_sources "$@" + ;; + lint-strict) + "${formatter[@]}" lint --strict "${common_arguments[@]}" "$@" + ;; + *) + usage + exit 64 + ;; +esac diff --git a/AgentGuidelines/Scripts/validate_guidelines.py b/AgentGuidelines/Scripts/validate_guidelines.py index b816c50..23c935d 100644 --- a/AgentGuidelines/Scripts/validate_guidelines.py +++ b/AgentGuidelines/Scripts/validate_guidelines.py @@ -3,6 +3,8 @@ from __future__ import annotations +import json +import os import re import sys from pathlib import Path, PurePosixPath @@ -12,6 +14,57 @@ README = ROOT / "README.md" VERSION = ROOT / "VERSION" CHANGELOG = ROOT / "CHANGELOG.md" +SWIFT_FORMAT_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".swift-format" +EDITOR_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".editorconfig" +SWIFT_FORMAT_SCRIPT = ROOT / "Scripts" / "swift_format.sh" +AUDIT_SKILL = ROOT / ".agents" / "skills" / "agent-guidelines-audit" / "SKILL.md" +DEVELOPMENT_GUIDELINE = ROOT / "Guidelines" / "Development.md" +AGENTS_TEMPLATE = ROOT / "Templates" / "AGENTS.md" +EXPECTED_SWIFT_FORMAT_RULES = { + "AllPublicDeclarationsHaveDocumentation": False, + "AlwaysUseLiteralForEmptyCollectionInit": True, + "AlwaysUseLowerCamelCase": True, + "AmbiguousTrailingClosureOverload": True, + "AvoidRetroactiveConformances": True, + "BeginDocumentationCommentWithOneLineSummary": False, + "DoNotUseSemicolons": True, + "DontRepeatTypeInStaticProperties": True, + "FileScopedDeclarationPrivacy": True, + "FullyIndirectEnum": True, + "GroupNumericLiterals": True, + "IdentifiersMustBeASCII": True, + "NeverForceUnwrap": False, + "NeverUseForceTry": True, + "NeverUseImplicitlyUnwrappedOptionals": False, + "NoAccessLevelOnExtensionDeclaration": True, + "NoAssignmentInExpressions": True, + "NoBlockComments": True, + "NoCasesWithOnlyFallthrough": True, + "NoEmptyLinesOpeningClosingBraces": True, + "NoEmptyTrailingClosureParentheses": True, + "NoLabelsInCasePatterns": True, + "NoLeadingUnderscores": False, + "NoParensAroundConditions": True, + "NoPlaygroundLiterals": True, + "NoVoidReturnOnFunctionSignature": True, + "OmitExplicitReturns": False, + "OneCasePerLine": True, + "OneVariableDeclarationPerLine": True, + "OnlyOneTrailingClosureArgument": True, + "OrderedImports": True, + "ReplaceForEachWithForLoop": True, + "ReturnVoidInsteadOfEmptyTuple": True, + "TypeNamesShouldBeCapitalized": True, + "UseEarlyExits": False, + "UseExplicitNilCheckInConditions": True, + "UseLetInEveryBoundCaseVariable": True, + "UseShorthandTypeNames": True, + "UseSingleLinePropertyGetter": True, + "UseSynthesizedInitializer": True, + "UseTripleSlashForDocumentationComments": True, + "UseWhereClausesInForLoops": True, + "ValidateDocumentationComments": True, +} MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") SEMVER = re.compile( @@ -31,9 +84,14 @@ def text_files() -> list[Path]: - suffixes = {".md", ".py", ".yml", ".yaml", ".txt"} + suffixes = {".md", ".py", ".swift", ".yml", ".yaml", ".txt"} files = [path for path in ROOT.rglob("*") if path.is_file() and path.suffix in suffixes] files.extend(path for path in (ROOT / "VERSION", ROOT / "LICENSE") if path.is_file()) + files.extend( + path + for path in (SWIFT_FORMAT_CONFIGURATION, EDITOR_CONFIGURATION) + if path.is_file() + ) return sorted(set(files)) @@ -87,6 +145,9 @@ def validate_readme_contract(errors: list[str]) -> None: "git subtree add": "subtree installation command", "git subtree pull": "subtree update command", "AgentGuidelines/** linguist-generated": "generated subtree attribute", + "AgentGuidelines/Configurations/Swift/.swift-format": "swift-format symlink command", + "AgentGuidelines/Configurations/Swift/.editorconfig": "EditorConfig symlink command", + ".agents/skills/agent-guidelines-audit": "completion-audit skill setup", } for value, description in required.items(): if value not in readme: @@ -102,6 +163,128 @@ def validate_public_content(errors: list[str]) -> None: errors.append(f"{relative}: contains {description}: {forbidden!r}") +def validate_swift_format_configuration(errors: list[str]) -> None: + try: + configuration = json.loads(SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + errors.append(f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: invalid JSON: {error}") + return + + expected_values = { + "indentation": {"spaces": 4}, + "indentSwitchCaseLabels": False, + "lineLength": 120, + "tabWidth": 4, + "version": 1, + } + for key, expected in expected_values.items(): + actual = configuration.get(key) + if actual != expected: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + f"{key} must be {expected!r}, found {actual!r}" + ) + + include_conditional_imports = configuration.get("orderedImports", {}).get( + "includeConditionalImports" + ) + if include_conditional_imports is not True: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + "orderedImports.includeConditionalImports must be True, " + f"found {include_conditional_imports!r}" + ) + + rules = configuration.get("rules") + if not isinstance(rules, dict) or not rules: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + "rules must be an exhaustive non-empty object" + ) + else: + missing = sorted(set(EXPECTED_SWIFT_FORMAT_RULES) - set(rules)) + unexpected = sorted(set(rules) - set(EXPECTED_SWIFT_FORMAT_RULES)) + if missing or unexpected: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + f"rule map mismatch; missing={missing!r}, unexpected={unexpected!r}" + ) + for rule in sorted(set(rules) & set(EXPECTED_SWIFT_FORMAT_RULES)): + expected = EXPECTED_SWIFT_FORMAT_RULES[rule] + actual = rules[rule] + if actual != expected: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + f"{rule} must be {expected!r}, found {actual!r}" + ) + + +def validate_editor_configuration(errors: list[str]) -> None: + try: + contents = EDITOR_CONFIGURATION.read_text(encoding="utf-8") + except OSError as error: + errors.append( + f"{EDITOR_CONFIGURATION.relative_to(ROOT)}: cannot read configuration: {error}" + ) + return + required = { + "root = true", + "[*.swift]", + "indent_style = space", + "indent_size = 4", + "tab_width = 4", + "max_line_length = 120", + "end_of_line = lf", + "insert_final_newline = true", + "trim_trailing_whitespace = true", + } + for value in sorted(required): + if value not in contents: + errors.append( + f"{EDITOR_CONFIGURATION.relative_to(ROOT)}: missing {value!r}" + ) + + +def validate_swift_format_script(errors: list[str]) -> None: + if not SWIFT_FORMAT_SCRIPT.is_file(): + errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: missing script") + elif not os.access(SWIFT_FORMAT_SCRIPT, os.X_OK): + errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: script is not executable") + + +def validate_audit_skill(errors: list[str]) -> None: + if not AUDIT_SKILL.is_file(): + errors.append(f"{AUDIT_SKILL.relative_to(ROOT)}: missing audit skill") + return + + skill = AUDIT_SKILL.read_text(encoding="utf-8") + required_skill_values = { + "name: agent-guidelines-audit": "skill name", + "before claiming completion": "completion trigger", + "git diff --check": "diff validation", + } + for value, description in required_skill_values.items(): + if value not in skill: + errors.append( + f"{AUDIT_SKILL.relative_to(ROOT)}: missing {description}: {value!r}" + ) + + development = DEVELOPMENT_GUIDELINE.read_text(encoding="utf-8") + if "$agent-guidelines-audit" not in development: + errors.append( + f"{DEVELOPMENT_GUIDELINE.relative_to(ROOT)}: " + "missing mandatory $agent-guidelines-audit invocation" + ) + + agents_template = AGENTS_TEMPLATE.read_text(encoding="utf-8") + if "AgentGuidelines/Guidelines/Development.md" not in agents_template: + errors.append( + f"{AGENTS_TEMPLATE.relative_to(ROOT)}: missing Development.md pointer" + ) + if "## Stack" not in agents_template: + errors.append(f"{AGENTS_TEMPLATE.relative_to(ROOT)}: missing Stack section") + + def main() -> int: errors: list[str] = [] validate_links(errors) @@ -109,6 +292,10 @@ def main() -> int: validate_version(errors) validate_readme_contract(errors) validate_public_content(errors) + validate_swift_format_configuration(errors) + validate_editor_configuration(errors) + validate_swift_format_script(errors) + validate_audit_skill(errors) if errors: print("Guideline validation failed:") diff --git a/AgentGuidelines/Templates/AGENTS.md b/AgentGuidelines/Templates/AGENTS.md index 4bad2bd..845abf7 100644 --- a/AgentGuidelines/Templates/AGENTS.md +++ b/AgentGuidelines/Templates/AGENTS.md @@ -8,15 +8,17 @@ Describe the product or package, supported platforms, and durable constraints. L Read only the guides relevant to the task: +- [Agent workflow](AgentGuidelines/Guidelines/AgentWorkflow.md) - [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) - [SwiftUI](AgentGuidelines/Guidelines/Swift/SwiftUI.md) -- [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) - [Localization](AgentGuidelines/Guidelines/Swift/Localization.md) - [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) - [Documentation](AgentGuidelines/Guidelines/Documentation.md) - [Logging](AgentGuidelines/Guidelines/Logging.md) - [Packages](AgentGuidelines/Guidelines/Packages.md) +- [Development workflow](AgentGuidelines/Guidelines/Development.md) - [CI/CD](AgentGuidelines/Guidelines/CICD.md) - [Git repositories and SSH-first cloning](AgentGuidelines/Guidelines/Git/Repositories.md) - [GitHub pull requests](AgentGuidelines/Guidelines/GitHub/PullRequests.md) @@ -47,6 +49,10 @@ Replace these examples with exact repository paths: | Services | `/Services/` | | Unit tests | `Tests/` | +## Stack + +Record the supported Xcode, Swift, and platform versions. State strict-concurrency mode, default actor isolation, infer-isolated-conformance behavior, and `nonisolated(nonsending)` defaults when they apply. Clarify whether application, package, and test targets share those settings. + ## Local specialization State only rules that specialize or override the shared baseline. Explain their scope and point to local source-of-truth documentation. diff --git a/AgentGuidelines/Templates/GlobalCodexInstructions.md b/AgentGuidelines/Templates/GlobalCodexInstructions.md new file mode 100644 index 0000000..4949810 --- /dev/null +++ b/AgentGuidelines/Templates/GlobalCodexInstructions.md @@ -0,0 +1,9 @@ +# Global Codex Instructions + +For repositories containing an `AGENTS.md`, read and follow the applicable repository instructions before starting substantive work. + +When a repository includes shared agent guidelines, read only the guides referenced by the applicable `AGENTS.md`. Treat those guides as the source of truth for language conventions, architecture, development workflow, testing, and agent execution. + +Repository and folder-level instructions may specialize the shared baseline within their scope. Do not replace deliberate repository conventions with generic global preferences. + +Do not duplicate repository guidance in global instructions. Global instructions should bootstrap discovery of the repository's own sources of truth. diff --git a/AgentGuidelines/Templates/Store.swift b/AgentGuidelines/Templates/Store.swift new file mode 100644 index 0000000..0120cf1 --- /dev/null +++ b/AgentGuidelines/Templates/Store.swift @@ -0,0 +1,112 @@ +import Foundation +import Observation + +typealias AppStore = Store +typealias StateType = Equatable & Sendable & Codable +typealias ActionType = Equatable & Sendable +typealias Reducer = (State, Action) -> State +typealias Middleware = (State, Action) async -> Action? + +/// A class representing the state management store for the app. +/// +/// The `Store` class is responsible for managing the state of the application and handling actions +/// through a reducer and optional middlewares. It's an `@Observable`, which allows SwiftUI views +/// to observe state changes. This template requires every application and test target that compiles +/// or exercises it to set `Default Actor Isolation` to `MainActor` and +/// `nonisolated(nonsending) By Default` to `Yes`. These settings keep middleware on the main actor +/// without redundant isolation annotations. +/// +/// - Parameters: +/// - State: The type representing the state of the application. +/// Must conform to `Equatable & Sendable & Codable`. +/// - Action: The type representing actions that can be dispatched to the store. +/// Must conform to `Equatable & Sendable`. +/// +/// Example usage: +/// ``` +/// let store = AppStore(initialState: AppState(), reducer: appReducer) +/// await store.dispatch(.someAction) +/// ``` +@Observable final class Store { + private(set) var state: State + + @ObservationIgnored + private let middlewares: [Middleware] + + @ObservationIgnored + private let reducer: Reducer + + init( + initialState: State, + middlewares: [Middleware] = [], + reducer: @escaping Reducer + ) { + self.state = initialState + self.middlewares = middlewares + self.reducer = reducer + } +} + +// MARK: - Dispatcher + +extension Store { + /// Dispatches an action, awaiting the entire middleware chain before returning. + /// + /// The reducer runs first, then every middleware executes sequentially against the same + /// post-reducer state snapshot; any follow-up actions they return are dispatched + /// recursively (depth-first) and awaited too. This guarantees: + /// - Middleware executes sequentially and completes before returning. + /// - Nested actions dispatched by middleware are also awaited. + /// - State updates are fully processed before subsequent operations. + /// - Network requests don't overlap or time out due to race conditions. + /// + /// Awaiting also keeps state mutation off the synchronous SwiftUI update/layout pass, + /// avoiding the re-entrant `@Observable` mutation that crashes on iOS 26 (recursive + /// layout / `SIGTRAP`). + /// + /// For fire-and-forget dispatching from a synchronous context (e.g. a `Button` action, + /// `onAppear` / `onChange`, app startup), wrap the call in a `Task`: + /// ```swift + /// Task { await store.dispatch(action) } + /// ``` + /// When several actions must keep their relative order, dispatch them from a single `Task` + /// so they can't interleave: + /// ```swift + /// Task { + /// await store.dispatch(firstAction) + /// await store.dispatch(secondAction) + /// } + /// ``` + /// Conversely, **independent** actions are intentionally left as one `Task` per call so they + /// run concurrently — don't merge them into a single `Task` just to save lines, as that + /// serializes them (the second waits for the first's full middleware chain): + /// ```swift + /// // Independent: keep separate so neither blocks the other. + /// Task { await store.dispatch(firstAction) } + /// Task { await store.dispatch(secondAction) } + /// ``` + /// + /// - Parameter action: The action to dispatch. + func dispatch(_ action: Action) async { + state = reducer(state, action) + + // Capture the post-reducer state snapshot so all middlewares in this action's + // chain see the same state, even if nested actions mutate state during execution. + let currentState = state + + // Execute all middlewares against the same state snapshot and collect their next + // actions. This ensures every middleware for this action sees the same state (Redux pattern). + var nextActions: [Action] = [] + for middleware in middlewares { + if let nextAction = await middleware(currentState, action) { + nextActions.append(nextAction) + } + } + + // Then dispatch the collected next actions sequentially, maintaining depth-first + // execution while preserving state-snapshot consistency. + for nextAction in nextActions { + await dispatch(nextAction) + } + } +} diff --git a/AgentGuidelines/Tests/test_validate_guidelines.py b/AgentGuidelines/Tests/test_validate_guidelines.py index c17e130..d250633 100644 --- a/AgentGuidelines/Tests/test_validate_guidelines.py +++ b/AgentGuidelines/Tests/test_validate_guidelines.py @@ -3,8 +3,11 @@ from __future__ import annotations import importlib.util +import json +import tempfile import unittest from pathlib import Path +from unittest import mock VALIDATOR_PATH = Path(__file__).resolve().parents[1] / "Scripts" / "validate_guidelines.py" @@ -48,5 +51,78 @@ def test_invalid_versions(self) -> None: self.assertIsNone(VALIDATOR.SEMVER.fullmatch(version)) +class SwiftFormattingConfigurationTests(unittest.TestCase): + """Verifies the shared Swift formatting contract.""" + + def test_swift_format_configuration(self) -> None: + """Accepts the exhaustive Xcode-aligned swift-format configuration.""" + errors: list[str] = [] + + VALIDATOR.validate_swift_format_configuration(errors) + + self.assertEqual(errors, []) + + def test_swift_format_configuration_rejects_undocumented_rule_change(self) -> None: + """Rejects a changed rule value even when the exhaustive key set is unchanged.""" + configuration = json.loads( + VALIDATOR.SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8") + ) + configuration["rules"]["NeverForceUnwrap"] = True + + with tempfile.TemporaryDirectory(dir=VALIDATOR.ROOT) as directory: + path = Path(directory) / ".swift-format" + path.write_text(json.dumps(configuration), encoding="utf-8") + errors: list[str] = [] + + with mock.patch.object(VALIDATOR, "SWIFT_FORMAT_CONFIGURATION", path): + VALIDATOR.validate_swift_format_configuration(errors) + + self.assertTrue( + any("NeverForceUnwrap must be False, found True" in error for error in errors) + ) + + def test_swift_format_configuration_requires_conditional_import_sorting(self) -> None: + """Rejects disabling conditional import sorting.""" + configuration = json.loads( + VALIDATOR.SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8") + ) + configuration["orderedImports"]["includeConditionalImports"] = False + + with tempfile.TemporaryDirectory(dir=VALIDATOR.ROOT) as directory: + path = Path(directory) / ".swift-format" + path.write_text(json.dumps(configuration), encoding="utf-8") + errors: list[str] = [] + + with mock.patch.object(VALIDATOR, "SWIFT_FORMAT_CONFIGURATION", path): + VALIDATOR.validate_swift_format_configuration(errors) + + self.assertTrue( + any( + "orderedImports.includeConditionalImports must be True" in error + for error in errors + ) + ) + + def test_editor_configuration(self) -> None: + """Accepts the shared Swift EditorConfig values.""" + errors: list[str] = [] + + VALIDATOR.validate_editor_configuration(errors) + + self.assertEqual(errors, []) + + +class AgentGuidelinesAuditSkillTests(unittest.TestCase): + """Verifies the mandatory completion-audit skill contract.""" + + def test_audit_skill_contract(self) -> None: + """Accepts the skill, Development rule, and consumer template.""" + errors: list[str] = [] + + VALIDATOR.validate_audit_skill(errors) + + self.assertEqual(errors, []) + + if __name__ == "__main__": unittest.main() diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION index c5d54ec..e3b86dd 100644 --- a/AgentGuidelines/VERSION +++ b/AgentGuidelines/VERSION @@ -1 +1 @@ -0.0.9 +0.0.16 diff --git a/README.md b/README.md index 10f4e18..b062f81 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ In your `Package.swift`, add `ProgressionKit` as a dependency: dependencies: [ .package( url: "https://github.com/thatfactory/progressionkit", - from: "0.1.4" + from: "0.1.5" ) ] ```