From d69309be8f1c5aec530c8b8bc9df5e108040e4a9 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Tue, 18 Aug 2026 23:23:57 +0200 Subject: [PATCH 1/4] Squashed 'AgentGuidelines/' changes from 1e6127b..94421d6 94421d6 Fix package README badge order (#23) 421ee10 Prepare 0.0.17 release (#22) 5727b48 Improve Codex review convergence and consumer audits (#21) git-subtree-dir: AgentGuidelines git-subtree-split: 94421d65ecfeb0aa669e74c6f5ad9a3a62ecbf87 --- .../skills/agent-guidelines-audit/SKILL.md | 15 +- .../agent-guidelines-audit/agents/openai.yaml | 4 + .gitattributes | 2 - .github/workflows/ci.yml | 32 +- .github/workflows/nightly.yml | 31 -- .github/workflows/release.yml | 125 ++----- .gitignore | 12 +- AGENTS.md | 97 +++-- .../agent-guidelines-audit/agents/openai.yaml | 4 - AgentGuidelines/.github/workflows/ci.yml | 23 -- AgentGuidelines/.github/workflows/release.yml | 44 --- AgentGuidelines/.gitignore | 3 - AgentGuidelines/AGENTS.md | 59 --- .../Guidelines/GitHub/PullRequests.md | 124 ------- AgentGuidelines/LICENSE | 22 -- AgentGuidelines/README.md | 166 --------- AgentGuidelines/Templates/AGENTS.md | 58 --- .../Templates/GlobalCodexInstructions.md | 9 - AgentGuidelines/VERSION | 1 - AgentGuidelines/CHANGELOG.md => CHANGELOG.md | 19 + .../Swift/.editorconfig | 0 .../Swift/.swift-format | 0 .../AgentWorkflow.md | 8 + .../Architecture/Redux.md | 0 .../Guidelines => Guidelines}/CICD.md | 0 .../Guidelines => Guidelines}/Development.md | 2 + .../Documentation.md | 0 .../Git/Repositories.md | 0 Guidelines/GitHub/PullRequests.md | 178 +++++++++ .../Guidelines => Guidelines}/Logging.md | 0 .../Guidelines => Guidelines}/Packages.md | 11 +- .../Swift/Localization.md | 0 .../Guidelines => Guidelines}/Swift/Swift.md | 0 .../Swift/SwiftFormat.md | 52 +++ .../Swift/SwiftStyle.md | 0 .../Swift/SwiftUI.md | 0 .../Testing/UnitTesting.md | 0 .../Guidelines => Guidelines}/Xcode/MCP.md | 0 .../Xcode/Security.md | 0 LICENSE | 1 + Package.resolved | 33 -- Package.swift | 44 --- README.md | 348 +++++++----------- .../Scripts => Scripts}/swift_format.sh | 0 Scripts/validate_consumer_setup.py | 304 +++++++++++++++ .../validate_guidelines.py | 37 ++ Sources/ProgressionKit/PKConfig.swift | 47 --- Sources/ProgressionKit/PKEngine.swift | 122 ------ Sources/ProgressionKit/PKEvent.swift | 35 -- Sources/ProgressionKit/PKLogging.swift | 42 --- Sources/ProgressionKit/PKProfile.swift | 23 -- Sources/ProgressionKit/PKTierProgress.swift | 14 - Sources/ProgressionKit/PKTrackProgress.swift | 23 -- Sources/ProgressionKit/PKUpdate.swift | 47 --- .../ProgressionKit.docc/ProgressionKit.md | 69 ---- Sources/ProgressionKit/ProgressionKit.swift | 1 - Templates/AGENTS.md | 74 ++++ Templates/GlobalCodexInstructions.md | 30 ++ .../Templates => Templates}/Store.swift | 0 .../ProgressionKitTests/PKLoggingTests.swift | 100 ----- .../ProgressionKitTests.swift | 196 ---------- Tests/test_validate_consumer_setup.py | 253 +++++++++++++ .../test_validate_guidelines.py | 16 + VERSION | 1 + 64 files changed, 1242 insertions(+), 1719 deletions(-) rename {AgentGuidelines/.agents => .agents}/skills/agent-guidelines-audit/SKILL.md (61%) create mode 100644 .agents/skills/agent-guidelines-audit/agents/openai.yaml delete mode 100644 .gitattributes delete mode 100644 .github/workflows/nightly.yml delete mode 100644 AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml delete mode 100644 AgentGuidelines/.github/workflows/ci.yml delete mode 100644 AgentGuidelines/.github/workflows/release.yml delete mode 100644 AgentGuidelines/.gitignore delete mode 100644 AgentGuidelines/AGENTS.md delete mode 100644 AgentGuidelines/Guidelines/GitHub/PullRequests.md delete mode 100644 AgentGuidelines/LICENSE delete mode 100644 AgentGuidelines/README.md delete mode 100644 AgentGuidelines/Templates/AGENTS.md delete mode 100644 AgentGuidelines/Templates/GlobalCodexInstructions.md delete mode 100644 AgentGuidelines/VERSION rename AgentGuidelines/CHANGELOG.md => CHANGELOG.md (85%) rename {AgentGuidelines/Configurations => Configurations}/Swift/.editorconfig (100%) rename {AgentGuidelines/Configurations => Configurations}/Swift/.swift-format (100%) rename {AgentGuidelines/Guidelines => Guidelines}/AgentWorkflow.md (78%) rename {AgentGuidelines/Guidelines => Guidelines}/Architecture/Redux.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/CICD.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Development.md (84%) rename {AgentGuidelines/Guidelines => Guidelines}/Documentation.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Git/Repositories.md (100%) create mode 100644 Guidelines/GitHub/PullRequests.md rename {AgentGuidelines/Guidelines => Guidelines}/Logging.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Packages.md (97%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/Localization.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/Swift.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftFormat.md (64%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftStyle.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftUI.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Testing/UnitTesting.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Xcode/MCP.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Xcode/Security.md (100%) delete mode 100644 Package.resolved delete mode 100644 Package.swift rename {AgentGuidelines/Scripts => Scripts}/swift_format.sh (100%) create mode 100755 Scripts/validate_consumer_setup.py rename {AgentGuidelines/Scripts => Scripts}/validate_guidelines.py (87%) delete mode 100644 Sources/ProgressionKit/PKConfig.swift delete mode 100644 Sources/ProgressionKit/PKEngine.swift delete mode 100644 Sources/ProgressionKit/PKEvent.swift delete mode 100644 Sources/ProgressionKit/PKLogging.swift delete mode 100644 Sources/ProgressionKit/PKProfile.swift delete mode 100644 Sources/ProgressionKit/PKTierProgress.swift delete mode 100644 Sources/ProgressionKit/PKTrackProgress.swift delete mode 100644 Sources/ProgressionKit/PKUpdate.swift delete mode 100644 Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md delete mode 100644 Sources/ProgressionKit/ProgressionKit.swift create mode 100644 Templates/AGENTS.md create mode 100644 Templates/GlobalCodexInstructions.md rename {AgentGuidelines/Templates => Templates}/Store.swift (100%) delete mode 100644 Tests/ProgressionKitTests/PKLoggingTests.swift delete mode 100644 Tests/ProgressionKitTests/ProgressionKitTests.swift create mode 100644 Tests/test_validate_consumer_setup.py rename {AgentGuidelines/Tests => Tests}/test_validate_guidelines.py (89%) create mode 100644 VERSION diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md b/.agents/skills/agent-guidelines-audit/SKILL.md similarity index 61% rename from AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md rename to .agents/skills/agent-guidelines-audit/SKILL.md index 1f33702..2110942 100644 --- a/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md +++ b/.agents/skills/agent-guidelines-audit/SKILL.md @@ -1,6 +1,6 @@ --- 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. +description: Audit completed repository work and checked-in consumer integration against 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 @@ -14,6 +14,9 @@ Perform a final, evidence-based compliance pass. Treat the applicable guidelines 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. +6. When the repository contains an `AgentGuidelines/` subtree, run `python3 AgentGuidelines/Scripts/validate_consumer_setup.py` from the consumer root. The validator detects Swift-format adoption from the root `AGENTS.md`; add `--require-swift-format` only when the repository must adopt it before that link is present. Treat failures as integration drift to fix or report before handoff. + +Do not inspect or require the user's global Codex instructions. They are user-level state outside the repository audit boundary; validate the checked-in root `AGENTS.md` contract instead. ## Audit the implementation @@ -28,6 +31,7 @@ Review the actual change rather than only checking whether files exist: - 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. +- For pull-request or merge readiness, apply the root `## Code Review Rules`: confirm the review covers the current head, no allowed review round is pending, every thread has a disposition, and no unresolved P0/P1 blocker remains. Treat P2/P3 observations as non-blocking and never request another Codex review unless the repository owner explicitly authorizes it. ## Validate the evidence @@ -39,6 +43,14 @@ Run the repository's declared non-destructive checks in proportion to the change - repository-specific validators; - `git diff --check`. +When the shared Swift-format guide applies: + +- For implementation work, run `AgentGuidelines/Scripts/swift_format.sh format-and-lint` over every changed or applicable checked-in Swift source root before tests. For review-only work, use `lint-strict` so the audit does not mutate files. +- Confirm the root `.swift-format` and `.editorconfig` symlinks resolve to the synchronized shared configurations. +- Confirm pull-request and protected-branch CI run the shared wrapper with `lint-strict` in a dedicated non-mutating job. Reject `format` or `format-and-lint` in CI and verify the listed paths cover the repository's checked-in Swift roots. +- For Xcode projects, verify every independently buildable app or test target has the target-scoped pre-compilation phase described by the guide, including its `CI=true` bypass. +- For Swift packages, format `Package.swift`, `Sources`, `Tests`, and other checked-in Swift roots that exist before running `swift test`. Do not require `swift build` or `swift test` themselves to rewrite source; formatting and testing are consecutive, independently visible checks. + 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 @@ -53,6 +65,7 @@ Use fresh successful evidence already produced in the same task instead of rerun Summarize: - the instruction and guideline areas audited; +- consumer-integration validation and any drift found; - findings fixed during the audit; - validation commands and outcomes; - any deliberate deviations, unavailable evidence, or remaining blockers. diff --git a/.agents/skills/agent-guidelines-audit/agents/openai.yaml b/.agents/skills/agent-guidelines-audit/agents/openai.yaml new file mode 100644 index 0000000..f7ecc03 --- /dev/null +++ b/.agents/skills/agent-guidelines-audit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Agent Guidelines Audit" + short_description: "Audit work and shared-guideline integration" + default_prompt: "Use $agent-guidelines-audit to audit this completed change and its consumer guideline integration before handoff." diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 38ec4db..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. -AgentGuidelines/** linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd6d684..5831117 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,23 @@ ---- name: CI on: + pull_request: push: branches: - main - pull_request: -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true +permissions: + contents: read jobs: - test: - name: Test - runs-on: [self-hosted, macOS] + validate: + name: Validate guidelines + runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true + - name: Checkout + uses: actions/checkout@v7 - - name: Clear SwiftPM Caches + - name: Validate run: | - rm -rf ~/.swiftpm - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - rm -rf .swiftpm - rm -rf .build - - - name: Run Tests - run: swift test -v + python3 -m unittest discover -s Tests + python3 Scripts/validate_guidelines.py diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index 5b67da9..0000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Nightly Tests - -on: - schedule: - - cron: '0 4 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - nightly_tests: - name: Nightly Tests - runs-on: [self-hosted, macOS] - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true - - - name: Clear SwiftPM Caches - run: | - rm -rf ~/.swiftpm - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - rm -rf .swiftpm - rm -rf .build - - - name: Run Tests - run: swift test -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c84201..20b4c8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,109 +1,44 @@ ---- name: Release -run-name: Release ${{ github.event.release.tag_name }} on: - release: - types: - - published + push: + tags: + - "*.*.*" permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + contents: write jobs: - test: - name: Test Release - runs-on: [self-hosted, macOS] + release: + name: Create GitHub release + runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true + - name: Checkout + uses: actions/checkout@v7 - - name: Clear SwiftPM Caches - run: | - rm -rf ~/.swiftpm - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - rm -rf .swiftpm - rm -rf .build + - name: Validate guidelines + run: python3 Scripts/validate_guidelines.py - - name: Run Tests - run: swift test -v - - build_docs: - name: Build DocC - runs-on: [self-hosted, macOS] - needs: test - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true - - - name: Generate DocC + - name: Validate tag run: | - set -euo pipefail - swift package --allow-writing-to-directory ./public generate-documentation \ - --target ProgressionKit \ - --disable-indexing \ - --output-path ./public \ - --transform-for-static-hosting \ - --hosting-base-path progressionkit - - cat > ./public/index.html <<'INDEX' - - - ProgressionKit Documentation - INDEX + version="$(tr -d '[:space:]' < VERSION)" + test "$GITHUB_REF_NAME" = "$version" - - name: Upload Pages Artifact - uses: actions/upload-pages-artifact@v5 - with: - path: ./public - name: github-pages - - deploy_docs: - name: Deploy DocC - needs: build_docs - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 - - notify_package_collection: - name: Notify Package Collection - runs-on: ubuntu-latest - needs: deploy_docs - steps: - - name: Trigger Swift Package Collection Rebuild + - name: Prepare release notes + run: | + version="$(tr -d '[:space:]' < VERSION)" + awk -v version="$version" ' + index($0, "## [" version "]") == 1 { capture = 1; next } + capture && /^## \[/ { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + test -s release-notes.md + + - name: Create release env: - COLLECTION_REPO: thatfactory/swift-package-collection - WORKFLOW_FILE: publish.yml - REF: main - GH_TOKEN: ${{ secrets.COLLECTION_TRIGGER_TOKEN }} - SOURCE_REPO: ${{ github.repository }} - SOURCE_VERSION: ${{ github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} run: | - set -euo pipefail - - if [ -z "${GH_TOKEN:-}" ]; then - echo "Missing COLLECTION_TRIGGER_TOKEN secret" - exit 1 - fi - - curl -sS -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GH_TOKEN" \ - "https://api.github.com/repos/$COLLECTION_REPO/actions/workflows/$WORKFLOW_FILE/dispatches" \ - -d "{\"ref\":\"$REF\",\"inputs\":{\"source_repo\":\"$SOURCE_REPO\",\"source_version\":\"$SOURCE_VERSION\"}}" + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --title "$GITHUB_REF_NAME" \ + --notes-file release-notes.md diff --git a/.gitignore b/.gitignore index 08c2ed7..dff2f41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,3 @@ .DS_Store -/.build -/Packages -/*.xcodeproj -*.xcworkspace -xcuserdata/ -Package.resolved -DerivedData/ -.swiftpm/configuration/registries.json -.netrc -/public-check/ +__pycache__/ +*.py[cod] diff --git a/AGENTS.md b/AGENTS.md index 19d9265..f82a61c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,46 +1,75 @@ -# ProgressionKit +# Agent Guidelines -## Context +## Purpose -ProgressionKit is a pure Swift package for deterministic XP, player levels, track mastery, and tier unlocks. Read [README.md](README.md) and the DocC catalog before changing public behavior. +This public repository is the versioned source of truth for reusable ThatFactory agent guidance. Keep it generic enough to apply to multiple applications and Swift packages. Product decisions, concrete project paths, and exceptions belong in each consumer repository. -The package is content-, storage-, UI-, and application-architecture agnostic. Host applications decide what content, tracks, tiers, persistence, and presentation mean. +## Sources of truth -## Shared guidelines +- Use official Apple documentation for Apple APIs and Xcode behavior. +- Distill durable policy from Xcode-provided skills; do not copy exported Apple skills into this repository. +- Do not include private company information, credentials, personal absolute paths, or consumer-specific implementation details. +- When shared and consumer guidance differ, the consumer's nearest applicable `AGENTS.md` is the explicit specialization. +- Before changing this repository, verify that the consumer's checked-in guidelines version is current where applicable. -Read only the guides relevant to the task: +## Documentation changes -- [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) -- [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.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) -- [Packages](AgentGuidelines/Guidelines/Packages.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) -- [Xcode MCP](AgentGuidelines/Guidelines/Xcode/MCP.md) -- [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) +- Keep each rule in the narrowest relevant guide and link to it rather than duplicating it. +- Use physical folder terminology for Xcode projects. Do not call filesystem folders Xcode groups. +- 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: -Redux, SwiftUI, and application-localization guidance do not apply to the package target. + ```sh + git subtree add \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + \ + --squash -## Physical folder map + git subtree pull \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + \ + --squash + ``` -| Role | Physical folder | -|---|---| -| Package sources | `Sources/ProgressionKit/` | -| DocC catalog | `Sources/ProgressionKit/ProgressionKit.docc/` | -| Unit tests | `Tests/ProgressionKitTests/` | + +## Code Review Rules -## Package specialization +Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. -- Keep progression updates deterministic for the same profile, event, and configuration. -- Do not add storage, network, UI, Redux, or game-content dependencies. -- Host applications own mapping from their domain identifiers and outcomes into `PKEvent`. -- Preserve compiler-synthesized value semantics and serialization when evolving public models. -- Update tests, DocC, README examples, and release notes when public behavior changes. -- Use logging subsystem `com.thatfactory.progressionkit`, category `progression`, and canonical package emoji `📈`. -## Codex review scope +A blocking finding must identify a concrete, reachable path in a supported use case or the documented threat model that can cause a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. -For consumer pull requests, do not substantively review `AgentGuidelines/**` after exact tagged-tree provenance has been verified. Verify its `VERSION`, compare its tree with the matching central tag, and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request. +For every blocking finding, state the severity, preconditions, execution path, impact, evidence, and actionable remediation. Group manifestations that share the same root cause into one finding. + +Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, behavior by components outside the threat model, style preferences, and speculative refactoring. Record a useful lower-severity observation once as deferred, declined, duplicate, or follow-up work; do not keep the review loop open for it. + +In an initial review, report substantiated blockers together. A follow-up review is limited to unresolved P0/P1 findings, changes since the last reviewed commit, and code directly affected by those changes. Do not restart an unrestricted review of unchanged code. A new follow-up finding must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +Automatic Codex review is the initial review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another review after each remediation commit. Within the normal review budget, at most one owner-authorized, delta-scoped verification review may be requested under [the pull-request review workflow](Guidelines/GitHub/PullRequests.md). + + +## Validation + +Run: + +```sh +python3 Scripts/validate_guidelines.py +``` + +Fix every validation failure before releasing a version. + +## Consumer pull-request review scope + +When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. That subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in this repository. Verify the intended `AgentGuidelines/VERSION`, compare the subtree tree with the matching central tag (for example with `git subtree split --prefix=AgentGuidelines HEAD` and a tree comparison after fetching that tag), and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request instead. + +## Releases + +- Use semantic versioning. +- Create a Git tag and GitHub release matching `VERSION`. +- Consumer repositories adopt releases deliberately through Git subtree updates. +- Follow [the pull-request review workflow](Guidelines/GitHub/PullRequests.md) before merging any release change. diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml b/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml deleted file mode 100644 index dc4aab7..0000000 --- a/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -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/.github/workflows/ci.yml b/AgentGuidelines/.github/workflows/ci.yml deleted file mode 100644 index 5831117..0000000 --- a/AgentGuidelines/.github/workflows/ci.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - validate: - name: Validate guidelines - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Validate - run: | - python3 -m unittest discover -s Tests - python3 Scripts/validate_guidelines.py diff --git a/AgentGuidelines/.github/workflows/release.yml b/AgentGuidelines/.github/workflows/release.yml deleted file mode 100644 index 20b4c8f..0000000 --- a/AgentGuidelines/.github/workflows/release.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Release - -on: - push: - tags: - - "*.*.*" - -permissions: - contents: write - -jobs: - release: - name: Create GitHub release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Validate guidelines - run: python3 Scripts/validate_guidelines.py - - - name: Validate tag - run: | - version="$(tr -d '[:space:]' < VERSION)" - test "$GITHUB_REF_NAME" = "$version" - - - name: Prepare release notes - run: | - version="$(tr -d '[:space:]' < VERSION)" - awk -v version="$version" ' - index($0, "## [" version "]") == 1 { capture = 1; next } - capture && /^## \[/ { exit } - capture { print } - ' CHANGELOG.md > release-notes.md - test -s release-notes.md - - - name: Create release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release create "$GITHUB_REF_NAME" \ - --verify-tag \ - --title "$GITHUB_REF_NAME" \ - --notes-file release-notes.md diff --git a/AgentGuidelines/.gitignore b/AgentGuidelines/.gitignore deleted file mode 100644 index dff2f41..0000000 --- a/AgentGuidelines/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -__pycache__/ -*.py[cod] diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md deleted file mode 100644 index c6eef34..0000000 --- a/AgentGuidelines/AGENTS.md +++ /dev/null @@ -1,59 +0,0 @@ -# Agent Guidelines - -## Purpose - -This public repository is the versioned source of truth for reusable ThatFactory agent guidance. Keep it generic enough to apply to multiple applications and Swift packages. Product decisions, concrete project paths, and exceptions belong in each consumer repository. - -## Sources of truth - -- Use official Apple documentation for Apple APIs and Xcode behavior. -- Distill durable policy from Xcode-provided skills; do not copy exported Apple skills into this repository. -- Do not include private company information, credentials, personal absolute paths, or consumer-specific implementation details. -- When shared and consumer guidance differ, the consumer's nearest applicable `AGENTS.md` is the explicit specialization. -- Before changing this repository, verify that the consumer's checked-in guidelines version is current where applicable. - -## Documentation changes - -- Keep each rule in the narrowest relevant guide and link to it rather than duplicating it. -- Use physical folder terminology for Xcode projects. Do not call filesystem folders Xcode groups. -- 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: - - ```sh - git subtree add \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - \ - --squash - - git subtree pull \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - \ - --squash - ``` - -## Validation - -Run: - -```sh -python3 Scripts/validate_guidelines.py -``` - -Fix every validation failure before releasing a version. - -## Consumer pull-request review scope - -When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. That subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in this repository. Verify the intended `AgentGuidelines/VERSION`, compare the subtree tree with the matching central tag (for example with `git subtree split --prefix=AgentGuidelines HEAD` and a tree comparison after fetching that tag), and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request instead. - -## Releases - -- Use semantic versioning. -- Create a Git tag and GitHub release matching `VERSION`. -- Consumer repositories adopt releases deliberately through Git subtree updates. -- Follow [the pull-request review workflow](Guidelines/GitHub/PullRequests.md) before merging any release change. diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/AgentGuidelines/Guidelines/GitHub/PullRequests.md deleted file mode 100644 index 3a2c349..0000000 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ /dev/null @@ -1,124 +0,0 @@ -# GitHub Pull Requests - -Use this guide whenever creating, reviewing, updating, or merging a GitHub pull request. - -## Before opening - -- Review the complete diff and exclude unrelated changes. -- 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 - -When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. The subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in the central `thatfactory/agent-guidelines` pull request. Verify `AgentGuidelines/VERSION`, compare the subtree tree with the matching central tag (for example with `git subtree split --prefix=AgentGuidelines HEAD` and a tree comparison after fetching that tag), and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central pull request instead. - -## Review gate - -Opening a pull request starts review; it does not authorize merging it. - -1. Wait for the configured Codex review to finish. No review yet means pending, not approved. -2. Inspect all review summaries, inline threads, checks, and requested changes. -3. Assess each comment on its technical merits. -4. Implement valid feedback and rerun the affected validation. -5. If feedback should not be implemented, reply in the original thread with a concise technical reason. -6. Reply to implemented feedback with what changed and where. -7. Resolve a thread only after its concern has been addressed or explicitly declined. -8. After addressing review comments, update the pull-request description so it matches the current implementation, validation, and any remaining limitations. -9. Recheck the pull request immediately before merge for late comments and check-state changes. - -When replying with a commit reference, write the commit hash as raw text without backticks (for example, the hash 185c04f should remain 185c04f). GitHub then auto-links the hash to the commit. - -A thumbs-up or clean Codex review satisfies the agent-review step, but it does not replace any human approval required by the repository. Do not enable auto-merge before all review gates are satisfied. - -### Codex review monitoring - -Use GitHub review data, reactions, and checks together. An eyes reaction means Codex is processing the pull request; it is not an approval. A thumbs-up means the review completed without suggestions. A submitted review means its inline threads must be assessed individually. - -```text -PR opened - | - v -Codex adds eyes reaction - | - +--> thumbs-up ----------------> Clean review - | - `--> Review comments ----------> Assess each comment - | - fix or decline with reason - | - reply in original thread - | - resolve thread -``` - -When using the GitHub CLI, monitor all three surfaces: - -```sh -gh api --paginate repos///issues//reactions -gh pr view --repo / --json reviews,headRefOid -gh pr checks --repo / -``` - -Retrieve inline review threads and their resolution state through GraphQL; top-level pull-request comments do not include this information: - -```sh -gh api graphql --paginate \ - -f query='query($owner: String!, $repository: String!, $number: Int!, $endCursor: String) { - repository(owner: $owner, name: $repository) { - pullRequest(number: $number) { - reviewThreads(first: 100, after: $endCursor) { - nodes { id isResolved } - pageInfo { hasNextPage endCursor } - } - } - } - }' \ - -F owner= \ - -F repository= \ - -F number= -``` - -For every unresolved thread identifier returned above, retrieve its complete comment history with a second paginated query: - -```sh -gh api graphql --paginate \ - -f query='query($thread: ID!, $endCursor: String) { - node(id: $thread) { - ... on PullRequestReviewThread { - comments(first: 100, after: $endCursor) { - nodes { id author { login } body url } - pageInfo { hasNextPage endCursor } - } - } - } - }' \ - -F thread= -``` - -Continue polling while actively working on the pull request. Inspect every returned page for reactions, review threads, and thread comments. Do not treat missing comments, a pending reaction, truncated results, or elapsed time as review completion. - -## Merge requirements - -Do not merge while any of the following is true: - -- Codex review is still pending; -- an actionable review comment is unanswered; -- a review conversation is unresolved; -- a required check is pending or failing; -- the branch is out of date when the repository requires an up-to-date branch; -- required human approval or explicit owner authorization is missing. - -If a review arrives after a premature merge, treat that as a process failure: assess the feedback, reply to every thread, and ship valid corrections through a follow-up pull request. - -## Repository protection - -Prefer GitHub rulesets or branch protection for the default branch. At minimum: - -- require changes to arrive through a pull request; -- require conversations to be resolved before merging; -- require the repository's mandatory status checks; -- prevent bypass except for an intentional emergency path. - -A formal one-approval rule works only when someone other than the pull-request author can submit an approving review. In a solo repository where the owner account also authors pull requests, use a bot or service account for authored changes before requiring owner approval; GitHub does not count self-approval. Until that separation exists, require explicit owner authorization operationally and keep conversation resolution enforced technically. diff --git a/AgentGuidelines/LICENSE b/AgentGuidelines/LICENSE deleted file mode 100644 index 42d8021..0000000 --- a/AgentGuidelines/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2026 ThatFactory - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/AgentGuidelines/README.md b/AgentGuidelines/README.md deleted file mode 100644 index e29c651..0000000 --- a/AgentGuidelines/README.md +++ /dev/null @@ -1,166 +0,0 @@ -

- Xcode - Codex - Updated - Revision - License - CI -

- -# Agent Guidelines - -`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 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 - -```text - thatfactory/agent-guidelines - versioned GitHub repository - | - tagged release - e.g. 0.0.3 - | - git subtree add/pull - | - v -+---------------- Consumer project or package -----------------+ -| | -| AGENTS.md | -| |-- local product/package context | -| |-- concrete project paths | -| |-- local exceptions | -| `-- pointers to shared guidelines -----------------+ | -| | | -| AgentGuidelines/ | | -| |-- VERSION | | -| |-- Configurations/ | | -| `-- Guidelines/ <----------------------------------+ | -| |-- Architecture/Redux.md | -| |-- Swift/SwiftUI.md | -| |-- Testing/UnitTesting.md | -| `-- Xcode/MCP.md | -| | -| Sources and project files | -+----------------------------+---------------------------------+ - | - reads instructions and project files - +----------+----------+ - v v - Codex Xcode agent - | - | Xcode MCP (`xcrun mcpbridge`) - v - Xcode -``` - -The subtree does not automatically import every guide into an agent's context. A consumer's root or folder-scoped `AGENTS.md` tells the agent which shared guides to read for the task. The nearest local `AGENTS.md` can specialize or override the shared baseline. - -## 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) -- [Unit and integration testing](Guidelines/Testing/UnitTesting.md) -- [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) -- [Xcode security audits](Guidelines/Xcode/Security.md) - -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 - -From the consumer repository root, install a tagged release: - -```sh -git subtree add \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - 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: - -```gitattributes -# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. -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: - -```sh -git subtree pull \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - 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. - -## Maintain the source of truth - -1. Export current Xcode skills to a temporary review location when a new Xcode release materially changes agent behavior: - - ```sh - xcrun agent skills export --output-dir - ``` - -2. Compare relevant guidance with this repository and official Apple documentation. -3. Bring over durable policy, not the exported skill text or an SDK API catalog. -4. Remove obsolete or conflicting rules instead of accumulating historical alternatives. -5. Run `python3 Scripts/validate_guidelines.py`. -6. Update `VERSION` and `CHANGELOG.md`, open a pull request, and wait for approval before merging. -7. After the pull request has merged, create the matching tag and GitHub release. - -## Precedence - -For a consumer task, apply instructions in this order: - -1. The user's explicit request. -2. The nearest applicable consumer `AGENTS.md`. -3. The consumer root `AGENTS.md`. -4. The shared guides explicitly referenced by those files. - -Official Apple documentation remains authoritative for API behavior. A local convention can deliberately narrow a choice, but it must not rely on behavior contradicted by the current SDK documentation. diff --git a/AgentGuidelines/Templates/AGENTS.md b/AgentGuidelines/Templates/AGENTS.md deleted file mode 100644 index 845abf7..0000000 --- a/AgentGuidelines/Templates/AGENTS.md +++ /dev/null @@ -1,58 +0,0 @@ -# Project Instructions - -## Context - -Describe the product or package, supported platforms, and durable constraints. Link to the project README or product documentation instead of duplicating it. - -## Shared guidelines - -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) -- [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) -- [Xcode MCP and visual verification](AgentGuidelines/Guidelines/Xcode/MCP.md) -- [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) - -For an application that uses Redux, also read [Redux architecture](AgentGuidelines/Guidelines/Architecture/Redux.md). - -Add the following section to the consumer repository's root `AGENTS.md` so it is loaded for root-level Codex and pull-request work: - -```md -## Codex review scope - -For consumer pull requests, do not substantively review `AgentGuidelines/**` after exact tagged-tree provenance has been verified. Verify its `VERSION`, compare its tree with the matching central tag, and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request. -``` - -This tracked, synchronized subtree is reviewed centrally in `thatfactory/agent-guidelines`; the root-level instruction ensures the review scope is loaded even when Codex starts from the repository root. - -## Physical folder map - -Replace these examples with exact repository paths: - -| Role | Physical folder | -|---|---| -| Application sources | `/` | -| Redux | `/Redux/` | -| Views | `/View/` | -| 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 deleted file mode 100644 index 4949810..0000000 --- a/AgentGuidelines/Templates/GlobalCodexInstructions.md +++ /dev/null @@ -1,9 +0,0 @@ -# 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/VERSION b/AgentGuidelines/VERSION deleted file mode 100644 index e3b86dd..0000000 --- a/AgentGuidelines/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.16 diff --git a/AgentGuidelines/CHANGELOG.md b/CHANGELOG.md similarity index 85% rename from AgentGuidelines/CHANGELOG.md rename to CHANGELOG.md index e622148..1600a67 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project are documented in this file. +## [0.0.18] - 2026-08-18 + +### Changed + +- Corrected the standard README badge order to place DocC/documentation before license, updated date, revision, CI badges, and release/publishing status. + +## [0.0.17] - 2026-08-18 + +### Added + +- A version-marked consumer Code Review contract that defines P0/P1 release blockers, non-blocking P2/P3 observations, and bounded follow-up review scope. +- A deterministic consumer-setup validator for review-contract drift, subtree review scope, `.gitattributes`, local guide links, audit-skill wiring, and Swift-format adoption. + +### Changed + +- Expanded the global Codex instruction template and pull-request workflow to prioritize concrete release risk, group shared root causes, and stop review loops after blockers are resolved. +- Extended the completion-audit skill to verify consumer integration, review convergence, Swift-format configuration, local execution, and non-mutating CI coverage. +- Documented explicit Swift package formatting before tests and added a strict Swift-format CI template with package path coverage. + ## [0.0.16] - 2026-08-13 ### Changed diff --git a/AgentGuidelines/Configurations/Swift/.editorconfig b/Configurations/Swift/.editorconfig similarity index 100% rename from AgentGuidelines/Configurations/Swift/.editorconfig rename to Configurations/Swift/.editorconfig diff --git a/AgentGuidelines/Configurations/Swift/.swift-format b/Configurations/Swift/.swift-format similarity index 100% rename from AgentGuidelines/Configurations/Swift/.swift-format rename to Configurations/Swift/.swift-format diff --git a/AgentGuidelines/Guidelines/AgentWorkflow.md b/Guidelines/AgentWorkflow.md similarity index 78% rename from AgentGuidelines/Guidelines/AgentWorkflow.md rename to Guidelines/AgentWorkflow.md index ebf6f13..8068dd6 100644 --- a/AgentGuidelines/Guidelines/AgentWorkflow.md +++ b/Guidelines/AgentWorkflow.md @@ -42,6 +42,14 @@ Bound the combined output of grouped operations so that every result can be insp Do not expand the investigation merely because additional operations can be executed concurrently. +## Bounded iteration + +Before starting an iterative review, remediation, or model-assisted refinement loop, define its objective, blocking threshold, round budget, and stop condition. New non-blocking observations do not reset the budget or widen the original objective. + +Do not translate feedback directly into both a change and another review request. Classify the feedback, group items with the same root cause, batch accepted corrections, and rerun only the validation or bounded review needed to verify them. + +Stop when the stated acceptance condition is satisfied. Zero possible comments, improvements, or edge cases is not a valid completion criterion. For pull-request review severity, state tracking, and round limits, follow [GitHub pull requests](GitHub/PullRequests.md). + ## Efficiency Avoid unnecessary repeated model and tool cycles when several independent operations are already known. diff --git a/AgentGuidelines/Guidelines/Architecture/Redux.md b/Guidelines/Architecture/Redux.md similarity index 100% rename from AgentGuidelines/Guidelines/Architecture/Redux.md rename to Guidelines/Architecture/Redux.md diff --git a/AgentGuidelines/Guidelines/CICD.md b/Guidelines/CICD.md similarity index 100% rename from AgentGuidelines/Guidelines/CICD.md rename to Guidelines/CICD.md diff --git a/AgentGuidelines/Guidelines/Development.md b/Guidelines/Development.md similarity index 84% rename from AgentGuidelines/Guidelines/Development.md rename to Guidelines/Development.md index 8f93d90..c733ca1 100644 --- a/AgentGuidelines/Guidelines/Development.md +++ b/Guidelines/Development.md @@ -25,6 +25,8 @@ Before claiming implementation is complete, handing work to the user, preparing, 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. +For subtree consumers, the audit runs `python3 AgentGuidelines/Scripts/validate_consumer_setup.py` to detect drift in the root Code Review contract, Codex subtree-review scope, `.gitattributes`, local guide links, and repository skill symlink. When the root `AGENTS.md` links the shared Swift-format guide, the validator also requires the shared configuration symlinks and strict non-mutating CI adoption. User-level global Codex instructions are outside this repository 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/Guidelines/Documentation.md similarity index 100% rename from AgentGuidelines/Guidelines/Documentation.md rename to Guidelines/Documentation.md diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/Guidelines/Git/Repositories.md similarity index 100% rename from AgentGuidelines/Guidelines/Git/Repositories.md rename to Guidelines/Git/Repositories.md diff --git a/Guidelines/GitHub/PullRequests.md b/Guidelines/GitHub/PullRequests.md new file mode 100644 index 0000000..80c12b1 --- /dev/null +++ b/Guidelines/GitHub/PullRequests.md @@ -0,0 +1,178 @@ +# GitHub Pull Requests + +Use this guide whenever creating, reviewing, updating, or merging a GitHub pull request. + +## Before opening + +- Review the complete diff and exclude unrelated changes. +- Keep each pull request to a coherent review unit with a bounded set of invariants. Split changes that combine independent architecture, persistence, security, transport, and CI concerns when they can be reviewed and delivered separately; do not split merely to minimize line count. +- State the supported use cases, explicit acceptance criteria, and relevant threat model for behavior whose review priority depends on those boundaries. +- For security guarantees based on enumerating formats or signatures, define the finite coverage contract and residual risk, or use a systemic boundary that enforces the guarantee without exhaustive enumeration. +- 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 + +When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. The subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in the central `thatfactory/agent-guidelines` pull request. Verify `AgentGuidelines/VERSION`, compare the subtree tree with the matching central tag (for example with `git subtree split --prefix=AgentGuidelines HEAD` and a tree comparison after fetching that tag), and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central pull request instead. + +## Review objective + +Automated review identifies release-blocking regressions; it does not attempt to eliminate every possible improvement. + +Classify findings by impact and reachable scope: + +- **P0 — critical:** an actively exploitable critical security issue, catastrophic durable data loss, or critical production outage. +- **P1 — blocking:** a supported use case, explicit acceptance criterion, or documented threat-model boundary has a concrete reachable failure path that causes a security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, or a serious compatibility regression. +- **P2 — non-blocking:** robustness, defense-in-depth, bounded edge cases, malformed state that trusted code cannot produce, unsupported scenarios, theoretical completeness, or useful hardening. +- **P3 — non-blocking:** style, naming, preferred refactoring, documentation polish, or optional test improvements. + +Only unresolved P0 and P1 findings block merge. A finding may be technically correct without being release-blocking. + +## Review gate + +Opening a pull request starts review; it does not authorize merging it. + +1. Wait for the configured Codex review to finish. No review yet means pending, not approved. +2. Record the reviewed head SHA and inspect all review summaries, inline threads, checks, and requested changes. +3. Assess each comment for technical correctness, severity, supported reachability, and root cause. +4. Give every thread one explicit disposition: `BLOCKER-P0`, `BLOCKER-P1`, `DEFER-P2`, `DEFER-P3`, `DECLINE`, or `DUPLICATE`. +5. Batch accepted P0/P1 corrections into one remediation pass and add regression coverage where reasonably possible. Lower-severity improvements may be included when they are small and clearly in scope, but they do not keep the review loop open. +6. Reply in the original thread with the disposition and either what changed or the concise technical reason for deferring, declining, or grouping it. +7. Resolve a thread only after its disposition is recorded. Reference a follow-up issue for deferred work when its value justifies one. +8. Rerun affected validation, then update the pull-request description so it matches the current implementation, validation, deferred work, and remaining limitations. +9. Recheck the pull request immediately before merge for late P0/P1 findings and check-state changes. + +When replying with a commit reference, write the commit hash as raw text without backticks (for example, the hash 185c04f should remain 185c04f). GitHub then auto-links the hash to the commit. + +A thumbs-up or clean Codex review satisfies the agent-review step, but it does not replace any human approval required by the repository. Do not enable auto-merge before all review gates are satisfied. + +### Review state and round budget + +Track enough state to prevent duplicate requests and unbounded review loops: + +```text +initial_review_sha +last_reviewed_sha +review_requested_sha +review_round +pending_review +unresolved_p0 +unresolved_p1 +deferred_findings +``` + +The automatic review is the one initial full review. Do not request another review after each fix. A repository owner may explicitly authorize at most one delta-scoped verification review after the known P0/P1 findings have been batch-remediated. + +Before sending that request, verify that no Codex review is pending, no existing request targets the current head SHA, the current head differs from `last_reviewed_sha`, and the verification-round budget is unused. Persist `review_requested_sha`, increment `review_round`, and mark `pending_review` before waiting for a result so a retry cannot submit a duplicate request. + +When authorized, scope the verification request explicitly: + +```text +@codex review only unresolved P0/P1 findings and changes since . +Do not search unchanged code for new P2/P3 issues. +``` + +Do not request a third review or restart a full review without separate, explicit repository-owner authorization and a named unresolved P0/P1 concern. A new finding in verification must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +Stop the review loop when no unresolved P0/P1 finding remains, every thread has an explicit disposition, required checks pass, and required human authorization is present. Zero comments, zero possible improvements, and zero technical debt are not completion criteria. + +### Codex review monitoring + +Use GitHub review data, reactions, and checks together. An eyes reaction means Codex is processing the pull request; it is not an approval. A thumbs-up means the review completed without suggestions. A submitted review means its inline threads must be assessed individually. + +```text +PR opened at stable head + | + v +One automatic full review + | + +--> thumbs-up ----------------> No P0/P1 blockers + | + `--> Review comments ----------> Classify and group + | + batch P0/P1 fixes + | + owner-authorized delta review? + | | + no yes + | | + stop one verification pass + | + no unresolved P0/P1 + | + stop +``` + +When using the GitHub CLI, monitor all three surfaces: + +```sh +gh api --paginate repos///issues//reactions +gh pr view --repo / --json reviews,headRefOid +gh pr checks --repo / +``` + +Retrieve inline review threads and their resolution state through GraphQL; top-level pull-request comments do not include this information: + +```sh +gh api graphql --paginate \ + -f query='query($owner: String!, $repository: String!, $number: Int!, $endCursor: String) { + repository(owner: $owner, name: $repository) { + pullRequest(number: $number) { + reviewThreads(first: 100, after: $endCursor) { + nodes { id isResolved } + pageInfo { hasNextPage endCursor } + } + } + } + }' \ + -F owner= \ + -F repository= \ + -F number= +``` + +For every unresolved thread identifier returned above, retrieve its complete comment history with a second paginated query: + +```sh +gh api graphql --paginate \ + -f query='query($thread: ID!, $endCursor: String) { + node(id: $thread) { + ... on PullRequestReviewThread { + comments(first: 100, after: $endCursor) { + nodes { id author { login } body url } + pageInfo { hasNextPage endCursor } + } + } + } + }' \ + -F thread= +``` + +Continue polling only while an allowed review round is pending. Inspect every returned page for reactions, review threads, and thread comments. Do not treat missing comments, a pending reaction, truncated results, or elapsed time as review completion, and do not submit a duplicate request merely because polling has not completed. + +## Merge requirements + +Do not merge while any of the following is true: + +- Codex review is still pending; +- an unresolved P0/P1 finding remains; +- a review thread lacks an explicit disposition or remains unresolved; +- a required check is pending or failing; +- the branch is out of date when the repository requires an up-to-date branch; +- required human approval or explicit owner authorization is missing. + +## Late findings + +If a review arrives after merge, assess and disposition its findings. A valid late P0/P1 finding requires prompt remediation through a corrective pull request and indicates that a review gate was missed. A late P2/P3 observation becomes backlog work when useful and is not by itself a process failure. + +## Repository protection + +Prefer GitHub rulesets or branch protection for the default branch. At minimum: + +- require changes to arrive through a pull request; +- require conversations to be resolved before merging; +- require the repository's mandatory status checks; +- prevent bypass except for an intentional emergency path. + +A formal one-approval rule works only when someone other than the pull-request author can submit an approving review. In a solo repository where the owner account also authors pull requests, use a bot or service account for authored changes before requiring owner approval; GitHub does not count self-approval. Until that separation exists, require explicit owner authorization operationally and keep conversation resolution enforced technically. diff --git a/AgentGuidelines/Guidelines/Logging.md b/Guidelines/Logging.md similarity index 100% rename from AgentGuidelines/Guidelines/Logging.md rename to Guidelines/Logging.md diff --git a/AgentGuidelines/Guidelines/Packages.md b/Guidelines/Packages.md similarity index 97% rename from AgentGuidelines/Guidelines/Packages.md rename to Guidelines/Packages.md index da332c3..a9b0576 100644 --- a/AgentGuidelines/Guidelines/Packages.md +++ b/Guidelines/Packages.md @@ -17,11 +17,12 @@ Use only badges that describe the repository, in this order: 3. Supported platforms. 4. Relevant package manager, runtime, or ecosystem badges, such as SPM or NPM. 5. Relevant agent or tooling badges, such as Xcode MCP, Codex, or Claude. -6. Updated date. -7. Revision or latest release. -8. License. -9. CI. -10. Release, publishing, or documentation status when applicable. +6. DocC, documentation. +7. License. +8. Updated date. +9. Revision or latest release. +10. CI badges. +11. Release/publishing status when applicable. The common package baseline is Swift, Xcode, Platforms, License, and CI. Add optional badges only when they convey useful repository-specific information. Keep the order stable even when some positions are omitted. diff --git a/AgentGuidelines/Guidelines/Swift/Localization.md b/Guidelines/Swift/Localization.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/Localization.md rename to Guidelines/Swift/Localization.md diff --git a/AgentGuidelines/Guidelines/Swift/Swift.md b/Guidelines/Swift/Swift.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/Swift.md rename to Guidelines/Swift/Swift.md diff --git a/AgentGuidelines/Guidelines/Swift/SwiftFormat.md b/Guidelines/Swift/SwiftFormat.md similarity index 64% rename from AgentGuidelines/Guidelines/Swift/SwiftFormat.md rename to Guidelines/Swift/SwiftFormat.md index b5060fb..eeb1f8d 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftFormat.md +++ b/Guidelines/Swift/SwiftFormat.md @@ -19,6 +19,58 @@ - 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. +## Swift package integration + +- Do not make `swift build` or `swift test` rewrite package sources. Formatting is an explicit local preparation step; builds and tests remain reproducible and non-mutating. +- Before building, testing, or handing off a package change, format and lint every checked-in Swift source root plus the manifest. A package with the standard layout runs: + + ```sh + AgentGuidelines/Scripts/swift_format.sh format-and-lint \ + Package.swift \ + Sources \ + Tests + + swift test + ``` + +- Omit a path only when it does not exist, and add nonstandard checked-in Swift source roots such as `Plugins` or `Examples`. Do not scan `.build`, generated artifacts, vendored dependencies, or another package's sources. +- Keep formatting and testing as consecutive, independently visible commands. A repository-owned convenience script may compose them, but formatting must finish before `swift test` begins and a formatting failure must stop the workflow. +- SwiftPM command plugins may provide an additional manual entry point, but they do not replace the shared configuration, wrapper, or CI check. Do not add a formatter package dependency solely to duplicate the toolchain-provided formatter without a documented repository need. + +[SwiftPM build-tool plugins](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) have read-only access to package source directories. This makes non-mutating lint possible in a custom build integration, but source-rewriting formatting does not belong inside the build. Prefer the explicit workflow above unless a package documents why every build must also pay the cost of a dedicated lint plugin. + +## CI integration + +- Run `lint-strict` in a dedicated, non-mutating job for pull requests and merges to the protected branch. Never run `format` or `format-and-lint` in CI. +- Use the same explicit source scope as the local workflow. Package CI includes `Package.swift`, `Sources`, `Tests`, and any additional checked-in Swift roots that exist. Xcode-project CI covers the union of source folders compiled by the project's independently buildable targets. +- Select the consumer's documented self-hosted macOS runner labels and supported Xcode toolchain. Keep repository-specific runner labels and Xcode selection outside this shared example. + +A typical Swift package job is: + +```yaml +swift-format: + name: Swift Format + runs-on: [self-hosted, macOS, ARM64] + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Select and log Xcode + run: | + xcodebuild -version + xcode-select -p + + - name: Run strict swift-format lint + run: | + AgentGuidelines/Scripts/swift_format.sh lint-strict \ + Package.swift \ + Sources \ + Tests +``` + +Adapt the runner labels and path list to the consumer. Keep the command shape unchanged so local execution, the consumer validator, and CI use the same shared wrapper and strict policy. + ## 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. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md b/Guidelines/Swift/SwiftStyle.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/SwiftStyle.md rename to Guidelines/Swift/SwiftStyle.md diff --git a/AgentGuidelines/Guidelines/Swift/SwiftUI.md b/Guidelines/Swift/SwiftUI.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/SwiftUI.md rename to Guidelines/Swift/SwiftUI.md diff --git a/AgentGuidelines/Guidelines/Testing/UnitTesting.md b/Guidelines/Testing/UnitTesting.md similarity index 100% rename from AgentGuidelines/Guidelines/Testing/UnitTesting.md rename to Guidelines/Testing/UnitTesting.md diff --git a/AgentGuidelines/Guidelines/Xcode/MCP.md b/Guidelines/Xcode/MCP.md similarity index 100% rename from AgentGuidelines/Guidelines/Xcode/MCP.md rename to Guidelines/Xcode/MCP.md diff --git a/AgentGuidelines/Guidelines/Xcode/Security.md b/Guidelines/Xcode/Security.md similarity index 100% rename from AgentGuidelines/Guidelines/Xcode/Security.md rename to Guidelines/Xcode/Security.md diff --git a/LICENSE b/LICENSE index 45f5b45..42d8021 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 06507de..0000000 --- a/Package.resolved +++ /dev/null @@ -1,33 +0,0 @@ -{ - "originHash" : "537618524d8febb222b9556d3d27add53e153103a47050a496e01cc2e96f821f", - "pins" : [ - { - "identity" : "applogger", - "kind" : "remoteSourceControl", - "location" : "https://github.com/thatfactory/applogger", - "state" : { - "revision" : "75dbe6e22170cb7e7507645cc23ab31611a07c84", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-plugin", - "state" : { - "revision" : "647c708be89f834fa6a6d4945442793a77ddf5b6", - "version" : "1.5.0" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - } - ], - "version" : 3 -} diff --git a/Package.swift b/Package.swift deleted file mode 100644 index 20faee8..0000000 --- a/Package.swift +++ /dev/null @@ -1,44 +0,0 @@ -// swift-tools-version:6.4 - -import PackageDescription - -let package = Package( - name: "ProgressionKit", - platforms: [ - .iOS(.v26), - .macOS(.v26), - .tvOS(.v26), - .watchOS(.v26) - ], - products: [ - .library( - name: "ProgressionKit", - targets: ["ProgressionKit"] - ) - ], - dependencies: [ - .package(url: "https://github.com/thatfactory/applogger", from: "1.1.0"), - .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") - ], - targets: [ - .target( - name: "ProgressionKit", - dependencies: [ - .product( - name: "AppLogger", - package: "applogger" - ) - ] - ), - .testTarget( - name: "ProgressionKitTests", - dependencies: [ - "ProgressionKit", - .product( - name: "AppLogger", - package: "applogger" - ) - ] - ) - ] -) diff --git a/README.md b/README.md index b062f81..b6ab81a 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,174 @@

- Swift - Xcode - Platforms - SPM - DocC + Xcode + Codex + Updated + Revision License - CI - Release + CI

-# ProgressionKit -A reusable progression engine that turns player performance into configurable XP, levels, and unlocks across games and apps. 📈 - -`ProgressionKit` is a pure Swift package for apps and games that need deterministic progression logic without coupling progression rules to storage or UI frameworks. - -It models: - -- `XP` gain from successful performance. -- Player levels derived from total XP. -- Track-scoped mastery across distinct content. -- Tier unlocks such as `beginner`, `intermediate`, and `advanced`. - -The package is deliberately content-agnostic. Host apps decide what a track, content item, and tier mean, then feed those identifiers into `ProgressionKit`. - -## Logging - -ProgressionKit logs concise progression outcomes through [AppLogger](https://github.com/thatfactory/applogger) with subsystem `com.thatfactory.progressionkit` and category `progression`. - -Every package-owned line starts with `📈` and includes only the XP granted, resulting player level, and number of newly unlocked tiers. ProgressionKit does not log content, track, or tier identifiers. - -## Implemented APIs - -- `PKEngine`: applies a progression event to a profile and returns the updated profile plus derived progress values. -- `PKProfile`: persisted progression state for a player. -- `PKConfig`: tunable progression rules such as level size, XP reward, tier order, and unlock thresholds. -- `PKEvent`: a single outcome emitted by the host app. -- `PKUpdate`: the result of applying one event. - -## Structure - -```mermaid -flowchart TB - subgraph HOST["Host App/Game"] - EVENTS["Performance Events"] - STORAGE["Storage Layer"] - UI["UI / HUD / XP Bar"] - end - - subgraph PK[" "] - ENGINE["ProgressionKit"] - PROFILE["PKProfile"] - CONFIG["PKConfig"] - UPDATE["PKUpdate"] - end - - EVENTS --> ENGINE - CONFIG --> ENGINE - ENGINE --> PROFILE - ENGINE --> UPDATE - PROFILE --> STORAGE - UPDATE --> UI +# Agent Guidelines + +`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 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 + +```text + thatfactory/agent-guidelines + versioned GitHub repository + | + tagged release + e.g. 0.0.3 + | + git subtree add/pull + | + v ++---------------- Consumer project or package -----------------+ +| | +| AGENTS.md | +| |-- local product/package context | +| |-- concrete project paths | +| |-- local exceptions | +| `-- pointers to shared guidelines -----------------+ | +| | | +| AgentGuidelines/ | | +| |-- VERSION | | +| |-- Configurations/ | | +| `-- Guidelines/ <----------------------------------+ | +| |-- Architecture/Redux.md | +| |-- Swift/SwiftUI.md | +| |-- Testing/UnitTesting.md | +| `-- Xcode/MCP.md | +| | +| Sources and project files | ++----------------------------+---------------------------------+ + | + reads instructions and project files + +----------+----------+ + v v + Codex Xcode agent + | + | Xcode MCP (`xcrun mcpbridge`) + v + Xcode ``` -## Quick Start - -Import the package and create an initial player profile: - -```swift -import ProgressionKit - -let profile = PKProfile() +The subtree does not automatically import every guide into an agent's context. A consumer's root or folder-scoped `AGENTS.md` tells the agent which shared guides to read for the task. The nearest local `AGENTS.md` can specialize or override the shared baseline. + +## 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) +- [Unit and integration testing](Guidelines/Testing/UnitTesting.md) +- [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) +- [Xcode security audits](Guidelines/Xcode/Security.md) + +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 + +From the consumer repository root, install a tagged release: + +```sh +git subtree add \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + 0.0.18 \ + --squash ``` -Create an event whenever the player finishes one unit of content: +Swift consumers that adopt the shared formatter expose its configuration at the repository root so Xcode and other tools discover it: -```swift -let event = PKEvent( - contentID: "lesson.greetings.001", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true -) +```sh +ln -s AgentGuidelines/Configurations/Swift/.swift-format .swift-format +ln -s AgentGuidelines/Configurations/Swift/.editorconfig .editorconfig ``` -Apply the event to the profile: +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: -```swift -let update = PKEngine.apply( - event: event, - to: profile -) +```gitattributes +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated ``` -`update` is a `PKUpdate` value that contains the updated `PKProfile` and derived progression values your app can render immediately. +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. Keep the version-marked code-review contract directly in the repository-root `AGENTS.md`; Markdown links to shared guides are navigation, not automatic instruction includes. -Common `PKUpdate` values you will typically use: +### Configure global Codex instructions -- `update.profile`: persist this as the new `PKProfile`. -- `update.playerLevel`: current player level. -- `update.xpIntoLevel` and `update.xpForNextLevel`: useful for progress bars. -- `update.newlyUnlockedTierIDs`: tiers unlocked by the latest event. -- `update.didGrantXP`: whether the event changed XP. +Copy the contents of [`Templates/GlobalCodexInstructions.md`](Templates/GlobalCodexInstructions.md) into the user's global Codex instructions. -## Configure Progression Rules +These instructions bootstrap discovery of repository-local `AGENTS.md` files and shared guides and provide generic high-signal code-review defaults. Repository engineering policy and specialized threat models remain versioned in this repository or the consumer rather than duplicated in each user's global configuration. -Use `PKConfig` when you want to customize level size, XP rewards, tier unlock order, and the mastery requirement for unlocking the next tier: +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. -```swift -let config = PKConfig( - levelXP: 120, - masteryXP: 15, - tierOrder: ["beginner", "intermediate", "advanced"], - masteryRequirement: 4 -) -``` +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). -Apply the same event with your custom config: +Expose the completion-audit skill at the consumer repository root so Codex can discover it: -```swift -let configuredUpdate = PKEngine.apply( - event: event, - to: profile, - config: config -) +```sh +mkdir -p .agents/skills +ln -s ../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ + .agents/skills/agent-guidelines-audit ``` -In practice: - -- Persist `configuredUpdate.profile` (your new `PKProfile`) after each event. -- Read other `PKUpdate` values to update your UI (XP gain, level changes, unlock state, and mastery). +Validate the checked-in consumer integration directly or through the completion-audit skill: -## SwiftUI Example (Simple Progress Bar) - -This example shows a simple integration pattern: apply progression events, keep the latest `PKUpdate`, and render a progress bar from the returned values. - -### Video - -https://github.com/user-attachments/assets/3920bbde-7b6b-40f6-b02f-f5506410b4fb - -### Code - -```swift -import ProgressionKit -import SwiftUI - -struct ProgressionDemoView: View { - @State private var profile = PKProfile() - @State private var lessonNumber = 1 - - private let config = PKConfig() - - private var progress: Double { - min(Double(profile.totalXP) / Double(config.levelXP), 1) - } - - var body: some View { - VStack(spacing: 16) { - Text(progress < 1 ? "Level 1" : "Level 2 🥳") - .font(.headline) - - GeometryReader { geometry in - let totalWidth = geometry.size.width - let fillWidth = totalWidth * progress - - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 10) - .fill(.gray.opacity(0.25)) - - RoundedRectangle(cornerRadius: 10) - .fill(.green) - .frame(width: fillWidth) - .animation(.snappy, value: progress) - } - } - .frame(height: 16) +```sh +python3 AgentGuidelines/Scripts/validate_consumer_setup.py +``` - Text("\(Int(progress * 100))%") - .font(.caption) - .foregroundStyle(.secondary) +The validator checks the version-marked root Code Review contract, Codex subtree-review scope, `.gitattributes`, local guide links, and the audit-skill symlink. When the root `AGENTS.md` links the shared Swift-format guide, it also requires both configuration symlinks and a non-mutating `lint-strict` CI invocation. Pass `--require-swift-format` only when auditing formatter adoption before adding that guide link. - Button("Complete Lesson") { - let event = PKEvent( - contentID: "lesson.greetings.\(lessonNumber)", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true - ) +## Update a consumer - let update = PKEngine.apply( - event: event, - to: profile, - config: config - ) +Review the target release's changelog, then pull it deliberately: - withAnimation(.snappy) { - profile = update.profile - } - lessonNumber += 1 - } - } - .padding() - } -} +```sh +git subtree pull \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + 0.0.18 \ + --squash +``` -// MARK: - Preview +Confirm `AgentGuidelines/VERSION`, review the subtree diff, synchronize the marked code-review contract when its version changes, run `python3 AgentGuidelines/Scripts/validate_consumer_setup.py`, 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. -#Preview { - ProgressionDemoView() -} -``` +## Maintain the source of truth -## Integration +1. Export current Xcode skills to a temporary review location when a new Xcode release materially changes agent behavior: -### Xcode -Use Xcode's [built-in support for SPM](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). + ```sh + xcrun agent skills export --output-dir + ``` -*or...* +2. Compare relevant guidance with this repository and official Apple documentation. +3. Bring over durable policy, not the exported skill text or an SDK API catalog. +4. Remove obsolete or conflicting rules instead of accumulating historical alternatives. +5. Run `python3 Scripts/validate_guidelines.py`. +6. Update `VERSION` and `CHANGELOG.md`, open a pull request, and wait for approval before merging. +7. After the pull request has merged, create the matching tag and GitHub release. -### Package.swift -In your `Package.swift`, add `ProgressionKit` as a dependency: +## Precedence -```swift -dependencies: [ - .package( - url: "https://github.com/thatfactory/progressionkit", - from: "0.1.5" - ) -] -``` +For a consumer task, apply instructions in this order: -Associate the dependency with your target: - -```swift -targets: [ - .target( - name: "YourTarget", - dependencies: [ - .product( - name: "ProgressionKit", - package: "progressionkit" - ) - ] - ) -] -``` +1. The user's explicit request. +2. The nearest applicable consumer `AGENTS.md`. +3. The consumer root `AGENTS.md`. +4. The shared guides explicitly referenced by those files. -Run: `swift build` +Official Apple documentation remains authoritative for API behavior. A local convention can deliberately narrow a choice, but it must not rely on behavior contradicted by the current SDK documentation. diff --git a/AgentGuidelines/Scripts/swift_format.sh b/Scripts/swift_format.sh similarity index 100% rename from AgentGuidelines/Scripts/swift_format.sh rename to Scripts/swift_format.sh diff --git a/Scripts/validate_consumer_setup.py b/Scripts/validate_consumer_setup.py new file mode 100755 index 0000000..687e1a1 --- /dev/null +++ b/Scripts/validate_consumer_setup.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +"""Validate a consumer repository's checked-in agent-guidelines integration.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + + +GUIDELINES_ROOT = Path(__file__).resolve().parents[1] +CONTRACT_BEGIN = "" +CONTRACT_END = "" +MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") +SWIFT_FORMAT_GUIDE = "AgentGuidelines/Guidelines/Swift/SwiftFormat.md" +STRICT_FORMAT_COMMAND = re.compile( + r"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?" + r"AgentGuidelines/Scripts/swift_format\.sh\s+lint-strict(?=\s|\\|$)", +) +MUTATING_FORMAT_COMMAND = re.compile( + r"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?" + r"AgentGuidelines/Scripts/swift_format\.sh\s+format(?:-and-lint)?(?=\s|\\|$)", +) +GENERATED_ATTRIBUTE = re.compile( + r"^\s*AgentGuidelines/\*\*\s+linguist-generated\s*$", + re.MULTILINE, +) + + +def read_text(path: Path, errors: list[str], label: str) -> str | None: + try: + return path.read_text(encoding="utf-8") + except OSError as error: + errors.append(f"{label}: cannot read {path}: {error}") + return None + + +def extract_contract(contents: str, errors: list[str], label: str) -> str | None: + if contents.count(CONTRACT_BEGIN) != 1 or contents.count(CONTRACT_END) != 1: + errors.append( + f"{label}: expected exactly one {CONTRACT_BEGIN!r} and {CONTRACT_END!r}" + ) + return None + + start = contents.index(CONTRACT_BEGIN) + end = contents.index(CONTRACT_END, start) + len(CONTRACT_END) + return contents[start:end].strip() + + +def validate_symlink(path: Path, expected: Path, errors: list[str], label: str) -> None: + if not path.is_symlink(): + errors.append(f"{label}: {path} must be a symlink to {expected}") + return + + if not path.exists(): + errors.append(f"{label}: {path} is a broken symlink") + return + + if path.resolve() != expected.resolve(): + errors.append(f"{label}: {path} resolves to {path.resolve()}, expected {expected.resolve()}") + + +def validate_agent_links( + agents_path: Path, + contents: str, + errors: list[str], +) -> None: + for raw_target in MARKDOWN_LINK.findall(contents): + target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] + if not target or target.startswith(("#", "http://", "https://", "mailto:")): + continue + + resolved = (agents_path.parent / target).resolve() + if not resolved.exists(): + errors.append(f"AGENTS.md: missing local link target {raw_target!r}") + + +def adopts_swift_format(contents: str) -> bool: + for raw_target in MARKDOWN_LINK.findall(contents): + target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] + if target.endswith(SWIFT_FORMAT_GUIDE): + return True + return False + + +def shell_invocations(contents: str, command: re.Pattern[str]) -> list[str]: + lines = contents.splitlines() + invocations: list[str] = [] + index = 0 + while index < len(lines): + line = lines[index] + if line.lstrip().startswith("#") or not command.search(line): + index += 1 + continue + + invocation = [line] + while invocation[-1].rstrip().endswith("\\") and index + 1 < len(lines): + index += 1 + invocation.append(lines[index]) + invocations.append("\n".join(invocation)) + index += 1 + return invocations + + +def validate_swift_format_ci(consumer_root: Path, errors: list[str]) -> None: + workflows_root = consumer_root / ".github" / "workflows" + workflows = sorted(workflows_root.glob("*.yml")) + sorted( + workflows_root.glob("*.yaml") + ) + if not workflows: + errors.append( + "consumer Swift format CI: no GitHub Actions workflows found under " + ".github/workflows" + ) + return + + strict_workflows: list[tuple[Path, str, list[str]]] = [] + for workflow in workflows: + contents = read_text( + workflow, + errors, + f"consumer Swift format CI workflow {workflow.relative_to(consumer_root)}", + ) + if contents is None: + continue + if shell_invocations(contents, MUTATING_FORMAT_COMMAND): + errors.append( + "consumer Swift format CI: " + f"{workflow.relative_to(consumer_root)} must not mutate sources with " + "format or format-and-lint" + ) + invocations = shell_invocations(contents, STRICT_FORMAT_COMMAND) + if invocations: + strict_workflows.append((workflow, contents, invocations)) + + if not strict_workflows: + errors.append( + "consumer Swift format CI: missing " + "'AgentGuidelines/Scripts/swift_format.sh lint-strict' invocation" + ) + return + + if not any( + re.search(r"(?m)^\s*pull_request\s*:", contents) + for _, contents, _ in strict_workflows + ): + errors.append("consumer Swift format CI: lint-strict does not run for pull requests") + + if not any( + re.search(r"(?m)^\s*push\s*:", contents) + and re.search(r"(?m)^\s*-?\s*main\s*$|branches\s*:\s*\[[^]]*\bmain\b", contents) + for _, contents, _ in strict_workflows + ): + errors.append("consumer Swift format CI: lint-strict does not run for pushes to main") + + if (consumer_root / "Package.swift").is_file(): + required_paths = ["Package.swift"] + required_paths.extend( + name for name in ("Sources", "Tests") if (consumer_root / name).is_dir() + ) + combined = "\n".join( + invocation + for _, _, invocations in strict_workflows + for invocation in invocations + ) + for path in required_paths: + if not re.search(rf"(? None: + consumer_root = consumer_root.resolve() + guidelines_root = guidelines_root.resolve() + subtree = consumer_root / "AgentGuidelines" + + if not subtree.exists(): + errors.append(f"consumer root: missing {subtree}") + elif subtree.resolve() != guidelines_root: + errors.append( + f"consumer root: {subtree} resolves to {subtree.resolve()}, " + f"expected the active guidelines at {guidelines_root}" + ) + + version_path = guidelines_root / "VERSION" + version = read_text(version_path, errors, "AgentGuidelines/VERSION") + if version is not None and not version.strip(): + errors.append("AgentGuidelines/VERSION: version is empty") + + template_path = guidelines_root / "Templates" / "AGENTS.md" + agents_path = consumer_root / "AGENTS.md" + template = read_text(template_path, errors, "AgentGuidelines template") + agents = read_text(agents_path, errors, "consumer AGENTS.md") + + swift_format_adopted = require_swift_format + if template is not None and agents is not None: + expected_contract = extract_contract(template, errors, "AgentGuidelines template") + actual_contract = extract_contract(agents, errors, "consumer AGENTS.md") + if ( + expected_contract is not None + and actual_contract is not None + and actual_contract != expected_contract + ): + errors.append( + "consumer AGENTS.md: code-review contract does not match " + "AgentGuidelines/Templates/AGENTS.md" + ) + + required_scope_values = ( + "## Codex review scope", + "AgentGuidelines/**", + "exact tagged-tree provenance", + ".gitattributes", + ) + for value in required_scope_values: + if value not in agents: + errors.append(f"consumer AGENTS.md: missing Codex review scope value {value!r}") + + validate_agent_links(agents_path, agents, errors) + swift_format_adopted = swift_format_adopted or adopts_swift_format(agents) + + attributes_path = consumer_root / ".gitattributes" + attributes = read_text(attributes_path, errors, "consumer .gitattributes") + if attributes is not None and not GENERATED_ATTRIBUTE.search(attributes): + errors.append( + "consumer .gitattributes: missing exact " + "'AgentGuidelines/** linguist-generated' rule" + ) + + expected_skill = guidelines_root / ".agents" / "skills" / "agent-guidelines-audit" + consumer_skill = consumer_root / ".agents" / "skills" / "agent-guidelines-audit" + validate_symlink(consumer_skill, expected_skill, errors, "consumer audit skill") + + formatter_links = { + ".swift-format": guidelines_root / "Configurations" / "Swift" / ".swift-format", + ".editorconfig": guidelines_root / "Configurations" / "Swift" / ".editorconfig", + } + for name, expected in formatter_links.items(): + path = consumer_root / name + if swift_format_adopted or path.is_symlink(): + validate_symlink(path, expected, errors, f"consumer {name}") + + if swift_format_adopted: + validate_swift_format_ci(consumer_root, errors) + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--consumer-root", + type=Path, + help="Consumer repository root. Defaults to the parent of an AgentGuidelines subtree.", + ) + parser.add_argument( + "--require-swift-format", + action="store_true", + help=( + "Require shared Swift-format configuration links and strict CI even when " + "AGENTS.md does not link the Swift-format guide." + ), + ) + return parser.parse_args() + + +def main() -> int: + arguments = parse_arguments() + consumer_root = arguments.consumer_root + if consumer_root is None: + if GUIDELINES_ROOT.name != "AgentGuidelines": + print( + "Consumer setup validation failed:\n" + "- --consumer-root is required when this checkout is not installed " + "as an AgentGuidelines subtree" + ) + return 1 + consumer_root = GUIDELINES_ROOT.parent + + errors: list[str] = [] + validate_consumer_setup( + errors, + consumer_root, + require_swift_format=arguments.require_swift_format, + ) + if errors: + print("Consumer setup validation failed:") + for error in errors: + print(f"- {error}") + return 1 + + version = (GUIDELINES_ROOT / "VERSION").read_text(encoding="utf-8").strip() + print(f"Validated consumer setup for agent-guidelines {version}.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/AgentGuidelines/Scripts/validate_guidelines.py b/Scripts/validate_guidelines.py similarity index 87% rename from AgentGuidelines/Scripts/validate_guidelines.py rename to Scripts/validate_guidelines.py index 23c935d..95fa1e3 100644 --- a/AgentGuidelines/Scripts/validate_guidelines.py +++ b/Scripts/validate_guidelines.py @@ -17,6 +17,8 @@ SWIFT_FORMAT_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".swift-format" EDITOR_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".editorconfig" SWIFT_FORMAT_SCRIPT = ROOT / "Scripts" / "swift_format.sh" +SWIFT_FORMAT_GUIDELINE = ROOT / "Guidelines" / "Swift" / "SwiftFormat.md" +CONSUMER_SETUP_SCRIPT = ROOT / "Scripts" / "validate_consumer_setup.py" AUDIT_SKILL = ROOT / ".agents" / "skills" / "agent-guidelines-audit" / "SKILL.md" DEVELOPMENT_GUIDELINE = ROOT / "Guidelines" / "Development.md" AGENTS_TEMPLATE = ROOT / "Templates" / "AGENTS.md" @@ -148,6 +150,8 @@ def validate_readme_contract(errors: list[str]) -> None: "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", + "validate_consumer_setup.py": "consumer setup validation command", + "--require-swift-format": "explicit Swift-format adoption validation", } for value, description in required.items(): if value not in readme: @@ -252,6 +256,33 @@ def validate_swift_format_script(errors: list[str]) -> None: errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: script is not executable") +def validate_swift_format_guideline(errors: list[str]) -> None: + contents = SWIFT_FORMAT_GUIDELINE.read_text(encoding="utf-8") + required = { + "## Swift package integration": "Swift package workflow", + "format-and-lint \\": "local package formatting command", + "Package.swift": "package manifest formatting scope", + "## CI integration": "CI workflow", + "lint-strict \\": "strict CI command", + "Never run `format` or `format-and-lint` in CI": "non-mutating CI rule", + } + for value, description in required.items(): + if value not in contents: + errors.append( + f"{SWIFT_FORMAT_GUIDELINE.relative_to(ROOT)}: " + f"missing {description}: {value!r}" + ) + + +def validate_consumer_setup_script(errors: list[str]) -> None: + if not CONSUMER_SETUP_SCRIPT.is_file(): + errors.append(f"{CONSUMER_SETUP_SCRIPT.relative_to(ROOT)}: missing script") + elif not os.access(CONSUMER_SETUP_SCRIPT, os.X_OK): + errors.append( + f"{CONSUMER_SETUP_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") @@ -262,6 +293,10 @@ def validate_audit_skill(errors: list[str]) -> None: "name: agent-guidelines-audit": "skill name", "before claiming completion": "completion trigger", "git diff --check": "diff validation", + "validate_consumer_setup.py": "consumer integration validation", + "format-and-lint": "local Swift-format audit", + "lint-strict": "strict Swift-format CI audit", + "no unresolved P0/P1 blocker remains": "Codex review stopping rule", } for value, description in required_skill_values.items(): if value not in skill: @@ -295,6 +330,8 @@ def main() -> int: validate_swift_format_configuration(errors) validate_editor_configuration(errors) validate_swift_format_script(errors) + validate_swift_format_guideline(errors) + validate_consumer_setup_script(errors) validate_audit_skill(errors) if errors: diff --git a/Sources/ProgressionKit/PKConfig.swift b/Sources/ProgressionKit/PKConfig.swift deleted file mode 100644 index 10044a7..0000000 --- a/Sources/ProgressionKit/PKConfig.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -/// Defines the rules that control XP gain, levels, and tier unlocks. -public struct PKConfig: Equatable, Codable, Sendable { - /// The amount of XP needed for each player level. - public let levelXP: Int - - /// The XP awarded when a content item grants mastery for the first time. - public let masteryXP: Int - - /// The ordered tier identifiers used to unlock more difficult content. - public let tierOrder: [String] - - /// The number of distinct mastered content items required to unlock the next tier. - public let masteryRequirement: Int - - /// Creates a progression configuration. - /// - /// - Parameters: - /// - levelXP: The amount of XP needed for each player level. - /// - masteryXP: The XP awarded when a content item grants mastery for the first time. - /// - tierOrder: The ordered tier identifiers used to unlock more difficult content. - /// - masteryRequirement: The number of distinct mastered content items required to unlock the next tier. - public init( - levelXP: Int = 100, - masteryXP: Int = 10, - tierOrder: [String] = ["beginner", "intermediate", "advanced"], - masteryRequirement: Int = 5 - ) { - precondition(levelXP > 0, "levelXP must be greater than zero.") - precondition(masteryXP >= 0, "masteryXP must be zero or greater.") - precondition(!tierOrder.isEmpty, "tierOrder must not be empty.") - precondition(masteryRequirement > 0, "masteryRequirement must be greater than zero.") - - self.levelXP = levelXP - self.masteryXP = masteryXP - self.tierOrder = tierOrder - self.masteryRequirement = masteryRequirement - } -} - -// MARK: - Defaults - -extension PKConfig { - /// The default configuration for tiered progression systems. - public static let standard = PKConfig() -} diff --git a/Sources/ProgressionKit/PKEngine.swift b/Sources/ProgressionKit/PKEngine.swift deleted file mode 100644 index 0873954..0000000 --- a/Sources/ProgressionKit/PKEngine.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Foundation - -/// Applies progression events to profiles using a deterministic rule set. -/// -/// Each application emits one `📈` debug log containing only the XP granted, resulting level, and unlock count. -public enum PKEngine { - /// Applies one progression event to a player profile. - /// - /// - Parameters: - /// - event: The event emitted by the host app. - /// - profile: The player profile to update. - /// - config: The progression rule set to apply. - /// - Returns: The updated profile and its derived values. - public static func apply( - event: PKEvent, - to profile: PKProfile, - config: PKConfig = .standard - ) -> PKUpdate { - var updatedProfile = profile - var trackProgress = updatedProfile.trackProgress[event.trackID] ?? defaultTrackProgress(config: config) - let isTierUnlocked = trackProgress.unlockedTierIDs.contains(event.tierID) - - guard event.wasSuccessful, isTierUnlocked else { - updatedProfile.trackProgress[event.trackID] = trackProgress - return makeUpdate( - profile: updatedProfile, - config: config, - didGrantXP: false, - newlyUnlockedTierIDs: [] - ) - } - - var tierProgress = trackProgress.tierProgress[event.tierID] ?? PKTierProgress() - guard !tierProgress.masteredContentIDs.contains(event.contentID) else { - updatedProfile.trackProgress[event.trackID] = trackProgress - return makeUpdate( - profile: updatedProfile, - config: config, - didGrantXP: false, - newlyUnlockedTierIDs: [] - ) - } - - tierProgress.masteredContentIDs.insert(event.contentID) - trackProgress.tierProgress[event.tierID] = tierProgress - updatedProfile.totalXP += config.masteryXP - - let newlyUnlockedTierIDs = unlockNextTierIfNeeded( - trackProgress: &trackProgress, - tierID: event.tierID, - config: config - ) - - updatedProfile.trackProgress[event.trackID] = trackProgress - - return makeUpdate( - profile: updatedProfile, - config: config, - didGrantXP: true, - newlyUnlockedTierIDs: newlyUnlockedTierIDs - ) - } -} - -// MARK: - Private - -private extension PKEngine { - static func defaultTrackProgress(config: PKConfig) -> PKTrackProgress { - let firstTierID = config.tierOrder[0] - - return PKTrackProgress( - unlockedTierIDs: [firstTierID] - ) - } - - static func unlockNextTierIfNeeded( - trackProgress: inout PKTrackProgress, - tierID: String, - config: PKConfig - ) -> [String] { - guard - let currentIndex = config.tierOrder.firstIndex(of: tierID), - currentIndex < config.tierOrder.count - 1, - let tierProgress = trackProgress.tierProgress[tierID], - tierProgress.masteredContentIDs.count >= config.masteryRequirement - else { - return [] - } - - let nextTierID = config.tierOrder[currentIndex + 1] - guard !trackProgress.unlockedTierIDs.contains(nextTierID) else { - return [] - } - - trackProgress.unlockedTierIDs.insert(nextTierID) - return [nextTierID] - } - - static func makeUpdate( - profile: PKProfile, - config: PKConfig, - didGrantXP: Bool, - newlyUnlockedTierIDs: [String] - ) -> PKUpdate { - let playerLevel = (profile.totalXP / config.levelXP) + 1 - let xpIntoLevel = profile.totalXP % config.levelXP - - let update = PKUpdate( - didGrantXP: didGrantXP, - newlyUnlockedTierIDs: newlyUnlockedTierIDs, - playerLevel: playerLevel, - profile: profile, - xpForNextLevel: config.levelXP, - xpIntoLevel: xpIntoLevel - ) - PKLogging.logProgression( - update: update, - xpGranted: didGrantXP ? config.masteryXP : 0 - ) - return update - } -} diff --git a/Sources/ProgressionKit/PKEvent.swift b/Sources/ProgressionKit/PKEvent.swift deleted file mode 100644 index dc1d24c..0000000 --- a/Sources/ProgressionKit/PKEvent.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -/// Represents one gameplay outcome that can affect player progression. -public struct PKEvent: Equatable, Codable, Sendable { - /// The stable identifier for the content item that was attempted. - public let contentID: String - - /// The stable identifier for the track this content belongs to. - public let trackID: String - - /// The tier identifier for the attempted content. - public let tierID: String - - /// Indicates whether the attempt should grant progression credit. - public let wasSuccessful: Bool - - /// Creates a progression event. - /// - /// - Parameters: - /// - contentID: The stable identifier for the content item that was attempted. - /// - trackID: The stable identifier for the track this content belongs to. - /// - tierID: The tier identifier for the attempted content. - /// - wasSuccessful: Indicates whether the attempt should grant progression credit. - public init( - contentID: String, - trackID: String, - tierID: String, - wasSuccessful: Bool - ) { - self.contentID = contentID - self.trackID = trackID - self.tierID = tierID - self.wasSuccessful = wasSuccessful - } -} diff --git a/Sources/ProgressionKit/PKLogging.swift b/Sources/ProgressionKit/PKLogging.swift deleted file mode 100644 index 5357884..0000000 --- a/Sources/ProgressionKit/PKLogging.swift +++ /dev/null @@ -1,42 +0,0 @@ -import AppLogger - -/// Routes ProgressionKit-owned diagnostics through the package logging identity. -enum PKLogging { - typealias Sink = @Sendable (AppLogLevel, PKLogCategory, String, Bool) -> Void - - static let emoji = "📈" - static let subsystem = "com.thatfactory.progressionkit" - - @TaskLocal - static var sink: Sink = { level, category, message, isPrivate in - let logger = AppLogger( - subsystem: subsystem, - category: category.rawValue - ) - logger.log( - level: level, - message, - isPrivate: isPrivate - ) - } - - static func logProgression( - update: PKUpdate, - xpGranted: Int - ) { - sink( - .debug, - .progression, - """ - \(emoji) apply | xpGranted=\(xpGranted), \ - level=\(update.playerLevel), unlocked=\(update.newlyUnlockedTierIDs.count) - """, - false - ) - } -} - -/// Identifies stable diagnostic categories owned by ProgressionKit. -enum PKLogCategory: String, Sendable { - case progression -} diff --git a/Sources/ProgressionKit/PKProfile.swift b/Sources/ProgressionKit/PKProfile.swift deleted file mode 100644 index d03acb3..0000000 --- a/Sources/ProgressionKit/PKProfile.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// Stores the persisted progression state for one player. -public struct PKProfile: Equatable, Codable, Sendable { - /// The player's cumulative XP across all tracks. - public var totalXP: Int - - /// The player's progression grouped by track identifier. - public var trackProgress: [String: PKTrackProgress] - - /// Creates a player progression profile. - /// - /// - Parameters: - /// - totalXP: The player's cumulative XP across all tracks. - /// - trackProgress: The player's progression grouped by track identifier. - public init( - totalXP: Int = 0, - trackProgress: [String: PKTrackProgress] = [:] - ) { - self.totalXP = totalXP - self.trackProgress = trackProgress - } -} diff --git a/Sources/ProgressionKit/PKTierProgress.swift b/Sources/ProgressionKit/PKTierProgress.swift deleted file mode 100644 index 6b2d0f9..0000000 --- a/Sources/ProgressionKit/PKTierProgress.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -/// Stores mastery information for a single tier within a track. -public struct PKTierProgress: Equatable, Codable, Sendable { - /// The distinct content identifiers that already granted mastery credit. - public var masteredContentIDs: Set - - /// Creates tier progress state. - /// - /// - Parameter masteredContentIDs: The distinct content identifiers that already granted mastery credit. - public init(masteredContentIDs: Set = []) { - self.masteredContentIDs = masteredContentIDs - } -} diff --git a/Sources/ProgressionKit/PKTrackProgress.swift b/Sources/ProgressionKit/PKTrackProgress.swift deleted file mode 100644 index 31962e6..0000000 --- a/Sources/ProgressionKit/PKTrackProgress.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// Stores progression for one track, including unlocked tiers and mastery history. -public struct PKTrackProgress: Equatable, Codable, Sendable { - /// The set of tiers currently unlocked for this track. - public var unlockedTierIDs: Set - - /// The mastery state grouped by tier identifier. - public var tierProgress: [String: PKTierProgress] - - /// Creates track progress state. - /// - /// - Parameters: - /// - unlockedTierIDs: The set of tiers currently unlocked for this track. - /// - tierProgress: The mastery state grouped by tier identifier. - public init( - unlockedTierIDs: Set = [], - tierProgress: [String: PKTierProgress] = [:] - ) { - self.unlockedTierIDs = unlockedTierIDs - self.tierProgress = tierProgress - } -} diff --git a/Sources/ProgressionKit/PKUpdate.swift b/Sources/ProgressionKit/PKUpdate.swift deleted file mode 100644 index 2b54e4d..0000000 --- a/Sources/ProgressionKit/PKUpdate.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -/// Describes the result of applying one progression event. -public struct PKUpdate: Equatable, Codable, Sendable { - /// Indicates whether the event granted new XP. - public let didGrantXP: Bool - - /// The tiers unlocked by this event, if any. - public let newlyUnlockedTierIDs: [String] - - /// The player's current level derived from total XP. - public let playerLevel: Int - - /// The updated profile after applying the event. - public let profile: PKProfile - - /// The XP required to complete the current level. - public let xpForNextLevel: Int - - /// The amount of XP already earned within the current level. - public let xpIntoLevel: Int - - /// Creates a progression update. - /// - /// - Parameters: - /// - didGrantXP: Indicates whether the event granted new XP. - /// - newlyUnlockedTierIDs: The tiers unlocked by this event, if any. - /// - playerLevel: The player's current level derived from total XP. - /// - profile: The updated profile after applying the event. - /// - xpForNextLevel: The XP required to complete the current level. - /// - xpIntoLevel: The amount of XP already earned within the current level. - public init( - didGrantXP: Bool, - newlyUnlockedTierIDs: [String], - playerLevel: Int, - profile: PKProfile, - xpForNextLevel: Int, - xpIntoLevel: Int - ) { - self.didGrantXP = didGrantXP - self.newlyUnlockedTierIDs = newlyUnlockedTierIDs - self.playerLevel = playerLevel - self.profile = profile - self.xpForNextLevel = xpForNextLevel - self.xpIntoLevel = xpIntoLevel - } -} diff --git a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md b/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md deleted file mode 100644 index 0754f83..0000000 --- a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md +++ /dev/null @@ -1,69 +0,0 @@ -# ``ProgressionKit`` - -Deterministic progression logic for XP, levels, and tier unlocks. - -@Metadata { - @Available(iOS, introduced: "26.0") - @Available(macOS, introduced: "26.0") - @Available(tvOS, introduced: "26.0") - @Available(watchOS, introduced: "26.0") - @Available(visionOS, introduced: "26.0") -} - -## Overview - -`ProgressionKit` is a pure Swift package for apps and games that need deterministic progression logic without coupling progression rules to storage or UI frameworks. - -It models XP gain from successful performance, player levels derived from total XP, track-scoped mastery across distinct content, and tier unlocks such as `beginner`, `intermediate`, and `advanced`. - -The package is content-agnostic. Host apps decide what a track, content item, and tier mean, then feed those identifiers into ``PKEngine``. - -Each applied event emits one concise `📈` debug log through `AppLogger`. ProgressionKit uses subsystem `com.thatfactory.progressionkit`, category `progression`, and omits content, track, and tier identifiers. - -## Usage - -```swift -import ProgressionKit - -let profile = PKProfile() -let event = PKEvent( - contentID: "A11IYR-CE4D7B84", - trackID: "A11IYR", - tierID: "beginner", - wasSuccessful: true -) - -let update = PKEngine.apply( - event: event, - to: profile -) -``` - -```swift -let config = PKConfig( - levelXP: 120, - masteryXP: 15, - tierOrder: ["bronze", "silver", "gold"], - masteryRequirement: 4 -) - -let tunedUpdate = PKEngine.apply( - event: event, - to: profile, - config: config -) -``` - -## Topics - -### Core Types - -Use ``PKProfile`` to store player progression, ``PKEvent`` to represent one gameplay outcome, and ``PKUpdate`` to read the derived result after applying an event. - -### Engine - -Use ``PKEngine/apply(event:to:config:)`` to apply progression rules synchronously and deterministically. - -### Configuration - -Use ``PKConfig`` to tune level size, mastery XP, tier order, and the unlock threshold for your app or game. diff --git a/Sources/ProgressionKit/ProgressionKit.swift b/Sources/ProgressionKit/ProgressionKit.swift deleted file mode 100644 index fecc4ab..0000000 --- a/Sources/ProgressionKit/ProgressionKit.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/Templates/AGENTS.md b/Templates/AGENTS.md new file mode 100644 index 0000000..2728920 --- /dev/null +++ b/Templates/AGENTS.md @@ -0,0 +1,74 @@ +# Project Instructions + +## Context + +Describe the product or package, supported platforms, and durable constraints. Link to the project README or product documentation instead of duplicating it. + +## Shared guidelines + +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) +- [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) +- [Xcode MCP and visual verification](AgentGuidelines/Guidelines/Xcode/MCP.md) +- [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) + +For an application that uses Redux, also read [Redux architecture](AgentGuidelines/Guidelines/Architecture/Redux.md). + +Keep the following marked contract in the consumer repository's root `AGENTS.md` so it is loaded directly for root-level Codex and pull-request work. Copy it unchanged and update it when the marker version changes in this template; a Markdown link to the detailed workflow is not an instruction include. + +```md + +## Code Review Rules + +Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. + +A blocking finding must identify a concrete, reachable path in a supported use case or the documented threat model that can cause a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. + +For every blocking finding, state the severity, preconditions, execution path, impact, evidence, and actionable remediation. Group manifestations that share the same root cause into one finding. + +Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, behavior by components outside the threat model, style preferences, and speculative refactoring. Record a useful lower-severity observation once as deferred, declined, duplicate, or follow-up work; do not keep the review loop open for it. + +In an initial review, report substantiated blockers together. A follow-up review is limited to unresolved P0/P1 findings, changes since the last reviewed commit, and code directly affected by those changes. Do not restart an unrestricted review of unchanged code. A new follow-up finding must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +Automatic Codex review is the initial review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another review after each remediation commit. Within the normal review budget, at most one owner-authorized, delta-scoped verification review may be requested under [the pull-request review workflow](AgentGuidelines/Guidelines/GitHub/PullRequests.md). + + +## Codex review scope + +For consumer pull requests, do not substantively review `AgentGuidelines/**` after exact tagged-tree provenance has been verified. Verify its `VERSION`, compare its tree with the matching central tag, and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request. +``` + +The marked block is intentional controlled duplication of the shared review policy. The tracked, synchronized subtree is reviewed centrally in `thatfactory/agent-guidelines`; the root-level instructions ensure the review contract and subtree scope are loaded even when Codex starts from the repository root. + +## Physical folder map + +Replace these examples with exact repository paths: + +| Role | Physical folder | +|---|---| +| Application sources | `/` | +| Redux | `/Redux/` | +| Views | `/View/` | +| 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/Templates/GlobalCodexInstructions.md b/Templates/GlobalCodexInstructions.md new file mode 100644 index 0000000..abe4578 --- /dev/null +++ b/Templates/GlobalCodexInstructions.md @@ -0,0 +1,30 @@ +# 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-specific guidance in global instructions. Global instructions should bootstrap discovery of the repository's own sources of truth. + +## Code review behavior + +When acting as a code reviewer, optimize for high-signal release risk and convergence rather than exhaustive perfection. + +Create an inline finding only when all of the following are true: + +1. The issue is introduced or materially exposed by the proposed change. +2. There is a concrete, reachable failure path in a supported use case or the documented threat model. +3. The impact is P0 or P1: a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. +4. The evidence and remediation are specific enough to be actionable. + +State the finding's severity, preconditions, execution path, impact, and evidence. Group findings that share the same root cause. Do not create separate serial comments for additional manifestations of an already reported root cause. + +Treat P2 and P3 observations as non-blocking. This includes defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, adversarial behavior by components outside the threat model, style preferences, speculative refactoring, and exhaustive enumeration of equivalent input formats. Summarize valuable lower-severity observations once or recommend a follow-up issue. + +In the initial review, report substantiated blockers together rather than drip-feeding them across repeated reviews. + +In a follow-up review, verify previously reported P0/P1 findings and review only changes since the previously reviewed commit plus code directly affected by those changes. Do not restart an unrestricted search of unchanged code. A newly introduced follow-up finding must be a P0/P1 issue introduced by the remediation or genuinely hidden by the previous defect. + +A clean review means that there are no unresolved P0/P1 blockers. It does not mean perfect software, zero possible improvements, or zero technical debt. diff --git a/AgentGuidelines/Templates/Store.swift b/Templates/Store.swift similarity index 100% rename from AgentGuidelines/Templates/Store.swift rename to Templates/Store.swift diff --git a/Tests/ProgressionKitTests/PKLoggingTests.swift b/Tests/ProgressionKitTests/PKLoggingTests.swift deleted file mode 100644 index ef2945f..0000000 --- a/Tests/ProgressionKitTests/PKLoggingTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -import AppLogger -import Foundation -import Testing -@testable import ProgressionKit - -@Suite struct PKLoggingTests { - @Test func applyingEventLogsOnePackageOwnedOutcome() throws { - // Given - let recorder = PKLogRecorder() - let event = PKEvent( - contentID: "Sensitive content", - trackID: "Sensitive track", - tierID: "beginner", - wasSuccessful: true - ) - let config = PKConfig( - levelXP: 10, - masteryXP: 15, - tierOrder: ["beginner", "intermediate"], - masteryRequirement: 1 - ) - - // When - PKLogging.$sink.withValue(recorder.record) { - _ = PKEngine.apply( - event: event, - to: PKProfile(), - config: config - ) - } - - // Then - let entry = try #require(recorder.entries.first) - #expect(recorder.entries.count == 1) - #expect(entry.category == .progression) - #expect(!entry.isPrivate) - #expect(entry.message == "📈 apply | xpGranted=15, level=2, unlocked=1") - #expect(!entry.message.contains(event.contentID)) - #expect(!entry.message.contains(event.trackID)) - #expect(!entry.message.contains(event.tierID)) - #expect(PKLogging.subsystem == "com.thatfactory.progressionkit") - expectDebug(entry.level) - } -} - -// MARK: - Private - -private func expectDebug( - _ level: AppLogLevel, - sourceLocation: SourceLocation = #_sourceLocation -) { - guard case .debug = level else { - Issue.record( - "Expected a debug log level.", - sourceLocation: sourceLocation - ) - return - } -} - -/// Records ProgressionKit log entries emitted during a test. -private final class PKLogRecorder: @unchecked Sendable { - private var internalEntries: [PKRecordedLog] = [] - private let lock = NSLock() - - var entries: [PKRecordedLog] { - lock.lock() - defer { - lock.unlock() - } - return internalEntries - } - - func record( - _ level: AppLogLevel, - _ category: PKLogCategory, - _ message: String, - _ isPrivate: Bool - ) { - lock.lock() - defer { - lock.unlock() - } - internalEntries.append( - PKRecordedLog( - level: level, - category: category, - message: message, - isPrivate: isPrivate - ) - ) - } -} - -private struct PKRecordedLog { - let level: AppLogLevel - let category: PKLogCategory - let message: String - let isPrivate: Bool -} diff --git a/Tests/ProgressionKitTests/ProgressionKitTests.swift b/Tests/ProgressionKitTests/ProgressionKitTests.swift deleted file mode 100644 index ed99a38..0000000 --- a/Tests/ProgressionKitTests/ProgressionKitTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -import Testing -@testable import ProgressionKit - -@Test func grantsXPForFirstSuccessfulCompletion() { - // Given - let event = PKEvent( - contentID: "lesson-1", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: true - ) - - // When - let update = PKEngine.apply( - event: event, - to: PKProfile() - ) - - // Then - #expect(update.didGrantXP) - #expect(update.profile.totalXP == 10) - #expect(update.playerLevel == 1) - #expect(update.xpIntoLevel == 10) -} - -@Test func doesNotGrantXPForRepeatedSuccessfulCompletion() { - // Given - let event = PKEvent( - contentID: "lesson-1", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: true - ) - let firstUpdate = PKEngine.apply( - event: event, - to: PKProfile() - ) - - // When - let secondUpdate = PKEngine.apply( - event: event, - to: firstUpdate.profile - ) - - // Then - #expect(!secondUpdate.didGrantXP) - #expect(secondUpdate.profile.totalXP == 10) -} - -@Test func doesNotGrantXPForIncorrectAttempts() { - // Given - let event = PKEvent( - contentID: "lesson-1", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: false - ) - - // When - let update = PKEngine.apply( - event: event, - to: PKProfile() - ) - - // Then - #expect(!update.didGrantXP) - #expect(update.profile.totalXP == 0) -} - -@Test func unlocksIntermediateAfterDistinctBeginnerMastery() { - // Given - let events = (1 ... 5).map { index in - PKEvent( - contentID: "lesson-\(index)", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: true - ) - } - - // When - let finalUpdate = events.reduce( - PKUpdate( - didGrantXP: false, - newlyUnlockedTierIDs: [], - playerLevel: 1, - profile: PKProfile(), - xpForNextLevel: 100, - xpIntoLevel: 0 - ) - ) { partialUpdate, event in - PKEngine.apply( - event: event, - to: partialUpdate.profile - ) - } - - // Then - #expect(finalUpdate.newlyUnlockedTierIDs == ["intermediate"]) - #expect( - finalUpdate.profile.trackProgress["a11-reading"]?.unlockedTierIDs.contains("intermediate") == true - ) -} - -@Test func unlocksAdvancedAfterDistinctIntermediateMastery() { - // Given - let beginnerEvents = (1 ... 5).map { index in - PKEvent( - contentID: "beginner-\(index)", - trackID: "a11-writing", - tierID: "beginner", - wasSuccessful: true - ) - } - let intermediateEvents = (1 ... 5).map { index in - PKEvent( - contentID: "intermediate-\(index)", - trackID: "a11-writing", - tierID: "intermediate", - wasSuccessful: true - ) - } - - let unlockedIntermediateProfile = beginnerEvents.reduce(PKProfile()) { profile, event in - PKEngine.apply( - event: event, - to: profile - ).profile - } - - // When - let finalUpdate = intermediateEvents.reduce( - PKUpdate( - didGrantXP: false, - newlyUnlockedTierIDs: [], - playerLevel: 1, - profile: unlockedIntermediateProfile, - xpForNextLevel: 100, - xpIntoLevel: 0 - ) - ) { partialUpdate, event in - PKEngine.apply( - event: event, - to: partialUpdate.profile - ) - } - - // Then - #expect(finalUpdate.newlyUnlockedTierIDs == ["advanced"]) - #expect( - finalUpdate.profile.trackProgress["a11-writing"]?.unlockedTierIDs.contains("advanced") == true - ) -} - -@Test func derivesLevelAcrossBoundary() { - // Given - let config = PKConfig( - levelXP: 100, - masteryXP: 25, - tierOrder: ["beginner", "intermediate", "advanced"], - masteryRequirement: 5 - ) - let events = (1 ... 4).map { index in - PKEvent( - contentID: "lesson-\(index)", - trackID: "a11-listening", - tierID: "beginner", - wasSuccessful: true - ) - } - - // When - let finalProfile = events.reduce(PKProfile()) { profile, event in - PKEngine.apply( - event: event, - to: profile, - config: config - ).profile - } - let finalUpdate = PKEngine.apply( - event: PKEvent( - contentID: "lesson-5", - trackID: "a11-listening", - tierID: "beginner", - wasSuccessful: false - ), - to: finalProfile, - config: config - ) - - // Then - #expect(finalUpdate.profile.totalXP == 100) - #expect(finalUpdate.playerLevel == 2) - #expect(finalUpdate.xpIntoLevel == 0) - #expect(finalUpdate.xpForNextLevel == 100) -} diff --git a/Tests/test_validate_consumer_setup.py b/Tests/test_validate_consumer_setup.py new file mode 100644 index 0000000..4db91af --- /dev/null +++ b/Tests/test_validate_consumer_setup.py @@ -0,0 +1,253 @@ +"""Tests for consumer agent-guidelines integration validation.""" + +from __future__ import annotations + +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +VALIDATOR_PATH = ( + Path(__file__).resolve().parents[1] / "Scripts" / "validate_consumer_setup.py" +) +SPEC = importlib.util.spec_from_file_location("validate_consumer_setup", VALIDATOR_PATH) +assert SPEC is not None +assert SPEC.loader is not None +VALIDATOR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VALIDATOR) + + +class ConsumerSetupTests(unittest.TestCase): + """Verifies checked-in consumer guidance and symlink wiring.""" + + def setUp(self) -> None: + self.temporary_directory = tempfile.TemporaryDirectory(dir=VALIDATOR.GUIDELINES_ROOT) + self.consumer_root = Path(self.temporary_directory.name) + (self.consumer_root / "AgentGuidelines").symlink_to( + VALIDATOR.GUIDELINES_ROOT, + target_is_directory=True, + ) + + template = ( + VALIDATOR.GUIDELINES_ROOT / "Templates" / "AGENTS.md" + ).read_text(encoding="utf-8") + contract_errors: list[str] = [] + contract = VALIDATOR.extract_contract(template, contract_errors, "template") + self.assertEqual(contract_errors, []) + self.assertIsNotNone(contract) + + agents = f"""# Project Instructions + +{contract} + +## Codex review scope + +For consumer pull requests, verify exact tagged-tree provenance before excluding +`AgentGuidelines/**`, and confirm the required `.gitattributes` rule. +""" + (self.consumer_root / "AGENTS.md").write_text(agents, encoding="utf-8") + (self.consumer_root / ".gitattributes").write_text( + "AgentGuidelines/** linguist-generated\n", + encoding="utf-8", + ) + + skill_parent = self.consumer_root / ".agents" / "skills" + skill_parent.mkdir(parents=True) + (skill_parent / "agent-guidelines-audit").symlink_to( + VALIDATOR.GUIDELINES_ROOT + / ".agents" + / "skills" + / "agent-guidelines-audit", + target_is_directory=True, + ) + + def tearDown(self) -> None: + self.temporary_directory.cleanup() + + def validate(self, require_swift_format: bool = False) -> list[str]: + errors: list[str] = [] + VALIDATOR.validate_consumer_setup( + errors, + self.consumer_root, + require_swift_format=require_swift_format, + ) + return errors + + def adopt_swift_format(self) -> None: + agents_path = self.consumer_root / "AGENTS.md" + agents = agents_path.read_text(encoding="utf-8") + agents_path.write_text( + f"{agents}\n[Swift format]({VALIDATOR.SWIFT_FORMAT_GUIDE})\n", + encoding="utf-8", + ) + + def add_swift_format_links(self) -> None: + (self.consumer_root / ".swift-format").symlink_to( + VALIDATOR.GUIDELINES_ROOT / "Configurations" / "Swift" / ".swift-format" + ) + (self.consumer_root / ".editorconfig").symlink_to( + VALIDATOR.GUIDELINES_ROOT / "Configurations" / "Swift" / ".editorconfig" + ) + + def add_package(self) -> None: + (self.consumer_root / "Package.swift").write_text( + "// swift-tools-version: 6.0\n", + encoding="utf-8", + ) + (self.consumer_root / "Sources").mkdir() + (self.consumer_root / "Tests").mkdir() + + def add_strict_format_ci(self, paths: tuple[str, ...] = ()) -> None: + workflows = self.consumer_root / ".github" / "workflows" + workflows.mkdir(parents=True) + command = " AgentGuidelines/Scripts/swift_format.sh lint-strict" + if paths: + path_lines = [] + for index, path in enumerate(paths): + continuation = " \\" if index < len(paths) - 1 else "" + path_lines.append(f" {path}{continuation}") + command += " \\\n" + "\n".join(path_lines) + (workflows / "ci-pr.yml").write_text( + f"""name: CI (PR) +on: + pull_request: + push: + branches: [main] +jobs: + swift-format: + name: Swift Format + steps: + - name: Run strict swift-format lint + run: | +{command} +""", + encoding="utf-8", + ) + + def test_valid_consumer_setup(self) -> None: + """Accepts a synchronized root contract and repository skill symlink.""" + self.assertEqual(self.validate(), []) + + def test_rejects_stale_code_review_contract(self) -> None: + """Rejects a consumer root contract that drifted from the template.""" + agents_path = self.consumer_root / "AGENTS.md" + agents = agents_path.read_text(encoding="utf-8") + agents_path.write_text(agents.replace("P0/P1", "P0", 1), encoding="utf-8") + + self.assertTrue( + any("code-review contract does not match" in error for error in self.validate()) + ) + + def test_rejects_copied_audit_skill(self) -> None: + """Rejects a stale-copy risk in place of the repository symlink.""" + skill = self.consumer_root / ".agents" / "skills" / "agent-guidelines-audit" + skill.unlink() + skill.mkdir() + (skill / "SKILL.md").write_text("stale copy\n", encoding="utf-8") + + self.assertTrue(any("must be a symlink" in error for error in self.validate())) + + def test_requires_generated_attribute(self) -> None: + """Rejects a consumer without the collapsed subtree diff rule.""" + (self.consumer_root / ".gitattributes").write_text("*.md text\n", encoding="utf-8") + + self.assertTrue(any("linguist-generated" in error for error in self.validate())) + + def test_rejects_missing_local_agent_link(self) -> None: + """Rejects a local guideline pointer that does not resolve.""" + agents_path = self.consumer_root / "AGENTS.md" + agents = agents_path.read_text(encoding="utf-8") + agents_path.write_text( + f"{agents}\n[Missing guide](AgentGuidelines/Guidelines/Missing.md)\n", + encoding="utf-8", + ) + + self.assertTrue( + any("missing local link target" in error for error in self.validate()) + ) + + def test_explicitly_requires_formatter_adoption(self) -> None: + """Allows an audit to require formatting before AGENTS.md links the guide.""" + self.assertEqual(self.validate(), []) + + errors = self.validate(require_swift_format=True) + + self.assertTrue(any("consumer .swift-format" in error for error in errors)) + self.assertTrue(any("consumer .editorconfig" in error for error in errors)) + self.assertTrue(any("Swift format CI" in error for error in errors)) + + def test_adopted_formatter_requires_links_and_ci(self) -> None: + """Detects incomplete adoption from the consumer AGENTS.md link.""" + self.adopt_swift_format() + + errors = self.validate() + + self.assertTrue(any("consumer .swift-format" in error for error in errors)) + self.assertTrue(any("consumer .editorconfig" in error for error in errors)) + self.assertTrue(any("Swift format CI" in error for error in errors)) + + def test_accepts_adopted_package_formatter(self) -> None: + """Accepts package configuration links and complete strict CI coverage.""" + self.adopt_swift_format() + self.add_swift_format_links() + self.add_package() + self.add_strict_format_ci(("Package.swift", "Sources", "Tests")) + + self.assertEqual(self.validate(), []) + + def test_rejects_mutating_ci_formatter(self) -> None: + """Rejects source rewriting inside CI even when strict lint is also present.""" + self.adopt_swift_format() + self.add_swift_format_links() + self.add_strict_format_ci() + workflow = self.consumer_root / ".github" / "workflows" / "ci-pr.yml" + workflow.write_text( + workflow.read_text(encoding="utf-8") + + " AgentGuidelines/Scripts/swift_format.sh format Sources\n", + encoding="utf-8", + ) + + self.assertTrue(any("must not mutate sources" in error for error in self.validate())) + + def test_rejects_ci_without_required_triggers(self) -> None: + """Requires strict lint for pull requests and main-branch merges.""" + self.adopt_swift_format() + self.add_swift_format_links() + self.add_strict_format_ci() + workflow = self.consumer_root / ".github" / "workflows" / "ci-pr.yml" + contents = workflow.read_text(encoding="utf-8") + workflow.write_text( + contents.replace(" pull_request:\n", "").replace( + " push:\n branches: [main]\n", + "", + ), + encoding="utf-8", + ) + + errors = self.validate() + + self.assertTrue(any("does not run for pull requests" in error for error in errors)) + self.assertTrue(any("does not run for pushes to main" in error for error in errors)) + + def test_rejects_incomplete_package_ci_scope(self) -> None: + """Requires all standard package Swift roots in strict CI.""" + self.adopt_swift_format() + self.add_swift_format_links() + self.add_package() + self.add_strict_format_ci(("Sources",)) + workflow = self.consumer_root / ".github" / "workflows" / "ci-pr.yml" + workflow.write_text( + workflow.read_text(encoding="utf-8") + + " - run: echo Package.swift Tests\n", + encoding="utf-8", + ) + + errors = self.validate() + + self.assertTrue(any("'Package.swift'" in error for error in errors)) + self.assertTrue(any("'Tests'" in error for error in errors)) + + +if __name__ == "__main__": + unittest.main() diff --git a/AgentGuidelines/Tests/test_validate_guidelines.py b/Tests/test_validate_guidelines.py similarity index 89% rename from AgentGuidelines/Tests/test_validate_guidelines.py rename to Tests/test_validate_guidelines.py index d250633..1216b37 100644 --- a/AgentGuidelines/Tests/test_validate_guidelines.py +++ b/Tests/test_validate_guidelines.py @@ -111,6 +111,14 @@ def test_editor_configuration(self) -> None: self.assertEqual(errors, []) + def test_swift_format_guideline(self) -> None: + """Accepts the documented package and non-mutating CI workflows.""" + errors: list[str] = [] + + VALIDATOR.validate_swift_format_guideline(errors) + + self.assertEqual(errors, []) + class AgentGuidelinesAuditSkillTests(unittest.TestCase): """Verifies the mandatory completion-audit skill contract.""" @@ -123,6 +131,14 @@ def test_audit_skill_contract(self) -> None: self.assertEqual(errors, []) + def test_consumer_setup_script(self) -> None: + """Accepts the executable deterministic consumer validator.""" + errors: list[str] = [] + + VALIDATOR.validate_consumer_setup_script(errors) + + self.assertEqual(errors, []) + if __name__ == "__main__": unittest.main() diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..32786aa --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.18 From d4a030be4665519514a6fe5a832c5ec78313353d Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Tue, 18 Aug 2026 23:42:35 +0200 Subject: [PATCH 2/4] Adopt agent-guidelines 0.0.18 consumer wiring --- .agents/skills/agent-guidelines-audit | 1 + .editorconfig | 1 + .swift-format | 1 + AGENTS.md | 23 +++++++++++++++++++++++ 4 files changed, 26 insertions(+) create mode 120000 .agents/skills/agent-guidelines-audit create mode 120000 .editorconfig create mode 120000 .swift-format diff --git a/.agents/skills/agent-guidelines-audit b/.agents/skills/agent-guidelines-audit new file mode 120000 index 0000000..9e33ff8 --- /dev/null +++ b/.agents/skills/agent-guidelines-audit @@ -0,0 +1 @@ +../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 120000 index 0000000..1e825fd --- /dev/null +++ b/.editorconfig @@ -0,0 +1 @@ +AgentGuidelines/Configurations/Swift/.editorconfig \ No newline at end of file diff --git a/.swift-format b/.swift-format new file mode 120000 index 0000000..06f3229 --- /dev/null +++ b/.swift-format @@ -0,0 +1 @@ +AgentGuidelines/Configurations/Swift/.swift-format \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 19d9265..2516863 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ The package is content-, storage-, UI-, and application-architecture agnostic. H 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) - [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) @@ -17,6 +18,7 @@ Read only the guides relevant to the task: - [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) @@ -25,6 +27,22 @@ Read only the guides relevant to the task: Redux, SwiftUI, and application-localization guidance do not apply to the package target. + +## Code Review Rules + +Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. + +A blocking finding must identify a concrete, reachable path in a supported use case or the documented threat model that can cause a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. + +For every blocking finding, state the severity, preconditions, execution path, impact, evidence, and actionable remediation. Group manifestations that share the same root cause into one finding. + +Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, behavior by components outside the threat model, style preferences, and speculative refactoring. Record a useful lower-severity observation once as deferred, declined, duplicate, or follow-up work; do not keep the review loop open for it. + +In an initial review, report substantiated blockers together. A follow-up review is limited to unresolved P0/P1 findings, changes since the last reviewed commit, and code directly affected by those changes. Do not restart an unrestricted review of unchanged code. A new follow-up finding must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +Automatic Codex review is the initial review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another review after each remediation commit. Within the normal review budget, at most one owner-authorized, delta-scoped verification review may be requested under [the pull-request review workflow](AgentGuidelines/Guidelines/GitHub/PullRequests.md). + + ## Physical folder map | Role | Physical folder | @@ -33,6 +51,11 @@ Redux, SwiftUI, and application-localization guidance do not apply to the packag | DocC catalog | `Sources/ProgressionKit/ProgressionKit.docc/` | | Unit tests | `Tests/ProgressionKitTests/` | +## CI/CD + +- Swift package CI uses the self-hosted runner labels `self-hosted` and `macOS`. +- Pull-request validation runs from `.github/workflows/ci-pr.yml`; protected-branch validation runs from `.github/workflows/ci.yml`. + ## Package specialization - Keep progression updates deterministic for the same profile, event, and configuration. From 2c62df219767fd21fb58bc529d120eca771da179 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Tue, 18 Aug 2026 23:42:49 +0200 Subject: [PATCH 3/4] Enforce strict Swift formatting in CI --- .github/workflows/ci-pr.yml | 45 +++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 17 +++++++++++-- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 4 ++-- 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci-pr.yml diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 0000000..7ed9b55 --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,45 @@ +--- +name: Pull Request CI + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format: + name: Swift Format + runs-on: [self-hosted, macOS] + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Lint Swift Sources + run: | + AgentGuidelines/Scripts/swift_format.sh lint-strict \ + Package.swift \ + Sources \ + Tests + + test: + name: Test + runs-on: [self-hosted, macOS] + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + clean: true + + - name: Clear SwiftPM Caches + run: | + rm -rf ~/.swiftpm + rm -rf ~/Library/Caches/org.swift.swiftpm + rm -rf ~/Library/org.swift.swiftpm + rm -rf .swiftpm + rm -rf .build + + - name: Run Tests + run: swift test -v diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd6d684..3b68c6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,19 +5,32 @@ on: push: branches: - main - pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + format: + name: Swift Format + runs-on: [self-hosted, macOS] + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Lint Swift Sources + run: | + AgentGuidelines/Scripts/swift_format.sh lint-strict \ + Package.swift \ + Sources \ + Tests + test: name: Test runs-on: [self-hosted, macOS] steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: clean: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5b67da9..df1de94 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -15,7 +15,7 @@ jobs: runs-on: [self-hosted, macOS] steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: clean: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c84201..0d98226 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: runs-on: [self-hosted, macOS] steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: clean: true @@ -43,7 +43,7 @@ jobs: needs: test steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: clean: true From ff3e38661f70d11f8bb1c2a562445bfce78f3203 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Tue, 18 Aug 2026 23:43:00 +0200 Subject: [PATCH 4/4] Apply shared Swift format to package sources --- Package.swift | 8 ++++---- Sources/ProgressionKit/PKEngine.swift | 8 ++++---- Tests/ProgressionKitTests/PKLoggingTests.swift | 1 + Tests/ProgressionKitTests/ProgressionKitTests.swift | 9 +++++---- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Package.swift b/Package.swift index 20faee8..cf26039 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( .iOS(.v26), .macOS(.v26), .tvOS(.v26), - .watchOS(.v26) + .watchOS(.v26), ], products: [ .library( @@ -18,7 +18,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/thatfactory/applogger", from: "1.1.0"), - .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0"), ], targets: [ .target( @@ -37,8 +37,8 @@ let package = Package( .product( name: "AppLogger", package: "applogger" - ) + ), ] - ) + ), ] ) diff --git a/Sources/ProgressionKit/PKEngine.swift b/Sources/ProgressionKit/PKEngine.swift index 0873954..67fdca3 100644 --- a/Sources/ProgressionKit/PKEngine.swift +++ b/Sources/ProgressionKit/PKEngine.swift @@ -64,8 +64,8 @@ public enum PKEngine { // MARK: - Private -private extension PKEngine { - static func defaultTrackProgress(config: PKConfig) -> PKTrackProgress { +extension PKEngine { + fileprivate static func defaultTrackProgress(config: PKConfig) -> PKTrackProgress { let firstTierID = config.tierOrder[0] return PKTrackProgress( @@ -73,7 +73,7 @@ private extension PKEngine { ) } - static func unlockNextTierIfNeeded( + fileprivate static func unlockNextTierIfNeeded( trackProgress: inout PKTrackProgress, tierID: String, config: PKConfig @@ -96,7 +96,7 @@ private extension PKEngine { return [nextTierID] } - static func makeUpdate( + fileprivate static func makeUpdate( profile: PKProfile, config: PKConfig, didGrantXP: Bool, diff --git a/Tests/ProgressionKitTests/PKLoggingTests.swift b/Tests/ProgressionKitTests/PKLoggingTests.swift index ef2945f..68c4778 100644 --- a/Tests/ProgressionKitTests/PKLoggingTests.swift +++ b/Tests/ProgressionKitTests/PKLoggingTests.swift @@ -1,6 +1,7 @@ import AppLogger import Foundation import Testing + @testable import ProgressionKit @Suite struct PKLoggingTests { diff --git a/Tests/ProgressionKitTests/ProgressionKitTests.swift b/Tests/ProgressionKitTests/ProgressionKitTests.swift index ed99a38..8832c4b 100644 --- a/Tests/ProgressionKitTests/ProgressionKitTests.swift +++ b/Tests/ProgressionKitTests/ProgressionKitTests.swift @@ -1,4 +1,5 @@ import Testing + @testable import ProgressionKit @Test func grantsXPForFirstSuccessfulCompletion() { @@ -69,7 +70,7 @@ import Testing @Test func unlocksIntermediateAfterDistinctBeginnerMastery() { // Given - let events = (1 ... 5).map { index in + let events = (1...5).map { index in PKEvent( contentID: "lesson-\(index)", trackID: "a11-reading", @@ -104,7 +105,7 @@ import Testing @Test func unlocksAdvancedAfterDistinctIntermediateMastery() { // Given - let beginnerEvents = (1 ... 5).map { index in + let beginnerEvents = (1...5).map { index in PKEvent( contentID: "beginner-\(index)", trackID: "a11-writing", @@ -112,7 +113,7 @@ import Testing wasSuccessful: true ) } - let intermediateEvents = (1 ... 5).map { index in + let intermediateEvents = (1...5).map { index in PKEvent( contentID: "intermediate-\(index)", trackID: "a11-writing", @@ -160,7 +161,7 @@ import Testing tierOrder: ["beginner", "intermediate", "advanced"], masteryRequirement: 5 ) - let events = (1 ... 4).map { index in + let events = (1...4).map { index in PKEvent( contentID: "lesson-\(index)", trackID: "a11-listening",