diff --git a/AGENTS.md b/AGENTS.md index 68dcb0baf..a9a357631 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ Use this file for durable repo-local guidance that Codex should follow before ch - Work directly on local `main` only when Gale explicitly asks for a direct-main workflow, when the task is a read-only investigation, or when a repo-owned release helper explicitly owns the direct-main release operation. - Before editing from a detached Codex GUI checkout, create or switch to a named branch in that worktree so the branch/worktree owner is explicit. - When a commit is made in `socket`, push the current branch as the normal checkpoint unless Gale asks for local-only work or the branch is intentionally incomplete. Pushing the branch does not imply opening a PR, waiting on CI, tagging, or releasing. +- On Gale-managed machines, expect `fetch.prune=true`, `pull.ff=only`, and `branch.autoSetupRebase=always` as global Git defaults. Inspect effective configuration before depending on them, surface any repository-specific override, and never install these machine preferences as repository-local configuration from a template, bootstrap, or sync workflow. - When an agent reaches a remote CI, review, release, deployment, or provider gate that will not resolve in the current command, it must schedule a host-native continuation at least five minutes later instead of holding a shell open or polling. Before creating one, find a live continuation for the same gate and reuse that same scheduler item while the gate remains pending and healthy; do not delete and recreate it after each unchanged snapshot. Pause or delete it only when the gate clears, fails, is cancelled, or its identity changes. Use a same-thread heartbeat in Codex desktop/ChatGPT when available; in Hermes, create or update the same self-contained `cronjob` delivered to the origin with `attach_to_session=true` after confirming its scheduler is active. A single sub-minute re-read is allowed only as a bounded part of the mutation that caused it, never as repeated polling. Use `maintain-project-repo` for the detailed release continuation contract. - Prefer small, focused commits over broad mixed changes. - For ordinary fixes in monorepo-owned child directories, edit the relevant copy under `plugins/` directly in `socket`. diff --git a/README.md b/README.md index 74c6e7b45..d21b8c5ce 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ Current Socket catalog shape: - `network-protocol-skills`: modern networking and application-protocol workflows for transport selection, HTTP/3 and QUIC planning, Media over QUIC draft-aware guidance, WebRTC signaling/media/data-channel work, and protocol diagnostics with stack-plugin handoffs - `professional-skills`: career and professional workflow guidance, starting with Dice job search and its bundled read-only remote MCP configuration - `python-skills`: Python runtime and tooling workflows, including local-first agent services, FastAPI and FastMCP service maintenance, and pytest-based testing; see the [Python skills expansion plan](./docs/maintainers/python-skills-plugin-plan.md) for maintainer details -- `repository-skills`: GitHub repository maintenance, release operations, and Codex GUI worktree workflow guidance +- `repository-skills`: routed Git and GitHub collaboration, repository settings, protected-main release, and Codex GUI worktree workflow guidance; its portable Git, GitHub, settings, release, and coordination workflows are available from the Hermes skill tap - `reverse-engineering-skills`: artifact triage, preservation, exact-build comparison, decompiler review, Apple Mach-O/runtime/signing/Apple Silicon/dyld/dynamic/kernel research, Cutter/Rizin, Malimite, Ghidra, Hopper, .NET, Unity and IL2CPP, and reproducible security evidence workflows - `server-side-jvm`: server-side JVM, Java, Scala, Google ADK agent-service, Gradle, Maven, SBT, and testing workflow guidance, with future Clojure support planned - `server-side-swift`: server-side Swift bootstrap and guidance sync, Vapor, Leaf-rendered websites and HTML email, Hummingbird, persistence, OpenAPI/RPC, SwiftNIO, observability, auth, app sync, Docker, Apple `container` 1.x, persistent `container machine` environments, exact-version Containerization APIs, and Fly.io support plus the source-bundled `server-swift-steward` custom-agent definition diff --git a/ROADMAP.md b/ROADMAP.md index 443cb75fb..a2416c08b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1118,6 +1118,10 @@ Planned - [x] Audit and update the default and recommended GitHub repository settings in relevant repo-maintenance, skill-repo, and Apple bootstrap/sync workflows. - [x] Add `repository-skills:maintain-github-repository` as the dedicated owner for GitHub repository settings audits and requested alignment, while keeping release and publish choreography in `maintain-project-repo`. - [x] GitHub #81: Strengthen `maintain-project-repo` release, publish, tag, protected-main, cleanup, and branch-accounting triggers while routing settings-only requests to `maintain-github-repository` ([#81](https://github.com/gaelic-ghost/socket/issues/81)). +- [x] Add the `xcode-workspace` `maintain-project-repo` profile for modular Apple roots with workspace-shape validation and serial component-maintenance dispatch. +- [x] Add the `repository-operations-workflow` router plus focused Git and GitHub collaboration workflows, and export their portable guidance with the host-neutral coordination contracts to Hermes. +- [x] Record Gale's expected machine-level Git defaults in the portable Git workflow: prune stale fetch refs, refuse pull-created merge commits, and configure new tracking branches to rebase; inspect overrides and never write these defaults as repository-local template configuration. +- [ ] Design a read-first native Hermes `git-steward` only after repeated use proves that a registered runtime role, slash command, or dedicated tool surface is needed. Keep pushes, merges, tags, deletion, settings changes, and publication explicitly authority-gated. - [x] Migrated the remaining root `TODO.md` backlog into canonical `ROADMAP.md` milestones, Small Tickets, or Backlog Candidates, then removed `TODO.md` in a reviewed documentation pass. - [ ] Investigate `uv audit` as a Socket validation and release-evidence input. Decide whether the first adoption belongs in root validation, Python-backed child plugin validation, release evidence capture, or reusable Python project-maintenance guidance, and test the `--locked`, `--frozen`, JSON output, ignore, and OSV service options before making it a required gate. - [ ] Explore an `ErrorHandles` Swift helper package for consistent, concise error construction, diagnostic context, typed wrapping, and recovery helpers. Use [`docs/maintainers/errorhandles-package-plan.md`](./docs/maintainers/errorhandles-package-plan.md) to keep the Swift package source outside Socket while keeping skills and adoption guidance in `plugins/swift-lang`. diff --git a/docs/maintainers/hermes-compatibility.md b/docs/maintainers/hermes-compatibility.md index 7785b5770..9a05c6522 100644 --- a/docs/maintainers/hermes-compatibility.md +++ b/docs/maintainers/hermes-compatibility.md @@ -75,6 +75,14 @@ Socket tap. The curated set is: - `fastapi-service-workflow` - `fastmcp-service-workflow` - `python-testing-workflow` +- `coordinate-external-agents` +- `coordinate-worktrees-and-threads` +- `orchestrate-agent-work` +- `repository-operations-workflow` +- `git-workflow` +- `github-collaboration-workflow` +- `maintain-github-repository` +- `maintain-project-repo` ```bash hermes skills tap add gaelic-ghost/socket @@ -150,6 +158,21 @@ agent services, FastAPI services, FastMCP services, and tests while relying on operator-installed Python tooling and project dependencies. They package no MCP server, credentials, hook, app, or native Hermes runtime surface. +The exported repository workflows are portable guidance. They use the +operator's `git` and, for GitHub work, authenticated `gh` CLI. They distinguish +read-only inspection from pushing, merging, tagging, deletion, settings +changes, publishing, and release actions; those mutations still need explicit +authority or a repository-owned release contract. The host-neutral coordinator +workflows accompany them so a Hermes agent can record worktree, branch, write, +validation, and handoff ownership. `codex-gui-worktree-workflow` remains +Codex-specific because its thread association and managed-worktree state have +no Hermes equivalent. + +A named autonomous `git-steward` remains a future native Hermes-plugin +decision. The current tap provides reusable workflow instructions, not a +registered runtime role, slash command, credential store, or additional GitHub +authority. + The exported macOS platform-security workflows are portable instruction contracts. Public app permission, sandbox file-access, and entitlement diagnosis remain in Apple Dev Skills; exact-build private-control research remains in Reverse Engineering Skills; defensive host investigation remains in Cybersecurity Skills. Hermes does not receive a TCC database tool, permission grant service, privileged helper, protection bypass, or native runtime plugin from this work. ## Maintainer Workflow diff --git a/plugins/agent-engineering-skills/.codex-plugin/plugin.json b/plugins/agent-engineering-skills/.codex-plugin/plugin.json index bbdd96473..123174666 100644 --- a/plugins/agent-engineering-skills/.codex-plugin/plugin.json +++ b/plugins/agent-engineering-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-engineering-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Agent-system design, orchestration, scheduling, external-agent, and evaluation workflow skills.", "author": { "name": "Gale", diff --git a/plugins/agent-engineering-skills/pyproject.toml b/plugins/agent-engineering-skills/pyproject.toml index d66c545f0..41f1e78d8 100644 --- a/plugins/agent-engineering-skills/pyproject.toml +++ b/plugins/agent-engineering-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-engineering-skills-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Maintainer-only Python tooling baseline for agent-engineering-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/agent-engineering-skills/skills/coordinate-worktrees-and-threads/SKILL.md b/plugins/agent-engineering-skills/skills/coordinate-worktrees-and-threads/SKILL.md index 42dc45a9e..7f78d6a79 100644 --- a/plugins/agent-engineering-skills/skills/coordinate-worktrees-and-threads/SKILL.md +++ b/plugins/agent-engineering-skills/skills/coordinate-worktrees-and-threads/SKILL.md @@ -1,6 +1,6 @@ --- name: coordinate-worktrees-and-threads -description: Assign worktree, branch, thread, artifact, write, validation, integration, and cleanup ownership before parallel repository work. Use when launching or resuming a worker that will inspect or modify repository state outside the coordinator's single worktree. +description: Assign worktree, branch, write, validation, integration, and cleanup ownership before parallel repository work. Use when a worker will inspect or modify repository state outside the coordinator's worktree. --- # Coordinate Worktrees And Threads @@ -18,6 +18,10 @@ cleanup decision. ## Rules - Give parallel writers disjoint files or directories and separate worktrees. +- When a Codex-managed worktree versus a plain Git worktree is itself a + decision, hand that host-specific choice to + `repository-skills:codex-gui-worktree-workflow`; retain this skill's + host-neutral ownership record in either case. - Serialize Git operations that change shared repository state. A worker must not assume it owns tags, merges, release branches, deletion, or cleanup. - Serialize heavy build and test lanes unless the environment explicitly proves diff --git a/plugins/agent-engineering-skills/skills/orchestrate-agent-work/SKILL.md b/plugins/agent-engineering-skills/skills/orchestrate-agent-work/SKILL.md index d8d9f776a..3a2899ff7 100644 --- a/plugins/agent-engineering-skills/skills/orchestrate-agent-work/SKILL.md +++ b/plugins/agent-engineering-skills/skills/orchestrate-agent-work/SKILL.md @@ -1,6 +1,6 @@ --- name: orchestrate-agent-work -description: Coordinate bounded work across in-run subagents or worker tasks. Use before spawning, resuming, steering, cancelling, or closing workers when the coordinator needs a structured launch envelope, required report-back, escalation, and synthesis contract. +description: Coordinate bounded worker tasks with a launch envelope, report-back, escalation, and synthesis contract. Use before spawning, resuming, steering, cancelling, or closing subagents. --- # Orchestrate Agent Work @@ -46,3 +46,7 @@ authority, ownership, evidence, or acceptance criteria change. - Use `coordinate-external-agents` when work crosses a host, session, or durable ownership boundary. - Use `coordinate-worktrees-and-threads` when workers need repository state. + When the selected host is Codex and the worktree mode, local environment, or + app-owned thread association needs a decision, compose it with + `repository-skills:codex-gui-worktree-workflow`. On Hermes or another host, + keep the ownership contract host-neutral and use ordinary Git worktrees. diff --git a/plugins/agent-engineering-skills/uv.lock b/plugins/agent-engineering-skills/uv.lock index 152125509..5f2f704d5 100644 --- a/plugins/agent-engineering-skills/uv.lock +++ b/plugins/agent-engineering-skills/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "agent-engineering-skills-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/agent-portability-skills/.codex-plugin/plugin.json b/plugins/agent-portability-skills/.codex-plugin/plugin.json index c65714a4f..3b5bc9f87 100644 --- a/plugins/agent-portability-skills/.codex-plugin/plugin.json +++ b/plugins/agent-portability-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-portability-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Skills for agent portability, ACP, A2A, Zed, Hermes, Codex plugin surfaces, and host adapter guidance.", "author": { "name": "Gale", diff --git a/plugins/agent-portability-skills/pyproject.toml b/plugins/agent-portability-skills/pyproject.toml index 179128148..b41aba522 100644 --- a/plugins/agent-portability-skills/pyproject.toml +++ b/plugins/agent-portability-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-portability-skills-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Maintainer-only Python tooling baseline for Agent Portability Skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/agent-portability-skills/uv.lock b/plugins/agent-portability-skills/uv.lock index 503b7b457..f1126b769 100644 --- a/plugins/agent-portability-skills/uv.lock +++ b/plugins/agent-portability-skills/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agent-portability-skills-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/agentdeck/.codex-plugin/plugin.json b/plugins/agentdeck/.codex-plugin/plugin.json index 3108fc012..f2bb2f0d8 100644 --- a/plugins/agentdeck/.codex-plugin/plugin.json +++ b/plugins/agentdeck/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentdeck", - "version": "9.31.2", + "version": "9.32.0", "description": "Local Codex runtime utilities for thread, hook, and app-server workflows.", "author": { "name": "Gale", diff --git a/plugins/android-dev-skills/.codex-plugin/plugin.json b/plugins/android-dev-skills/.codex-plugin/plugin.json index 7e8b51278..525f8d213 100644 --- a/plugins/android-dev-skills/.codex-plugin/plugin.json +++ b/plugins/android-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "android-dev-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Android, Kotlin, Java, Gradle, Android Gradle Plugin, testing, lint, UI implementation, and release-readiness workflow skills.", "author": { "name": "Gale", diff --git a/plugins/apple-creator-studio-skills/.codex-plugin/plugin.json b/plugins/apple-creator-studio-skills/.codex-plugin/plugin.json index c1361aaf0..665d33fbd 100644 --- a/plugins/apple-creator-studio-skills/.codex-plugin/plugin.json +++ b/plugins/apple-creator-studio-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "apple-creator-studio-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Human-facing and Computer Use-aware Apple Creator Studio workflows for Final Cut Pro editing, Motion templates, Compressor delivery, Logic Pro production, MainStage concert preparation, and GarageBand projects.", "author": { "name": "Gale", diff --git a/plugins/apple-dev-skills/.codex-plugin/plugin.json b/plugins/apple-dev-skills/.codex-plugin/plugin.json index cb8c57045..408b3da84 100644 --- a/plugins/apple-dev-skills/.codex-plugin/plugin.json +++ b/plugins/apple-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "apple-dev-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Apple development workflows for Codex, including macOS privacy permissions, sandbox file access, entitlement diagnosis, virtualization, SwiftPM, Xcode, app extensions, media, provisioning, SwiftUI, AppKit, Safari, security, OpenAPI, and DocC.", "author": { "name": "Gale", diff --git a/plugins/apple-dev-skills/pyproject.toml b/plugins/apple-dev-skills/pyproject.toml index ed51b5276..062cf57e3 100644 --- a/plugins/apple-dev-skills/pyproject.toml +++ b/plugins/apple-dev-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apple-dev-skills-maintainer" -version = "9.31.2" +version = "9.32.0" description = "Maintainer tooling for the apple-dev-skills repository" requires-python = ">=3.10" dependencies = [] diff --git a/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/SKILL.md b/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/SKILL.md index 880572ae9..fc13b1d0b 100644 --- a/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/SKILL.md +++ b/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/SKILL.md @@ -30,7 +30,12 @@ guidance section when needed. 6. Route app-specific guidance to `sync-xcode-project-guidance`, package-specific guidance to `sync-swift-package-guidance`, and active execution to `xcode-build-run-workflow` or `xcode-testing-workflow`. -7. Keep optional services under `Services/` as their own deployment and runtime +7. When the workspace root needs shared validation, release, or GitHub Actions + maintenance, compose it with `repository-skills:maintain-project-repo` using + the `xcode-workspace` profile. Its root dispatcher validates workspace shape + and runs any component-owned maintenance entrypoints serially; it does not + replace app, package, or service-local build policy. +8. Keep optional services under `Services/` as their own deployment and runtime boundary. Use the selected server skill for service-local policy. ## Audit Contract diff --git a/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/assets/append-section.md b/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/assets/append-section.md index 4a393cae2..2871e6b57 100644 --- a/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/assets/append-section.md +++ b/plugins/apple-dev-skills/skills/sync-xcode-workspace-guidance/assets/append-section.md @@ -15,3 +15,8 @@ - Use `sync-xcode-project-guidance` for an app project, `sync-swift-package-guidance` for a package, and `xcode-build-run-workflow` or `xcode-testing-workflow` for active Xcode work. +- Use `repository-skills:maintain-project-repo` with the `xcode-workspace` + profile when the workspace root needs shared validation, release, or GitHub + Actions maintenance. It validates the root composition and serially delegates + to component-owned maintenance entrypoints; it does not replace app, package, + or service-local build policy. diff --git a/plugins/apple-dev-skills/uv.lock b/plugins/apple-dev-skills/uv.lock index 0ccf912cd..14a1731da 100644 --- a/plugins/apple-dev-skills/uv.lock +++ b/plugins/apple-dev-skills/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "apple-dev-skills-maintainer" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/cardhop-app/.codex-plugin/plugin.json b/plugins/cardhop-app/.codex-plugin/plugin.json index c7955869c..c8c9f96c1 100644 --- a/plugins/cardhop-app/.codex-plugin/plugin.json +++ b/plugins/cardhop-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cardhop-app", - "version": "9.31.2", + "version": "9.32.0", "description": "Cardhop.app workflow guidance plus a bundled local MCP server for contact capture and updates on macOS.", "author": { "name": "Gale", diff --git a/plugins/cardhop-app/mcp/pyproject.toml b/plugins/cardhop-app/mcp/pyproject.toml index 573ac51f1..7562bc658 100644 --- a/plugins/cardhop-app/mcp/pyproject.toml +++ b/plugins/cardhop-app/mcp/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cardhop-app-mcp" -version = "9.31.2" +version = "9.32.0" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/cardhop-app/mcp/uv.lock b/plugins/cardhop-app/mcp/uv.lock index 563f73a9b..c21b189f0 100644 --- a/plugins/cardhop-app/mcp/uv.lock +++ b/plugins/cardhop-app/mcp/uv.lock @@ -141,7 +141,7 @@ wheels = [ [[package]] name = "cardhop-app-mcp" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/cloud-deployment-skills/.codex-plugin/plugin.json b/plugins/cloud-deployment-skills/.codex-plugin/plugin.json index 1b15852ba..3b6e261fe 100644 --- a/plugins/cloud-deployment-skills/.codex-plugin/plugin.json +++ b/plugins/cloud-deployment-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cloud-deployment-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for routing cloud deployment work through official provider plugins, MCP servers, CLIs, and a reusable Dockerized-service release and deployment contract.", "author": { "name": "Gale", diff --git a/plugins/cloud-inference-skills/.codex-plugin/plugin.json b/plugins/cloud-inference-skills/.codex-plugin/plugin.json index 420465281..e568c74aa 100644 --- a/plugins/cloud-inference-skills/.codex-plugin/plugin.json +++ b/plugins/cloud-inference-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cloud-inference-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Cloud AI inference workflow skills for routing model serving, training, conversion, and GPU infrastructure work across Runpod, Hugging Face, AWS, Vast.ai, CoreWeave, and similar providers.", "author": { "name": "Gale", diff --git a/plugins/codebase-understanding-skills/.codex-plugin/plugin.json b/plugins/codebase-understanding-skills/.codex-plugin/plugin.json index dcda113ca..2d327c615 100644 --- a/plugins/codebase-understanding-skills/.codex-plugin/plugin.json +++ b/plugins/codebase-understanding-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codebase-understanding-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Code-path exploration, call-site tracing, and evidence-based codebase explanation skills.", "author": { "name": "Gale", diff --git a/plugins/cybersecurity-skills/.codex-plugin/plugin.json b/plugins/cybersecurity-skills/.codex-plugin/plugin.json index f449e0127..94cf8be18 100644 --- a/plugins/cybersecurity-skills/.codex-plugin/plugin.json +++ b/plugins/cybersecurity-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cybersecurity-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Defensive cybersecurity, isolated Linux and macOS analysis labs, suspicious-content and malware analysis, macOS defense, vulnerability testing, pentesting, and incident response workflows.", "skills": "./skills/", "author": { diff --git a/plugins/documentation-skills/.codex-plugin/plugin.json b/plugins/documentation-skills/.codex-plugin/plugin.json index 3429f4f11..0c6511863 100644 --- a/plugins/documentation-skills/.codex-plugin/plugin.json +++ b/plugins/documentation-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "documentation-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Documentation maintenance skills for README, contributor, agent, API, accessibility, architecture, and roadmap surfaces.", "author": { "name": "Gale", diff --git a/plugins/documentation-skills/pyproject.toml b/plugins/documentation-skills/pyproject.toml index 3666e1a41..7b68ce08c 100644 --- a/plugins/documentation-skills/pyproject.toml +++ b/plugins/documentation-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "documentation-skills-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Maintainer-only Python tooling baseline for documentation-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/documentation-skills/uv.lock b/plugins/documentation-skills/uv.lock index cac7c9671..d0d1ae8cd 100644 --- a/plugins/documentation-skills/uv.lock +++ b/plugins/documentation-skills/uv.lock @@ -57,7 +57,7 @@ wheels = [ [[package]] name = "documentation-skills-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/dotnet-skills/.codex-plugin/plugin.json b/plugins/dotnet-skills/.codex-plugin/plugin.json index b82500675..00ee357fb 100644 --- a/plugins/dotnet-skills/.codex-plugin/plugin.json +++ b/plugins/dotnet-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dotnet-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for choosing, bootstrapping, building, testing, packaging, diagnosing, and maintaining .NET projects, including F# web applications, with F# and C# as equal first-party languages.", "author": { "name": "Gale", diff --git a/plugins/game-dev-skills/.codex-plugin/plugin.json b/plugins/game-dev-skills/.codex-plugin/plugin.json index 9c48e6911..4fbfe4070 100644 --- a/plugins/game-dev-skills/.codex-plugin/plugin.json +++ b/plugins/game-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "game-dev-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Apple platform game development workflow skills for native Metal rendering, Game Porting Toolkit routing, MetalFX, GPU asset streaming, neural rendering, frameworks, input, haptics, and profiling.", "author": { "name": "Gale", diff --git a/plugins/messaging-collaboration-skills/.codex-plugin/plugin.json b/plugins/messaging-collaboration-skills/.codex-plugin/plugin.json index 050958fe6..d384f70b9 100644 --- a/plugins/messaging-collaboration-skills/.codex-plugin/plugin.json +++ b/plugins/messaging-collaboration-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "messaging-collaboration-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex workflows for chat apps, bots, collaboration, iMessage, Apple notifications and Push to Talk, VoIP, and default communication-app planning.", "author": { "name": "Gale", diff --git a/plugins/model-lab-skills/.codex-plugin/plugin.json b/plugins/model-lab-skills/.codex-plugin/plugin.json index 0741b10d9..46b1c950e 100644 --- a/plugins/model-lab-skills/.codex-plugin/plugin.json +++ b/plugins/model-lab-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "model-lab-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Reproducible model training, evaluation, intervention, and runtime research workflows.", "author": { "name": "Gale" diff --git a/plugins/network-protocol-skills/.codex-plugin/plugin.json b/plugins/network-protocol-skills/.codex-plugin/plugin.json index 33967a76c..7f4d05823 100644 --- a/plugins/network-protocol-skills/.codex-plugin/plugin.json +++ b/plugins/network-protocol-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "network-protocol-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for choosing, planning, implementing, and diagnosing modern application transports and real-time networking protocols, including QUIC, HTTP/3, WebRTC, Media over QUIC, WebTransport-adjacent handoffs, protocol maturity checks, and stack-specific implementation routing.", "author": { "name": "Gale", diff --git a/plugins/professional-skills/.codex-plugin/plugin.json b/plugins/professional-skills/.codex-plugin/plugin.json index 744b383f4..7929e5948 100644 --- a/plugins/professional-skills/.codex-plugin/plugin.json +++ b/plugins/professional-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "professional-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Professional workflow skills for job search, career materials, and external professional services.", "author": { "name": "Gale", diff --git a/plugins/professional-skills/pyproject.toml b/plugins/professional-skills/pyproject.toml index cce7c5b6e..89a5f7593 100644 --- a/plugins/professional-skills/pyproject.toml +++ b/plugins/professional-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "professional-skills-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Maintainer-only Python tooling baseline for professional-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/professional-skills/uv.lock b/plugins/professional-skills/uv.lock index aba20bd35..3a86f7182 100644 --- a/plugins/professional-skills/uv.lock +++ b/plugins/professional-skills/uv.lock @@ -40,7 +40,7 @@ wheels = [ [[package]] name = "professional-skills-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/python-skills/.codex-plugin/plugin.json b/plugins/python-skills/.codex-plugin/plugin.json index 17d14508f..77c610ab4 100644 --- a/plugins/python-skills/.codex-plugin/plugin.json +++ b/plugins/python-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "python-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Bundled Python-focused Codex skills for uv bootstrapping, project implementation, diagnostics, packaging, tooling, CI, upgrades, FastAPI service maintenance, FastMCP service maintenance, and testing workflows.", "author": { "name": "Gale", diff --git a/plugins/python-skills/pyproject.toml b/plugins/python-skills/pyproject.toml index e740d1b12..8a95a42c3 100644 --- a/plugins/python-skills/pyproject.toml +++ b/plugins/python-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-skills-maintainer" -version = "9.31.2" +version = "9.32.0" description = "Maintainer tooling for the python-skills repository" requires-python = ">=3.11" dependencies = [] diff --git a/plugins/python-skills/uv.lock b/plugins/python-skills/uv.lock index b756de26f..da90d39e1 100644 --- a/plugins/python-skills/uv.lock +++ b/plugins/python-skills/uv.lock @@ -251,7 +251,7 @@ wheels = [ [[package]] name = "python-skills-maintainer" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/repository-skills/.codex-plugin/plugin.json b/plugins/repository-skills/.codex-plugin/plugin.json index c107b86fd..c5feba7f2 100644 --- a/plugins/repository-skills/.codex-plugin/plugin.json +++ b/plugins/repository-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "repository-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Repository operations, GitHub settings, worktree, validation, and release workflow skills.", "author": { "name": "Gale", diff --git a/plugins/repository-skills/pyproject.toml b/plugins/repository-skills/pyproject.toml index cecd28865..72a806576 100644 --- a/plugins/repository-skills/pyproject.toml +++ b/plugins/repository-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "repository-skills-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Maintainer-only Python tooling baseline for repository-skills." requires-python = ">=3.11" dependencies = [] @@ -9,4 +9,4 @@ dependencies = [] dev = ["pytest>=9.0.0", "mypy>=1.20.1", "PyYAML>=6.0.0", "types-PyYAML>=6.0.12.20250915"] [tool.pytest.ini_options] -testpaths = ["skills/maintain-github-repository/tests", "skills/maintain-project-repo/tests"] +testpaths = ["skills/maintain-github-repository/tests", "skills/maintain-project-repo/tests", "skills/repository-operations-workflow/tests"] diff --git a/plugins/repository-skills/skills/git-workflow/SKILL.md b/plugins/repository-skills/skills/git-workflow/SKILL.md new file mode 100644 index 000000000..817bbe56e --- /dev/null +++ b/plugins/repository-skills/skills/git-workflow/SKILL.md @@ -0,0 +1,61 @@ +--- +name: git-workflow +description: "Safely inspect and perform everyday Git work: branches, focused commits, history, integration, conflicts, worktrees, and recovery. Use for local version-control tasks that are not GitHub settings or release publication." +--- + +# Git Workflow + +## Purpose + +Handle ordinary local Git work with enough evidence to preserve uncommitted +work, branch reachability, and worktree ownership. This workflow does not own +GitHub settings or protected-main releases. + +## Workflow + +1. Read the closest `AGENTS.md`; inspect `git status --short --branch`, + `git worktree list`, and the relevant history/diff before a mutation. +2. Inspect the effective Git configuration before an operation depends on fetch, + pull, or tracking-branch behavior. On Gale-managed machines, expect + `fetch.prune=true`, `pull.ff=only`, and `branch.autoSetupRebase=always`; + report any local or worktree override rather than silently changing it. See + `references/gale-git-baseline.md`. +3. Classify the operation: + - inspection: status, diff, log, blame, or reachability; + - focused change: create/switch a branch, stage intentional files, commit; + - integration: fetch, compare, rebase or merge, resolve conflicts; + - recovery: reflog, lost commit investigation, or safe restoration plan. +4. Preserve the current work before an operation that rewrites, discards, or + moves it. Explain the exact target and recovery path before using reset, + clean, rebase, force push, or branch/worktree deletion. +5. For branch work, use a feature branch and separate worktree when required by + repository guidance. Never keep the same branch live in two worktrees except + as a short recovery step. +6. Make focused commits with the repository's required subject format. Review + staged changes and commit reachability after each shared Git mutation. +7. For integration, fetch first. The normal Gale baseline prunes stale refs and + refuses pull-created merge commits; newly tracking branches rebase on pull. + Choose a merge or rebase only when repository policy and the branch's + publication state justify it. Resolve each conflict from source intent, run + proportionate validation, and inspect the resulting diff. +8. Before deleting a branch, worktree, ref, or archive, verify reachability and + complete any repository-required branch accounting. + +## Boundaries + +- Route GitHub pull requests, reviews, issues, and CI collaboration to + `github-collaboration-workflow`. +- Route tags, publishing, GitHub releases, and protected-main release cleanup + to `maintain-project-repo`. +- Route GitHub settings, rulesets, and security configuration to + `maintain-github-repository`. +- Push, force push, merge, tag, and destructive recovery actions require clear + user authority or an existing repository-owned release contract. +- Do not encode Gale's machine-level Git baseline as repository-local config in + a generated template or existing repository. + +## Hermes Notes + +This is portable guidance. Verify that `git` is installed, the target is a Git +worktree, and the active identity/remotes are the intended ones. Hermes gains +no Git credentials or mutation authority from this skill. diff --git a/plugins/repository-skills/skills/git-workflow/agents/openai.yaml b/plugins/repository-skills/skills/git-workflow/agents/openai.yaml new file mode 100644 index 000000000..0b4f7bc24 --- /dev/null +++ b/plugins/repository-skills/skills/git-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Git Workflow" + short_description: "Safely inspect, integrate, and recover local Git work" + default_prompt: "Use $git-workflow to inspect this repository's Git state, choose a safe branch, commit, integration, conflict-resolution, or recovery path, and explain any required mutation authority before acting." diff --git a/plugins/repository-skills/skills/git-workflow/references/gale-git-baseline.md b/plugins/repository-skills/skills/git-workflow/references/gale-git-baseline.md new file mode 100644 index 000000000..a0b1b565b --- /dev/null +++ b/plugins/repository-skills/skills/git-workflow/references/gale-git-baseline.md @@ -0,0 +1,38 @@ +# Gale Git Baseline + +Gale-managed development machines normally provide these global Git defaults: + +```text +fetch.prune = true +pull.ff = only +branch.autoSetupRebase = always +``` + +They mean that ordinary fetches remove stale remote-tracking refs, ordinary +pulls refuse to create merge commits, and newly created tracking branches use +rebase when pulled. They are machine policy, not repository content. + +## Workflow Contract + +1. Before relying on the baseline, inspect effective values and their origins: + + ```bash + git config --show-origin --get-regexp '^(fetch\.prune|pull\.ff|branch\.autoSetupRebase)$' + ``` + +2. Treat a repository-local, worktree-local, command-line, or environment + override as an explicit repository policy. Report it before an operation + would behave differently from this baseline; do not silently remove it. +3. Keep explicit safety commands when their behavior must be invariant. For + example, a protected-main release may use `git pull --ff-only` even when the + global default already has that value. +4. Do not write these preferences with `git config --local` in bootstrap, + sync, or repository-maintenance templates. A generated repository must work + for contributors whose machine policy differs. + +## Scope + +This contract guides everyday Git work, worktree planning, repository +maintenance, and Git-initializing bootstrap workflows. It does not change a +repository's merge strategy, branch protection, remote permissions, or release +authority. diff --git a/plugins/repository-skills/skills/github-collaboration-workflow/SKILL.md b/plugins/repository-skills/skills/github-collaboration-workflow/SKILL.md new file mode 100644 index 000000000..aa575911e --- /dev/null +++ b/plugins/repository-skills/skills/github-collaboration-workflow/SKILL.md @@ -0,0 +1,51 @@ +--- +name: github-collaboration-workflow +description: Prepare and maintain GitHub pull requests, reviews, issues, CI triage, and durable handoffs. Use for collaboration work after local Git changes exist; not for GitHub settings or release publication. +--- + +# GitHub Collaboration Workflow + +## Purpose + +Own the collaboration layer around a change: pull requests, review and comment +triage, issue linkage, CI status, and durable handoffs. Keep local Git, +GitHub-settings, and release operations with their focused owners. + +## Workflow + +1. Confirm the exact repository, branch, base branch, current `git status`, + remote visibility, and `gh` authentication before a remote action. +2. Read repository contribution and review policy. Inspect existing pull + requests and issues before creating duplicates. +3. Prepare a PR only after the change is committed, validated proportionately, + and pushed with explicit authority. Use a concise body that explains intent, + risk, and verification; link the governing issue when one exists. +4. Inspect CI checks, reviews, and unresolved comments as separate gates. Name + failures, requested changes, and unknown states precisely. +5. Address valid review feedback in the owning branch; route broader follow-up + to the repository's roadmap or issue system. Re-read the changed thread and + checks before reporting it resolved. +6. For an external wait, do one bounded snapshot. Use the host's supported + continuation mechanism no sooner than five minutes later; do not keep a + shell watch or poll loop running. +7. Merge, label, close, reopen, or delete remote work only with explicit + authority or a repository-owned release contract. Verify the resulting + GitHub state after each mutation. + +## Boundaries + +- Route local branch, commit, rebase, conflict, and recovery work to + `git-workflow`. +- Route GitHub server settings, rulesets, and security configuration to + `maintain-github-repository`. +- Route protected-main release PRs, tags, releases, and branch-accounting + cleanup to `maintain-project-repo`. +- Keep credentials, private issue content, and security findings out of public + PR bodies and reports. + +## Hermes Notes + +This is portable guidance. Verify `gh` availability and authentication before +remote actions. On a pending CI or review gate, Hermes uses a self-contained +`cronjob` delivered to the origin session with `attach_to_session=true`; the +packet must include repository, branch, PR, commit, gate, and safe inspection. diff --git a/plugins/repository-skills/skills/github-collaboration-workflow/agents/openai.yaml b/plugins/repository-skills/skills/github-collaboration-workflow/agents/openai.yaml new file mode 100644 index 000000000..1234f4b1d --- /dev/null +++ b/plugins/repository-skills/skills/github-collaboration-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "GitHub Collaboration" + short_description: "Prepare PRs, triage reviews and CI, and coordinate handoffs" + default_prompt: "Use $github-collaboration-workflow to inspect this change's GitHub collaboration state, prepare or update a pull request when authorized, triage checks and review feedback, and preserve clear remote-mutation boundaries." diff --git a/plugins/repository-skills/skills/maintain-github-repository/SKILL.md b/plugins/repository-skills/skills/maintain-github-repository/SKILL.md index ec678dba5..08de857db 100644 --- a/plugins/repository-skills/skills/maintain-github-repository/SKILL.md +++ b/plugins/repository-skills/skills/maintain-github-repository/SKILL.md @@ -1,6 +1,6 @@ --- name: maintain-github-repository -description: Audit or align a GitHub repository's server-side settings, including repository features, merge methods, branch protection or rulesets, Dependabot, secret scanning, push protection, private vulnerability reporting, DCO web sign-off, and related defaults. Use when the user asks to configure a new GitHub repo, apply normal repository settings, inspect settings drift, audit branch protection or security settings, or change specific GitHub repository policy. Do not use for ordinary local Git commits, branches, pushes, pull-request prose, code review replies, or release choreography. +description: "Audit or align a GitHub repository's server-side settings, rulesets, security automation, Dependabot, and sign-off policy. Use for server-side GitHub policy work, not ordinary local Git commits, PR collaboration, or releases." --- # Maintain GitHub Repository diff --git a/plugins/repository-skills/skills/maintain-project-repo/SKILL.md b/plugins/repository-skills/skills/maintain-project-repo/SKILL.md index 69ae21a5a..5c3b90d48 100644 --- a/plugins/repository-skills/skills/maintain-project-repo/SKILL.md +++ b/plugins/repository-skills/skills/maintain-project-repo/SKILL.md @@ -1,6 +1,6 @@ --- name: maintain-project-repo -description: Install or refresh the profile-aware local-first maintain-project-repo toolkit, or execute its protected-main release workflow. Use when the user asks to install or refresh repo-owned validate, sync, and release scripts; release or publish a version; bump and tag a release; create the GitHub release; prepare or merge a protected-main release; or complete release cleanup and branch accounting. Do not use for ordinary edits, local Git work, documentation maintenance, or GitHub repository-settings audits. +description: Install or refresh validation, sync, and release tooling. Use to release or publish a version, bump and tag a release, create the GitHub release, prepare a protected-main release, or finish release cleanup and branch accounting. license: Apache-2.0 metadata: semver: 0.2.1 @@ -39,14 +39,19 @@ Install or refresh the reusable `maintain-project-repo` toolkit inside a general 2. Classify the repo and profile: - prefer `maintain-project-repo` for SwiftPM repos, Xcode app repos, mixed Apple repos, and general software repos that need local maintainer automation - choose `swift-package` for plain Swift package repos - - choose `xcode-app` for native Apple app repos + - choose `xcode-app` for one native Apple app project + - choose `xcode-workspace` for a modular Apple root with one `.xcworkspace`, `Apps/`, `Packages/`, and optional `Services/` - choose `generic` when no stronger Swift or Xcode profile applies - stop if the requested path is not a repository root - - use `scripts/repo-maintenance/` for `generic` and `swift-package`; use `Scripts/repo-maintenance/` for `xcode-app` so native app repos keep one standard top-level `Scripts/` directory + - use `scripts/repo-maintenance/` for `generic` and `swift-package`; use `Scripts/repo-maintenance/` for `xcode-app` and `xcode-workspace` so Apple repos keep one standard top-level `Scripts/` directory 3. Explain the architecture boundary before mutating anything: - this is a durable building-block change because it creates one repo-owned maintainer surface that bootstrap, sync, validation, CI, and release flows can all share - it removes the pain of CI-only helper scripts and scattered release glue - the simpler extension path considered first was leaving helper scripts under `.github/scripts/` and adding more workflow-specific wrappers, but that would keep local and CI behavior drifting apart + - preserve machine-level Git defaults such as Gale's fetch pruning, + fast-forward-only pulls, and tracking-branch rebases; the installer does + not write `git config --local` because generated repositories must remain + portable across contributor machines 4. Run `scripts/run_workflow.py` to normalize the inputs and choose the installer path. 5. Apply the managed `maintain-project-repo` files: - install or refresh the managed repo-maintenance files under the selected profile's toolkit root @@ -81,7 +86,7 @@ Install or refresh the reusable `maintain-project-repo` toolkit inside a general - `repo_root`: optional absolute or relative path to the repository root; defaults to `.` - `operation`: `install`, `refresh`, or `report-only` -- `profile`: `generic`, `swift-package`, or `xcode-app` +- `profile`: `generic`, `swift-package`, `xcode-app`, or `xcode-workspace` - `skip_github_workflow`: optional flag to skip `.github/workflows/validate-repo-maintenance.yml` - `dry_run`: optional flag to report the managed actions without writing files - Defaults: @@ -120,6 +125,9 @@ Install or refresh the reusable `maintain-project-repo` toolkit inside a general - `report-only` is the non-mutating fallback path. - The installer preserves repo-specific extra files under the selected profile's repo-maintenance root, `.github/workflows/`, and adjacent surfaces when they are not part of the managed file set. - The installer keeps the selected `maintain-project-repo` profile explicit via the selected profile's `config/profile.env`. +- The installer does not write repository-local Git defaults. Its release script + uses explicit `git pull --ff-only` where protected-main safety must not depend + on a caller's global configuration. - Apple profiles install checked-in `.swiftformat` and `.swiftlint.yml` samples so SwiftFormat owns formatting shape while SwiftLint stays focused on complementary safety and clarity checks. - The generated workflow's branch-protection check context is `validate`; GitHub exposes the job check run by that context, not by the workflow title plus job name. - The generated GitHub Actions wrapper uses Node 24-compatible Actions versions, with `actions/checkout@v6.0.2` as the current validated floor. Newer stable official action versions are allowed and often preferred after checking release notes and running the relevant validation. Apple profiles report the runner-selected Xcode with shell commands instead of using the Node 20-based `maxim-lobanov/setup-xcode@v1` action. diff --git a/plugins/repository-skills/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh b/plugins/repository-skills/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh new file mode 100644 index 000000000..946047d2e --- /dev/null +++ b/plugins/repository-skills/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +require_exactly_one_workspace() { + workspace_count=$(find "$REPO_ROOT" -maxdepth 1 -type d -name '*.xcworkspace' -print | wc -l | tr -d ' ') + [ "$workspace_count" -eq 1 ] || die "The xcode-workspace profile requires exactly one root .xcworkspace; found $workspace_count." +} + +require_apps_and_packages() { + [ -d "$REPO_ROOT/Apps" ] || die "The xcode-workspace profile requires Apps/ at the repository root." + [ -d "$REPO_ROOT/Packages" ] || die "The xcode-workspace profile requires Packages/ at the repository root." + + app_count=$(find "$REPO_ROOT/Apps" -type d -name '*.xcodeproj' -print | wc -l | tr -d ' ') + [ "$app_count" -gt 0 ] || die "The xcode-workspace profile requires at least one .xcodeproj under Apps/." + + package_count=$(find "$REPO_ROOT/Packages" -type f -name 'Package.swift' -print | wc -l | tr -d ' ') + [ "$package_count" -gt 0 ] || die "The xcode-workspace profile requires at least one Package.swift under Packages/." +} + +require_exactly_one_workspace +require_apps_and_packages +log "Validated xcode-workspace composition: one root workspace, Apps/, and Packages/." diff --git a/plugins/repository-skills/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh b/plugins/repository-skills/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh new file mode 100644 index 000000000..b5904d10a --- /dev/null +++ b/plugins/repository-skills/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh + +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$REPO_MAINTENANCE_COMMON_DIR/common.sh" + +run_component_validation() { + component_root=$1 + component_kind=$2 + for candidate in \ + "$component_root/Scripts/repo-maintenance/validate-all.sh" \ + "$component_root/scripts/repo-maintenance/validate-all.sh" + do + if [ -f "$candidate" ]; then + log "Validating $component_kind component at $component_root with ${candidate#"$component_root/"}." + sh "$candidate" + return 0 + fi + done + log "No component-owned repo-maintenance validation found for $component_kind at $component_root; skipping." +} + +find "$REPO_ROOT/Apps" -type d -name '*.xcodeproj' -print | sort | while IFS= read -r project; do + run_component_validation "$(dirname -- "$project")" "app" +done + +find "$REPO_ROOT/Packages" -type f -name 'Package.swift' -print | sort | while IFS= read -r manifest; do + run_component_validation "$(dirname -- "$manifest")" "package" +done + +if [ -d "$REPO_ROOT/Services" ]; then + find "$REPO_ROOT/Services" -mindepth 1 -maxdepth 1 -type d -print | sort | while IFS= read -r service; do + run_component_validation "$service" "service" + done +fi diff --git a/plugins/repository-skills/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh b/plugins/repository-skills/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh index 583696be8..fdc434748 100755 --- a/plugins/repository-skills/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh +++ b/plugins/repository-skills/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh @@ -10,4 +10,7 @@ load_env_file "$SELF_DIR/config/validation.env" ensure_git_repo log "Running repo-maintenance validation from $REPO_ROOT with the $REPO_MAINTENANCE_PROFILE profile." run_dispatch_dir "$SELF_DIR/validations" "validation" +if [ "$REPO_MAINTENANCE_PROFILE" = "xcode-workspace" ]; then + "$SELF_DIR/workspace/validate-components.sh" +fi log "Repo-maintenance validation completed successfully." diff --git a/plugins/repository-skills/skills/maintain-project-repo/scripts/install_maintain_project_repo.py b/plugins/repository-skills/skills/maintain-project-repo/scripts/install_maintain_project_repo.py index a1a934d7b..86e978c29 100755 --- a/plugins/repository-skills/skills/maintain-project-repo/scripts/install_maintain_project_repo.py +++ b/plugins/repository-skills/skills/maintain-project-repo/scripts/install_maintain_project_repo.py @@ -16,11 +16,13 @@ "generic": "Generic repo-maintenance baseline with no Swift or Xcode specialization.", "swift-package": "Swift Package Manager repo-maintenance profile for library, tool, and package repos.", "xcode-app": "Xcode app repo-maintenance profile for native Apple app repositories.", + "xcode-workspace": "Xcode workspace repo-maintenance profile for Apps, Packages, and optional Services roots.", } PROFILE_TOOLKIT_ROOTS = { "generic": Path("scripts/repo-maintenance"), "swift-package": Path("scripts/repo-maintenance"), "xcode-app": Path("Scripts/repo-maintenance"), + "xcode-workspace": Path("Scripts/repo-maintenance"), } PROFILE_OVERLAY_FILES = { "swift-package": [ @@ -39,10 +41,27 @@ "scripts/repo-maintenance/hooks/pre-commit.sample", ), ], + "xcode-workspace": [ + ("profiles/apple/repo-maintenance/.swiftformat", ".swiftformat"), + ("profiles/apple/repo-maintenance/.swiftlint.yml", ".swiftlint.yml"), + ( + "profiles/apple/repo-maintenance/hooks/pre-commit.sample", + "scripts/repo-maintenance/hooks/pre-commit.sample", + ), + ( + "profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh", + "scripts/repo-maintenance/validations/40-xcode-workspace-layout.sh", + ), + ( + "profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh", + "scripts/repo-maintenance/workspace/validate-components.sh", + ), + ], } PROFILE_WORKFLOW_FILES = { "swift-package": "profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", "xcode-app": "profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", + "xcode-workspace": "profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", } MANAGED_TOOLKIT_FILES = [ ("repo-maintenance/validate-all.sh", "scripts/repo-maintenance/validate-all.sh"), @@ -136,8 +155,40 @@ def ensure_safe_target(repo_root: Path, relative_target: Path) -> None: ) +def xcode_workspace_findings(repo_root: Path) -> list[str]: + findings: list[str] = [] + workspaces = list(repo_root.glob("*.xcworkspace")) + if len(workspaces) != 1: + findings.append(f"expected exactly one root .xcworkspace, found {len(workspaces)}") + + apps_root = repo_root / "Apps" + if not apps_root.is_dir(): + findings.append("expected Apps/ at the repository root") + elif not any(apps_root.glob("**/*.xcodeproj")): + findings.append("expected at least one .xcodeproj under Apps/") + + packages_root = repo_root / "Packages" + if not packages_root.is_dir(): + findings.append("expected Packages/ at the repository root") + elif not any(packages_root.glob("**/Package.swift")): + findings.append("expected at least one Package.swift under Packages/") + return findings + + +def ensure_profile_shape(repo_root: Path, profile: str) -> None: + if profile != "xcode-workspace": + return + findings = xcode_workspace_findings(repo_root) + if findings: + raise RuntimeError( + "The xcode-workspace profile requires a modular Apple workspace: " + + "; ".join(findings) + + ". Use xcode-app, swift-package, or generic when this is not the workspace root." + ) + + def legacy_xcode_toolkit_migration(repo_root: Path, profile: str) -> tuple[Path, Path] | None: - if profile != "xcode-app": + if profile not in {"xcode-app", "xcode-workspace"}: return None legacy_root = repo_root / DEFAULT_TOOLKIT_ROOT @@ -152,7 +203,7 @@ def legacy_xcode_toolkit_migration(repo_root: Path, profile: str) -> tuple[Path, except OSError: pass raise RuntimeError( - "The xcode-app profile expects repo-maintenance under " + f"The {profile} profile expects repo-maintenance under " f"{toolkit_root(profile).as_posix()}, but both {DEFAULT_TOOLKIT_ROOT.as_posix()} " f"and {toolkit_root(profile).as_posix()} already exist as separate paths. " "Choose the intentional toolkit root, preserve any repo-specific custom files, " @@ -161,7 +212,7 @@ def legacy_xcode_toolkit_migration(repo_root: Path, profile: str) -> tuple[Path, if not legacy_root.is_dir(): raise RuntimeError( - "The xcode-app profile expects repo-maintenance under " + f"The {profile} profile expects repo-maintenance under " f"{toolkit_root(profile).as_posix()}, but the legacy path " f"{DEFAULT_TOOLKIT_ROOT.as_posix()} exists and is not a directory." ) @@ -183,13 +234,13 @@ def apply_legacy_xcode_toolkit_migration(repo_root: Path, profile: str) -> str | pass return ( f"migrated legacy {DEFAULT_TOOLKIT_ROOT.as_posix()} to " - f"{toolkit_root(profile).as_posix()} for xcode-app profile" + f"{toolkit_root(profile).as_posix()} for {profile} profile" ) def copy_file(source: Path, target: Path, profile: str) -> None: target.parent.mkdir(parents=True, exist_ok=True) - if profile == "xcode-app": + if profile in {"xcode-app", "xcode-workspace"}: content = source.read_text(encoding="utf-8") content = content.replace("scripts/repo-maintenance", "Scripts/repo-maintenance") target.write_text(content, encoding="utf-8") @@ -259,6 +310,7 @@ def main() -> int: return 1 try: + ensure_profile_shape(repo_root, args.profile) planned_migration = legacy_xcode_toolkit_migration(repo_root, args.profile) for _, relative_target in target_pairs(args.profile, args.skip_github_workflow): ensure_safe_target(repo_root, relative_target) @@ -285,7 +337,7 @@ def main() -> int: if planned_migration is not None: actions.append( f"migrate legacy {DEFAULT_TOOLKIT_ROOT.as_posix()} to " - f"{toolkit_root(args.profile).as_posix()} for xcode-app profile" + f"{toolkit_root(args.profile).as_posix()} for {args.profile} profile" ) for source, relative_target in target_pairs(args.profile, args.skip_github_workflow): target = repo_root / relative_target diff --git a/plugins/repository-skills/skills/maintain-project-repo/scripts/run_workflow.py b/plugins/repository-skills/skills/maintain-project-repo/scripts/run_workflow.py index ec6d39d83..3f8110bd4 100755 --- a/plugins/repository-skills/skills/maintain-project-repo/scripts/run_workflow.py +++ b/plugins/repository-skills/skills/maintain-project-repo/scripts/run_workflow.py @@ -19,7 +19,7 @@ def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo-root") parser.add_argument("--operation", choices=("install", "refresh", "report-only")) - parser.add_argument("--profile", choices=("generic", "swift-package", "xcode-app")) + parser.add_argument("--profile", choices=("generic", "swift-package", "xcode-app", "xcode-workspace")) parser.add_argument("--skip-github-workflow", action="store_true") parser.add_argument("--dry-run", action="store_true") return parser diff --git a/plugins/repository-skills/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py b/plugins/repository-skills/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py index 5e9398c89..0519e9d97 100644 --- a/plugins/repository-skills/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py +++ b/plugins/repository-skills/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py @@ -107,6 +107,59 @@ def test_xcode_profile_migrates_legacy_lowercase_toolkit_root(self) -> None: self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validations/90-custom.sh").is_file()) self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validate-all.sh").is_file()) + def test_xcode_workspace_profile_installs_workspace_validation_and_dispatches_components(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + (root / "Product.xcworkspace").mkdir() + (root / "Apps/ProductApp.xcodeproj").mkdir(parents=True) + package_root = root / "Packages/ProductCore" + package_root.mkdir(parents=True) + (package_root / "Package.swift").write_text("// package\n", encoding="utf-8") + component_validation = package_root / "scripts/repo-maintenance/validate-all.sh" + component_validation.parent.mkdir(parents=True) + component_validation.write_text("#!/usr/bin/env sh\nprintf '%s\\n' package-validated\n", encoding="utf-8") + (root / "AGENTS.md").write_text( + "# AGENTS.md\n\n- Scripts/repo-maintenance/validate-all.sh\n- Scripts/repo-maintenance/sync-shared.sh\n- Scripts/repo-maintenance/release.sh\n", + encoding="utf-8", + ) + + code, payload = self.run_script( + "--repo-root", tmpdir, "--operation", "install", "--profile", "xcode-workspace" + ) + + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertIn("Scripts/repo-maintenance/workspace/validate-components.sh", payload["managed_files"]) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validations/40-xcode-workspace-layout.sh").is_file()) + profile_env = Path(tmpdir, "Scripts/repo-maintenance/config/profile.env").read_text(encoding="utf-8") + self.assertIn('REPO_MAINTENANCE_PROFILE="xcode-workspace"', profile_env) + + subprocess.run(["git", "init"], cwd=tmpdir, check=True, capture_output=True, text=True) + proc = subprocess.run( + ["sh", "Scripts/repo-maintenance/validate-all.sh"], + cwd=tmpdir, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr or proc.stdout) + self.assertIn("Validated xcode-workspace composition", proc.stdout) + self.assertIn("package-validated", proc.stdout) + + def test_xcode_workspace_profile_rejects_invalid_workspace_layout(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + Path(tmpdir, "AGENTS.md").write_text( + "# AGENTS.md\n\n- Scripts/repo-maintenance/validate-all.sh\n- Scripts/repo-maintenance/sync-shared.sh\n- Scripts/repo-maintenance/release.sh\n", + encoding="utf-8", + ) + code, payload = self.run_script( + "--repo-root", tmpdir, "--operation", "install", "--profile", "xcode-workspace" + ) + self.assertEqual(code, 1) + self.assertEqual(payload["status"], "blocked") + self.assertIn("requires a modular Apple workspace", payload["stderr"]) + self.assertIn("expected exactly one root .xcworkspace", payload["stderr"]) + def test_generic_profile_uses_generic_macos_latest_workflow(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install") diff --git a/plugins/repository-skills/skills/repository-operations-workflow/SKILL.md b/plugins/repository-skills/skills/repository-operations-workflow/SKILL.md new file mode 100644 index 000000000..f7d8821e7 --- /dev/null +++ b/plugins/repository-skills/skills/repository-operations-workflow/SKILL.md @@ -0,0 +1,48 @@ +--- +name: repository-operations-workflow +description: Route repository work to focused Git, GitHub, worktree, settings, or release workflows. Use first when a request spans several repository operations or the correct owner is unclear. +--- + +# Repository Operations Workflow + +## Purpose + +Provide one entry point for `repository-skills` without turning it into a +catch-all implementation surface. Classify the request, inspect only the state +needed for that classification, then hand work to the narrowest owner. + +## Routing + +1. Read the closest `AGENTS.md`, inspect `git status --short --branch`, and + identify the repository root and remote only when the request needs them. +2. Route by the primary outcome: + + | Outcome | Owner | + | --- | --- | + | Status, branch, commit, history, rebase, conflict, recovery, or Git-default alignment | `git-workflow` | + | Pull request, review, issue, CI, or collaboration handoff | `github-collaboration-workflow` | + | Codex worktree mode, local environment, or app-owned worktree behavior | `codex-gui-worktree-workflow` | + | Parallel worker branch, worktree, write, or integration ownership | `coordinate-worktrees-and-threads` | + | GitHub settings, rulesets, security automation, or repository policy | `maintain-github-repository` | + | Version bump, tag, release, publication, or branch-accounting cleanup | `maintain-project-repo` | +3. When the request crosses multiple outcomes, name the sequence and keep each + mutation with its owning workflow. For example: select a worktree, make a + focused Git change, prepare a PR, then use the release workflow only after a + release is explicitly requested. +4. Report the selected owner, observed repository state, required authority, + and one next action. + +## Guardrails + +- Do not use a release workflow for ordinary local edits or a GitHub settings + workflow for pull-request collaboration. +- Do not infer permission to push, merge, tag, delete, publish, or change + GitHub settings from a read-only request. +- Keep branch/worktree ownership explicit before parallel work; a branch does + not grant permission to mutate shared repository state. +- Treat Gale's fetch-prune, fast-forward-only pull, and tracking-branch rebase + preferences as machine-level defaults. Route their inspection and any + repository-specific override to `git-workflow`; do not install them as + repository-local configuration. +- On Hermes or another non-Codex host, treat Codex GUI worktree association as + unavailable and use ordinary Git worktree guidance instead. diff --git a/plugins/repository-skills/skills/repository-operations-workflow/agents/openai.yaml b/plugins/repository-skills/skills/repository-operations-workflow/agents/openai.yaml new file mode 100644 index 000000000..6a8ad9752 --- /dev/null +++ b/plugins/repository-skills/skills/repository-operations-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Repository Operations" + short_description: "Route Git, GitHub, worktree, settings, and release work" + default_prompt: "Use $repository-operations-workflow to classify this repository task, inspect the minimum relevant state, and hand it to the focused Git, GitHub, worktree, settings, or release workflow." diff --git a/plugins/repository-skills/skills/repository-operations-workflow/tests/test_repository_operations_workflows.py b/plugins/repository-skills/skills/repository-operations-workflow/tests/test_repository_operations_workflows.py new file mode 100644 index 000000000..95199b80a --- /dev/null +++ b/plugins/repository-skills/skills/repository-operations-workflow/tests/test_repository_operations_workflows.py @@ -0,0 +1,81 @@ +from pathlib import Path + +import yaml + + +SKILL_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_SKILLS_ROOT = SKILL_ROOT.parent + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def metadata(path: Path) -> dict: + return yaml.safe_load(read(path).split("---", 2)[1]) + + +def test_router_names_each_repository_owner_and_its_mutation_boundaries() -> None: + router = read(SKILL_ROOT / "SKILL.md") + router_metadata = metadata(SKILL_ROOT / "SKILL.md") + agent = yaml.safe_load(read(SKILL_ROOT / "agents" / "openai.yaml"))["interface"] + + assert router_metadata["name"] == "repository-operations-workflow" + for expected in ( + "`git-workflow`", + "`github-collaboration-workflow`", + "`codex-gui-worktree-workflow`", + "`coordinate-worktrees-and-threads`", + "`maintain-github-repository`", + "`maintain-project-repo`", + "machine-level defaults", + "Do not infer permission to push, merge, tag, delete, publish, or change", + ): + assert expected in router + assert "$repository-operations-workflow" in agent["default_prompt"] + + +def test_git_and_github_workflows_keep_local_collaboration_and_release_owners_separate() -> None: + git_skill = REPOSITORY_SKILLS_ROOT / "git-workflow" / "SKILL.md" + github_skill = REPOSITORY_SKILLS_ROOT / "github-collaboration-workflow" / "SKILL.md" + + assert metadata(git_skill)["name"] == "git-workflow" + assert metadata(github_skill)["name"] == "github-collaboration-workflow" + + git_text = read(git_skill) + github_text = read(github_skill) + for expected in ( + "`git worktree list`", + "reflog", + "Push, force push, merge, tag, and destructive recovery actions require clear", + "`github-collaboration-workflow`", + "`maintain-project-repo`", + "fetch.prune=true", + "pull.ff=only", + "branch.autoSetupRebase=always", + "repository-local config", + ): + assert expected in git_text + for expected in ( + "`gh` authentication", + "one bounded snapshot", + "`cronjob`", + "`maintain-github-repository`", + "`maintain-project-repo`", + ): + assert expected in github_text + + +def test_git_baseline_is_shared_machine_policy_without_local_template_writes() -> None: + baseline = read(REPOSITORY_SKILLS_ROOT / "git-workflow" / "references" / "gale-git-baseline.md") + repo_maintenance = read(REPOSITORY_SKILLS_ROOT / "maintain-project-repo" / "SKILL.md") + + for expected in ( + "fetch.prune = true", + "pull.ff = only", + "branch.autoSetupRebase = always", + "git config --show-origin --get-regexp", + "git config --local", + ): + assert expected in baseline + assert "not write `git config --local`" in repo_maintenance diff --git a/plugins/repository-skills/uv.lock b/plugins/repository-skills/uv.lock index d3ca0bd5a..93ebd1e18 100644 --- a/plugins/repository-skills/uv.lock +++ b/plugins/repository-skills/uv.lock @@ -310,7 +310,7 @@ wheels = [ [[package]] name = "repository-skills-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/reverse-engineering-skills/.codex-plugin/plugin.json b/plugins/reverse-engineering-skills/.codex-plugin/plugin.json index 29b3d017c..57cd108fd 100644 --- a/plugins/reverse-engineering-skills/.codex-plugin/plugin.json +++ b/plugins/reverse-engineering-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "reverse-engineering-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Workflow skills for reverse engineering, decompilation, disassembly, symbols, artifact analysis, and exact-build macOS security-control research.", "skills": "./skills/", "author": { diff --git a/plugins/rust-skills/.codex-plugin/plugin.json b/plugins/rust-skills/.codex-plugin/plugin.json index 136fbfccc..9d48066d2 100644 --- a/plugins/rust-skills/.codex-plugin/plugin.json +++ b/plugins/rust-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rust-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Rust, Cargo, rustup, crate, workspace, CLI, library, package, CI, testing, linting, and formatting workflow skills.", "skills": "./skills/", "author": { diff --git a/plugins/server-side-jvm/.codex-plugin/plugin.json b/plugins/server-side-jvm/.codex-plugin/plugin.json index db4a25a0f..18002e4a5 100644 --- a/plugins/server-side-jvm/.codex-plugin/plugin.json +++ b/plugins/server-side-jvm/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "server-side-jvm", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for choosing, building, testing, and maintaining server-side JVM backend projects with Java and Scala as equal first-party languages and future Clojure support planned.", "author": { "name": "Gale", diff --git a/plugins/server-side-swift/.codex-plugin/plugin.json b/plugins/server-side-swift/.codex-plugin/plugin.json index 16939e30d..e22e63339 100644 --- a/plugins/server-side-swift/.codex-plugin/plugin.json +++ b/plugins/server-side-swift/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "server-side-swift", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for bootstrapping, syncing, building, running, containerizing, deploying, and maintaining server-side Swift services, including Vapor, Leaf-rendered web and HTML email, Hummingbird, hb, persistence, Swift OpenAPI, RPC-fit decisions, SwiftNIO, observability, auth, app sync, Docker, Apple Containerization, Fly.io, and SwiftPM-first workflows.", "author": { "name": "Gale", diff --git a/plugins/spotify/.codex-plugin/plugin.json b/plugins/spotify/.codex-plugin/plugin.json index faa0316a0..30b7f687a 100644 --- a/plugins/spotify/.codex-plugin/plugin.json +++ b/plugins/spotify/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spotify", - "version": "9.31.2", + "version": "9.32.0", "description": "Placeholder plugin repository for future Spotify-focused Codex workflows.", "author": { "name": "Gale", diff --git a/plugins/swift-lang/.codex-plugin/plugin.json b/plugins/swift-lang/.codex-plugin/plugin.json index ce3a03f00..10bd6e1fe 100644 --- a/plugins/swift-lang/.codex-plugin/plugin.json +++ b/plugins/swift-lang/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "swift-lang", - "version": "9.31.2", + "version": "9.32.0", "description": "Shared Swift language and tooling skills for API style, errors, functional pipelines, formatting, source organization, SwiftSyntax, compiler inspection, SourceKit, indexing, SourceKit-LSP, and modernization.", "skills": "./skills/", "author": { diff --git a/plugins/swiftasb-skills/.codex-plugin/plugin.json b/plugins/swiftasb-skills/.codex-plugin/plugin.json index 20f827a58..2161c7368 100644 --- a/plugins/swiftasb-skills/.codex-plugin/plugin.json +++ b/plugins/swiftasb-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "swiftasb-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for explaining SwiftASB and building SwiftUI, AppKit, and Swift package integrations on top of it.", "author": { "name": "Gale", diff --git a/plugins/things-app/.codex-plugin/plugin.json b/plugins/things-app/.codex-plugin/plugin.json index 7815cfe92..8fa0f98ab 100644 --- a/plugins/things-app/.codex-plugin/plugin.json +++ b/plugins/things-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "things-app", - "version": "9.31.2", + "version": "9.32.0", "description": "Things.app skills and a bundled local MCP server for reminders, planning digests, and structured task workflows.", "author": { "name": "Gale", diff --git a/plugins/things-app/mcp/pyproject.toml b/plugins/things-app/mcp/pyproject.toml index cbbe7bbba..76929d0da 100644 --- a/plugins/things-app/mcp/pyproject.toml +++ b/plugins/things-app/mcp/pyproject.toml @@ -7,7 +7,7 @@ packages = ["app"] [project] name = "things-mcp" -version = "9.31.2" +version = "9.32.0" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/things-app/mcp/uv.lock b/plugins/things-app/mcp/uv.lock index fa1fe9af0..a524453a9 100644 --- a/plugins/things-app/mcp/uv.lock +++ b/plugins/things-app/mcp/uv.lock @@ -1244,7 +1244,7 @@ wheels = [ [[package]] name = "things-mcp" -version = "9.31.2" +version = "9.32.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/things-app/pyproject.toml b/plugins/things-app/pyproject.toml index 46b43de77..8cf3e3aa0 100644 --- a/plugins/things-app/pyproject.toml +++ b/plugins/things-app/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "things-app-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Maintainer-only Python tooling baseline for things-app skills and plugin packaging." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/things-app/uv.lock b/plugins/things-app/uv.lock index ae7a3fdf4..8a03188b4 100644 --- a/plugins/things-app/uv.lock +++ b/plugins/things-app/uv.lock @@ -120,7 +120,7 @@ wheels = [ [[package]] name = "things-app-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/web-dev-skills/.codex-plugin/plugin.json b/plugins/web-dev-skills/.codex-plugin/plugin.json index fd73ddda9..aae3775d9 100644 --- a/plugins/web-dev-skills/.codex-plugin/plugin.json +++ b/plugins/web-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "web-dev-skills", - "version": "9.31.2", + "version": "9.32.0", "description": "Codex skills for focused web and Expo native-boundary workflows.", "author": { "name": "Gale", diff --git a/pyproject.toml b/pyproject.toml index 6bd501410..31ec4fe9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "socket-maintenance" -version = "9.31.2" +version = "9.32.0" description = "Root uv tooling baseline for the socket superproject." requires-python = ">=3.11" dependencies = [] diff --git a/scripts/export_hermes_skills.py b/scripts/export_hermes_skills.py index 012f2b27d..080204550 100644 --- a/scripts/export_hermes_skills.py +++ b/scripts/export_hermes_skills.py @@ -28,6 +28,7 @@ PYTHON_SOURCE_ROOT = REPO_ROOT / "plugins" / "python-skills" / "skills" JVM_SOURCE_ROOT = REPO_ROOT / "plugins" / "server-side-jvm" / "skills" CLOUD_DEPLOYMENT_SOURCE_ROOT = REPO_ROOT / "plugins" / "cloud-deployment-skills" / "skills" +REPOSITORY_SOURCE_ROOT = REPO_ROOT / "plugins" / "repository-skills" / "skills" EXPORT_ROOT = REPO_ROOT / "skills" AGENT_PORTABILITY_SKILLS = ( "bootstrap-skills-plugin-repo", @@ -153,7 +154,12 @@ "build-oxpecker-web-app", "build-dotnet-agent-service", ) -AGENT_ENGINEERING_SKILLS = ("design-n8n-agent-workflow",) +AGENT_ENGINEERING_SKILLS = ( + "coordinate-external-agents", + "coordinate-worktrees-and-threads", + "design-n8n-agent-workflow", + "orchestrate-agent-work", +) PYTHON_SKILLS = ( "build-python-agent-service", "fastapi-service-workflow", @@ -165,6 +171,13 @@ "cloud-deployment-routing-workflow", "dockerized-service-release-deployment-workflow", ) +REPOSITORY_SKILLS = ( + "repository-operations-workflow", + "git-workflow", + "github-collaboration-workflow", + "maintain-github-repository", + "maintain-project-repo", +) EXPORTED_SKILLS = ( AGENT_PORTABILITY_SKILLS + MESSAGING_SKILLS @@ -179,6 +192,7 @@ + PYTHON_SKILLS + JVM_SKILLS + CLOUD_DEPLOYMENT_SKILLS + + REPOSITORY_SKILLS ) @@ -217,6 +231,7 @@ def source_paths(source_root: Path | None = None) -> dict[str, Path]: skill_name: CLOUD_DEPLOYMENT_SOURCE_ROOT for skill_name in CLOUD_DEPLOYMENT_SKILLS }, + **{skill_name: REPOSITORY_SOURCE_ROOT for skill_name in REPOSITORY_SKILLS}, } return { skill_name: roots[skill_name] / skill_name for skill_name in EXPORTED_SKILLS diff --git a/skills.sh.json b/skills.sh.json index e651c5fb9..0507b6ee9 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -117,7 +117,10 @@ { "title": "Productivity Skills", "skills": [ - "design-n8n-agent-workflow" + "coordinate-external-agents", + "coordinate-worktrees-and-threads", + "design-n8n-agent-workflow", + "orchestrate-agent-work" ] }, { @@ -179,6 +182,16 @@ "evaluate-tool-calling-model", "benchmark-model-runtime" ] + }, + { + "title": "Repository Skills", + "skills": [ + "repository-operations-workflow", + "git-workflow", + "github-collaboration-workflow", + "maintain-github-repository", + "maintain-project-repo" + ] } ] } diff --git a/skills/coordinate-external-agents/SKILL.md b/skills/coordinate-external-agents/SKILL.md new file mode 100644 index 000000000..7b7d95e9b --- /dev/null +++ b/skills/coordinate-external-agents/SKILL.md @@ -0,0 +1,40 @@ +--- +name: coordinate-external-agents +description: Coordinate independently operated external agents through durable handoffs. Use when work crosses hosts, sessions, accounts, services, queues, boards, pull requests, or human-owned task systems and cannot rely on an in-run subagent result. +--- + +# Coordinate External Agents + +Treat an external agent as an independently operated worker, not as a magical +subagent. Starting it is not progress unless the coordinator can later discover +its state and consume its report. + +## Workflow + +1. Choose a durable shared surface: task record, issue, pull request, branch, + worktree, artifact directory, shared document, queue, or host-native board. +2. Create a task record with the normal launch envelope plus access, retention, + retry/resume, and result-delivery details. +3. Assign one owner, one coordinator, one artifact/write surface, and one final + verification owner. Do not assume direct worker-to-worker messages exist. +4. Record status using `queued`, `running`, `blocked`, `needs-decision`, + `completed`, `failed`, or `cancelled`; use the selected host's closest + equivalent without hiding meaning. +5. Require a final report on the durable surface and notify the coordinator + through the specified return route. +6. Verify the returned artifact, branch, change, or evidence before treating + the task as complete. + +## Durable Work Rule + +Use a queue, board, scheduled task, CI workflow, or service for work that must +survive session closure, restart, human input, retry, multiple roles, or later +audit. Do not represent a transient child-agent call as durable work. + +## Boundaries + +- Keep credentials, private context, and mutation authority scoped to the + external worker's actual access surface. +- Use host-specific adapters only for mechanics. This skill owns the universal + handoff contract; it does not claim Codex, ChatGPT Work, Hermes, or another + host shares one message or task API. diff --git a/skills/coordinate-external-agents/agents/openai.yaml b/skills/coordinate-external-agents/agents/openai.yaml new file mode 100644 index 000000000..7281acc4c --- /dev/null +++ b/skills/coordinate-external-agents/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Coordinate External Agents" + short_description: "Create durable handoffs across agent systems." + default_prompt: "Use $coordinate-external-agents to hand this work to an external agent through a durable task and artifact contract." diff --git a/skills/coordinate-worktrees-and-threads/SKILL.md b/skills/coordinate-worktrees-and-threads/SKILL.md new file mode 100644 index 000000000..7f78d6a79 --- /dev/null +++ b/skills/coordinate-worktrees-and-threads/SKILL.md @@ -0,0 +1,39 @@ +--- +name: coordinate-worktrees-and-threads +description: Assign worktree, branch, write, validation, integration, and cleanup ownership before parallel repository work. Use when a worker will inspect or modify repository state outside the coordinator's worktree. +--- + +# Coordinate Worktrees And Threads + +Establish repository ownership before a worker begins. A thread is not a +worktree, and a branch is not permission to make shared Git mutations. + +## Launch Record + +For every worker, record the worker/task identity, coordinator, repository and +base commit, worktree path, branch, read/write scope, allowed Git operations, +validation lane, output/merge target, final report route, and retention or +cleanup decision. + +## Rules + +- Give parallel writers disjoint files or directories and separate worktrees. +- When a Codex-managed worktree versus a plain Git worktree is itself a + decision, hand that host-specific choice to + `repository-skills:codex-gui-worktree-workflow`; retain this skill's + host-neutral ownership record in either case. +- Serialize Git operations that change shared repository state. A worker must + not assume it owns tags, merges, release branches, deletion, or cleanup. +- Serialize heavy build and test lanes unless the environment explicitly proves + they are independent and safe. +- Keep final integration, conflict resolution, release, branch accounting, and + cleanup with the coordinator unless the user explicitly assigns them. +- When a worker returns, report branch/worktree state, commits, validation, + uncommitted changes, artifacts, and the recommended integration action. + +## Completion + +The coordinator explicitly chooses whether to retain the worker thread or +worktree for follow-up, merge/preserve its result, archive it, or remove it +after branch accounting. Never delete a worktree or branch merely because a +worker says it is done. diff --git a/skills/coordinate-worktrees-and-threads/agents/openai.yaml b/skills/coordinate-worktrees-and-threads/agents/openai.yaml new file mode 100644 index 000000000..66f8d9591 --- /dev/null +++ b/skills/coordinate-worktrees-and-threads/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Coordinate Worktrees And Threads" + short_description: "Assign repository ownership before parallel work." + default_prompt: "Use $coordinate-worktrees-and-threads to assign worker, worktree, branch, write, validation, and cleanup ownership." diff --git a/skills/git-workflow/SKILL.md b/skills/git-workflow/SKILL.md new file mode 100644 index 000000000..817bbe56e --- /dev/null +++ b/skills/git-workflow/SKILL.md @@ -0,0 +1,61 @@ +--- +name: git-workflow +description: "Safely inspect and perform everyday Git work: branches, focused commits, history, integration, conflicts, worktrees, and recovery. Use for local version-control tasks that are not GitHub settings or release publication." +--- + +# Git Workflow + +## Purpose + +Handle ordinary local Git work with enough evidence to preserve uncommitted +work, branch reachability, and worktree ownership. This workflow does not own +GitHub settings or protected-main releases. + +## Workflow + +1. Read the closest `AGENTS.md`; inspect `git status --short --branch`, + `git worktree list`, and the relevant history/diff before a mutation. +2. Inspect the effective Git configuration before an operation depends on fetch, + pull, or tracking-branch behavior. On Gale-managed machines, expect + `fetch.prune=true`, `pull.ff=only`, and `branch.autoSetupRebase=always`; + report any local or worktree override rather than silently changing it. See + `references/gale-git-baseline.md`. +3. Classify the operation: + - inspection: status, diff, log, blame, or reachability; + - focused change: create/switch a branch, stage intentional files, commit; + - integration: fetch, compare, rebase or merge, resolve conflicts; + - recovery: reflog, lost commit investigation, or safe restoration plan. +4. Preserve the current work before an operation that rewrites, discards, or + moves it. Explain the exact target and recovery path before using reset, + clean, rebase, force push, or branch/worktree deletion. +5. For branch work, use a feature branch and separate worktree when required by + repository guidance. Never keep the same branch live in two worktrees except + as a short recovery step. +6. Make focused commits with the repository's required subject format. Review + staged changes and commit reachability after each shared Git mutation. +7. For integration, fetch first. The normal Gale baseline prunes stale refs and + refuses pull-created merge commits; newly tracking branches rebase on pull. + Choose a merge or rebase only when repository policy and the branch's + publication state justify it. Resolve each conflict from source intent, run + proportionate validation, and inspect the resulting diff. +8. Before deleting a branch, worktree, ref, or archive, verify reachability and + complete any repository-required branch accounting. + +## Boundaries + +- Route GitHub pull requests, reviews, issues, and CI collaboration to + `github-collaboration-workflow`. +- Route tags, publishing, GitHub releases, and protected-main release cleanup + to `maintain-project-repo`. +- Route GitHub settings, rulesets, and security configuration to + `maintain-github-repository`. +- Push, force push, merge, tag, and destructive recovery actions require clear + user authority or an existing repository-owned release contract. +- Do not encode Gale's machine-level Git baseline as repository-local config in + a generated template or existing repository. + +## Hermes Notes + +This is portable guidance. Verify that `git` is installed, the target is a Git +worktree, and the active identity/remotes are the intended ones. Hermes gains +no Git credentials or mutation authority from this skill. diff --git a/skills/git-workflow/agents/openai.yaml b/skills/git-workflow/agents/openai.yaml new file mode 100644 index 000000000..0b4f7bc24 --- /dev/null +++ b/skills/git-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Git Workflow" + short_description: "Safely inspect, integrate, and recover local Git work" + default_prompt: "Use $git-workflow to inspect this repository's Git state, choose a safe branch, commit, integration, conflict-resolution, or recovery path, and explain any required mutation authority before acting." diff --git a/skills/git-workflow/references/gale-git-baseline.md b/skills/git-workflow/references/gale-git-baseline.md new file mode 100644 index 000000000..a0b1b565b --- /dev/null +++ b/skills/git-workflow/references/gale-git-baseline.md @@ -0,0 +1,38 @@ +# Gale Git Baseline + +Gale-managed development machines normally provide these global Git defaults: + +```text +fetch.prune = true +pull.ff = only +branch.autoSetupRebase = always +``` + +They mean that ordinary fetches remove stale remote-tracking refs, ordinary +pulls refuse to create merge commits, and newly created tracking branches use +rebase when pulled. They are machine policy, not repository content. + +## Workflow Contract + +1. Before relying on the baseline, inspect effective values and their origins: + + ```bash + git config --show-origin --get-regexp '^(fetch\.prune|pull\.ff|branch\.autoSetupRebase)$' + ``` + +2. Treat a repository-local, worktree-local, command-line, or environment + override as an explicit repository policy. Report it before an operation + would behave differently from this baseline; do not silently remove it. +3. Keep explicit safety commands when their behavior must be invariant. For + example, a protected-main release may use `git pull --ff-only` even when the + global default already has that value. +4. Do not write these preferences with `git config --local` in bootstrap, + sync, or repository-maintenance templates. A generated repository must work + for contributors whose machine policy differs. + +## Scope + +This contract guides everyday Git work, worktree planning, repository +maintenance, and Git-initializing bootstrap workflows. It does not change a +repository's merge strategy, branch protection, remote permissions, or release +authority. diff --git a/skills/github-collaboration-workflow/SKILL.md b/skills/github-collaboration-workflow/SKILL.md new file mode 100644 index 000000000..aa575911e --- /dev/null +++ b/skills/github-collaboration-workflow/SKILL.md @@ -0,0 +1,51 @@ +--- +name: github-collaboration-workflow +description: Prepare and maintain GitHub pull requests, reviews, issues, CI triage, and durable handoffs. Use for collaboration work after local Git changes exist; not for GitHub settings or release publication. +--- + +# GitHub Collaboration Workflow + +## Purpose + +Own the collaboration layer around a change: pull requests, review and comment +triage, issue linkage, CI status, and durable handoffs. Keep local Git, +GitHub-settings, and release operations with their focused owners. + +## Workflow + +1. Confirm the exact repository, branch, base branch, current `git status`, + remote visibility, and `gh` authentication before a remote action. +2. Read repository contribution and review policy. Inspect existing pull + requests and issues before creating duplicates. +3. Prepare a PR only after the change is committed, validated proportionately, + and pushed with explicit authority. Use a concise body that explains intent, + risk, and verification; link the governing issue when one exists. +4. Inspect CI checks, reviews, and unresolved comments as separate gates. Name + failures, requested changes, and unknown states precisely. +5. Address valid review feedback in the owning branch; route broader follow-up + to the repository's roadmap or issue system. Re-read the changed thread and + checks before reporting it resolved. +6. For an external wait, do one bounded snapshot. Use the host's supported + continuation mechanism no sooner than five minutes later; do not keep a + shell watch or poll loop running. +7. Merge, label, close, reopen, or delete remote work only with explicit + authority or a repository-owned release contract. Verify the resulting + GitHub state after each mutation. + +## Boundaries + +- Route local branch, commit, rebase, conflict, and recovery work to + `git-workflow`. +- Route GitHub server settings, rulesets, and security configuration to + `maintain-github-repository`. +- Route protected-main release PRs, tags, releases, and branch-accounting + cleanup to `maintain-project-repo`. +- Keep credentials, private issue content, and security findings out of public + PR bodies and reports. + +## Hermes Notes + +This is portable guidance. Verify `gh` availability and authentication before +remote actions. On a pending CI or review gate, Hermes uses a self-contained +`cronjob` delivered to the origin session with `attach_to_session=true`; the +packet must include repository, branch, PR, commit, gate, and safe inspection. diff --git a/skills/github-collaboration-workflow/agents/openai.yaml b/skills/github-collaboration-workflow/agents/openai.yaml new file mode 100644 index 000000000..1234f4b1d --- /dev/null +++ b/skills/github-collaboration-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "GitHub Collaboration" + short_description: "Prepare PRs, triage reviews and CI, and coordinate handoffs" + default_prompt: "Use $github-collaboration-workflow to inspect this change's GitHub collaboration state, prepare or update a pull request when authorized, triage checks and review feedback, and preserve clear remote-mutation boundaries." diff --git a/skills/maintain-github-repository/SKILL.md b/skills/maintain-github-repository/SKILL.md new file mode 100644 index 000000000..08de857db --- /dev/null +++ b/skills/maintain-github-repository/SKILL.md @@ -0,0 +1,113 @@ +--- +name: maintain-github-repository +description: "Audit or align a GitHub repository's server-side settings, rulesets, security automation, Dependabot, and sign-off policy. Use for server-side GitHub policy work, not ordinary local Git commits, PR collaboration, or releases." +--- + +# Maintain GitHub Repository + +## Purpose + +Audit or align server-side GitHub repository settings without mixing those +changes into local Git operations or release automation. The default path is a +read-only audit. Apply changes only when the user requests mutation. + +## When To Use + +- Use this skill when creating or configuring a GitHub repository. +- Use this skill when the user asks to apply their normal GitHub repository + defaults. +- Use this skill when auditing repository features, merge methods, security + automation, sign-off requirements, branch protection, or rulesets. +- Use this skill when GitHub settings may have drifted from repo policy. +- Use this skill when a bootstrap or sync workflow reaches the GitHub remote + configuration step. +- Do not use this skill for local commits, branches, rebases, merges, pushes, + pull-request bodies, review replies, tags, or GitHub release creation. +- Use `maintain-project-repo` for protected-main releases, publishing, tags, + GitHub release objects, release cleanup, and branch accounting. +- Use `maintain-project-contributing` when the repository's DCO, contributor + grant, or sign-off documentation needs to change. + +## Workflow + +1. Resolve the repository: + - confirm `owner/repo`, default branch, visibility, and current user + permissions + - verify the target is the repository the user intended + - read repo-local `AGENTS.md`, contribution policy, security policy, and + release guidance before selecting a baseline +2. Load `references/github-repository-settings.md`. +3. Audit before mutation: + - read supported repository settings with `gh repo view` and `gh api` + - inspect branch protection or rulesets separately from general repo + settings + - inspect Dependabot, secret scanning, push protection, private + vulnerability reporting, web commit sign-off, and auto-close behavior + independently + - verify required status-check names against actual recent check runs when + branch protection depends on CI +4. Classify every relevant setting as: + - `aligned` + - `drifted` + - `unavailable` + - `unknown` + - `not applicable` +5. Report current values, recommended values, unavailable settings, and exact + planned mutations separately. +6. Apply only requested changes: + - use `gh repo edit` for supported general toggles + - use current documented `gh api` endpoints for security, sign-off, + auto-close, branch-protection, and ruleset settings + - prefer file-backed `.github/dependabot.yml`, `CODEOWNERS`, `SECURITY.md`, + workflow, and contribution-policy changes when the behavior is owned by + repository files + - keep visibility changes separately approval-gated + - preserve documented maintainer direct-push workflows +7. Re-read every changed setting and report the verified result. + +## Inputs + +- `repository`: optional `owner/repo`; default to the current checkout's GitHub + remote only when unambiguous +- `mode`: `audit` or `apply` +- `requested_changes`: optional explicit settings changes +- `baseline`: repo-local policy or the recommended baseline reference +- Defaults: + - `mode=audit` + - visibility remains unchanged + - no settings are mutated without an apply request + +## Output + +- `Repository`: exact `owner/repo`, visibility, and default branch +- `Current Settings`: observed values +- `Recommended Settings`: repo-policy or fallback baseline +- `Unavailable Settings`: plan, visibility, permission, or API limitations +- `Planned Changes`: exact mutations, empty in audit-only mode +- `Applied Changes`: mutation results, apply mode only +- `Verification`: post-mutation values or audit evidence +- `Follow-Up`: file-backed policy, DCO check, CI check-name, release, or + contributor-doc work that belongs to another skill + +## Guards + +- Never infer or bundle a repository visibility change. +- Never weaken security settings just to make an audit pass. +- Never require approving reviews when the repo has no independent reviewer + unless the user explicitly wants that policy. +- Never block a documented maintainer direct-push workflow accidentally. +- Never guess a required status-check context from a workflow display name. +- Do not treat GitHub web sign-off as command-line DCO enforcement. +- Apply all available requested settings before reporting settings that GitHub + cannot expose for the current plan or visibility. +- Keep credentials, tokens, and private vulnerability details out of reports. + +## Trigger Evaluation + +Use `references/trigger-eval.md` to verify that settings and policy requests +trigger this skill while ordinary Git and release requests route elsewhere. + +## References + +- `references/github-repository-settings.md` +- `references/trigger-eval.md` diff --git a/skills/maintain-github-repository/agents/openai.yaml b/skills/maintain-github-repository/agents/openai.yaml new file mode 100644 index 000000000..9b7369933 --- /dev/null +++ b/skills/maintain-github-repository/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Maintain GitHub Repository" + short_description: "Audit and align GitHub repository settings" + default_prompt: "Use $maintain-github-repository to audit this GitHub repository against the recommended settings baseline, explain drift, and apply only the changes I request." diff --git a/skills/maintain-github-repository/references/github-repository-settings.md b/skills/maintain-github-repository/references/github-repository-settings.md new file mode 100644 index 000000000..96b7aafa3 --- /dev/null +++ b/skills/maintain-github-repository/references/github-repository-settings.md @@ -0,0 +1,110 @@ +# GitHub Repository Settings + +Use this reference when a repository is hosted on GitHub and the user asks to +create, bootstrap, sync, audit, or update repository settings. + +## Audit Before Mutation + +1. Confirm the repository, owner, default branch, visibility, and current user + permissions. +2. Read the repository's `AGENTS.md`, contributor policy, release workflow, and + current GitHub settings before choosing a baseline. +3. Report current values, recommended values, unavailable settings, and planned + changes separately. +4. Treat visibility changes as explicit approval-gated operations. Never infer + a visibility change from a settings audit. +5. Apply available settings first. Record plan, visibility, permission, or API + limitations instead of changing visibility or weakening the baseline. + +## Recommended Baseline + +Use narrower repo policy when it exists. Otherwise, prefer this baseline for a +maintained software repository: + +- issues enabled +- wiki, projects, and discussions disabled unless the repository actively uses + them +- delete pull-request branches after merge enabled +- pull-request branch updates enabled +- auto-merge enabled when the repository uses pull-request automation +- merge commits and rebase merges enabled +- squash merges enabled unless repository policy intentionally narrows merge + methods +- auto-close linked issues enabled when supported +- Dependabot alerts and security updates enabled +- secret scanning and push protection enabled when supported +- private vulnerability reporting enabled for public repositories +- web commit sign-off required when the repository uses DCO or another sign-off + policy + +Do not require approving reviews by default for a single-maintainer repository. +Do not add branch restrictions that block the owner or maintainer's documented +direct-push workflow. + +## Branch Protection + +Branch protection must match the repository's real workflow: + +- require the exact status-check context emitted by CI; for the managed + repo-maintenance workflow, require `validate` +- keep force pushes and branch deletion disabled unless repo policy explicitly + allows them +- use strict required checks only when keeping pull-request branches current is + acceptable for that repository +- do not require pull-request reviews by default when the repository has no + independent reviewer +- preserve documented owner or maintainer direct pushes when that is an + intentional workflow +- distinguish branch protection from DCO sign-off enforcement; GitHub's web + sign-off setting covers web commits only, while command-line sign-off needs a + separate status check or contribution gate + +## Apply Surfaces + +Use `gh repo edit` for supported repository toggles: + +```bash +gh repo edit OWNER/REPO \ + --enable-issues=true \ + --enable-wiki=false \ + --enable-projects=false \ + --enable-discussions=false \ + --delete-branch-on-merge=true \ + --allow-update-branch=true \ + --enable-auto-merge=true \ + --enable-merge-commit=true \ + --enable-rebase-merge=true \ + --enable-squash-merge=true +``` + +Use `gh api` for settings that are not exposed by `gh repo edit`, including +Dependabot security updates, private vulnerability reporting, web commit +sign-off, auto-closing linked issues, and branch protection details. Read the +current REST documentation before constructing mutation requests. + +Prefer repository-owned `.github/dependabot.yml`, workflow files, `CODEOWNERS`, +`SECURITY.md`, and contribution policy when the requested behavior is +file-backed rather than a server-side toggle. + +## Verification + +After applying settings: + +1. Re-read repository settings through `gh repo view`, `gh api`, and branch or + ruleset endpoints. +2. Verify required status-check names against an actual recent check run. +3. Verify Dependabot, secret scanning, push protection, private vulnerability + reporting, and web sign-off independently. +4. Record settings that could not be applied and why. +5. Keep the repository's visibility unchanged unless the user separately + approved a visibility transition. + +## Official Sources + +- [GitHub CLI `gh repo edit`](https://cli.github.com/manual/gh_repo_edit) +- [Managing the commit signoff policy](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-commit-signoff-policy-for-your-repository) +- [Configuring Dependabot security updates](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-security-updates) +- [Configuring private vulnerability reporting](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/configure-for-a-repository) +- [About protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) +- [REST API endpoints for repositories](https://docs.github.com/en/rest/repos/repos) +- [REST API endpoints for protected branches](https://docs.github.com/en/rest/branches/branch-protection) diff --git a/skills/maintain-github-repository/references/trigger-eval.md b/skills/maintain-github-repository/references/trigger-eval.md new file mode 100644 index 000000000..0d7f47e30 --- /dev/null +++ b/skills/maintain-github-repository/references/trigger-eval.md @@ -0,0 +1,30 @@ +# Trigger Evaluation + +## Should Trigger + +- Apply my normal GitHub repository settings. +- Audit this repo's branch protection and Dependabot settings. +- Configure the GitHub settings for this new repository. +- Turn on private vulnerability reporting and push protection. +- Check whether web commit sign-off is enabled for our DCO policy. +- Align merge methods, issue tracking, and branch cleanup with our defaults. +- Why can I not enable this security setting on the repository? +- Verify that the `validate` status check is required on `main`. + +## Should Not Trigger + +- Commit and push these changes. +- Rebase this branch onto `main`. +- Write the pull-request body. +- Address this review comment. +- Release version 1.4.0. +- Tag this commit and create the GitHub release. +- Install the repo-maintenance toolkit. +- Update `CONTRIBUTING.md` with our DCO policy. + +## Routing + +- Local Git and pull-request mechanics follow repo-local Git guidance. +- Release, publish, tag, protected-main, cleanup, and branch-accounting requests + route to `maintain-project-repo`. +- Contribution-document changes route to `maintain-project-contributing`. diff --git a/skills/maintain-github-repository/tests/test_maintain_github_repository.py b/skills/maintain-github-repository/tests/test_maintain_github_repository.py new file mode 100644 index 000000000..3db373c05 --- /dev/null +++ b/skills/maintain-github-repository/tests/test_maintain_github_repository.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import yaml + + +SKILL_ROOT = Path(__file__).resolve().parents[1] + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def test_skill_metadata_and_interface_name_the_settings_scope() -> None: + skill_text = read(SKILL_ROOT / "SKILL.md") + frontmatter = skill_text.split("---", 2)[1] + metadata = yaml.safe_load(frontmatter) + interface = yaml.safe_load(read(SKILL_ROOT / "agents" / "openai.yaml"))["interface"] + + assert metadata["name"] == "maintain-github-repository" + assert "GitHub repository's server-side settings" in metadata["description"] + assert "ordinary local Git commits" in metadata["description"] + assert interface["display_name"] == "Maintain GitHub Repository" + assert "$maintain-github-repository" in interface["default_prompt"] + + +def test_settings_reference_covers_recommended_baseline_and_guards() -> None: + reference = read(SKILL_ROOT / "references" / "github-repository-settings.md") + + for expected in ( + "Dependabot alerts and security updates enabled", + "private vulnerability reporting enabled for public repositories", + "web commit sign-off required when the repository uses DCO", + "preserve documented owner or maintainer direct pushes", + "visibility changes", + "gh repo edit", + "gh api", + ): + assert expected in reference + + +def test_trigger_eval_routes_settings_and_release_requests_separately() -> None: + trigger_eval = read(SKILL_ROOT / "references" / "trigger-eval.md") + + for expected in ( + "Apply my normal GitHub repository settings.", + "Audit this repo's branch protection and Dependabot settings.", + "Release version 1.4.0.", + "Tag this commit and create the GitHub release.", + "route to `maintain-project-repo`", + ): + assert expected in trigger_eval diff --git a/skills/maintain-project-repo/SKILL.md b/skills/maintain-project-repo/SKILL.md new file mode 100644 index 000000000..5c3b90d48 --- /dev/null +++ b/skills/maintain-project-repo/SKILL.md @@ -0,0 +1,182 @@ +--- +name: maintain-project-repo +description: Install or refresh validation, sync, and release tooling. Use to release or publish a version, bump and tag a release, create the GitHub release, prepare a protected-main release, or finish release cleanup and branch accounting. +license: Apache-2.0 +metadata: + semver: 0.2.1 +--- + +# Maintain Project Repo + +## Purpose + +Install or refresh the reusable `maintain-project-repo` toolkit inside a general, SwiftPM, or Xcode repository so validation, shared-sync work, and release steps live in repo-owned local scripts rather than in CI-only glue. `scripts/run_workflow.py` is the runtime entrypoint, and `scripts/install_maintain_project_repo.py` applies the managed file set, writes the profile marker, and keeps the installed profile explicit. + +## When To Use + +- Use this skill when a Swift or Xcode repo needs one local entrypoint for validation, shared sync work, and releases. +- Use this skill when a repo has GitHub Actions or local shell helpers that should become thin wrappers around repo-owned scripts. +- Use this skill when a repo needs a protected-main standard release flow and a submodule-aware release flow. +- Use this skill when the user asks to release or publish a version. +- Use this skill when the user asks to bump versions, tag a release, create a + GitHub release, prepare or merge a protected-main release, or finish release + cleanup and branch accounting. +- Use this skill when the user wants a local-first alternative to putting maintainer logic under `.github/scripts/`. +- Do not use this skill to make ordinary questions, investigations, local edits, or documentation maintenance take a full PR, CI, release, tag, and cleanup path. +- Do not run or recommend the release choreography unless the user is actually asking to release, publish, merge, tag, open a release PR, or prepare the repo for that protected-main release workflow. +- Do not use this skill for app bootstrap, Swift package bootstrap, or AGENTS-only guidance sync by themselves. +- Recommend `bootstrap-swift-package` when the repo does not exist yet and package scaffold creation is still the primary task. +- Recommend `bootstrap-xcode-app-project` when the repo does not exist yet and native Apple app bootstrap is still the primary task. +- Recommend `sync-swift-package-guidance` or `sync-xcode-project-guidance` when the immediate task is AGENTS alignment rather than `maintain-project-repo` installation. + +## Single-Path Workflow + +1. Collect the required inputs: + - `repo_root` + - optional `operation` + - optional `skip_github_workflow` + - optional `dry_run` +2. Classify the repo and profile: + - prefer `maintain-project-repo` for SwiftPM repos, Xcode app repos, mixed Apple repos, and general software repos that need local maintainer automation + - choose `swift-package` for plain Swift package repos + - choose `xcode-app` for one native Apple app project + - choose `xcode-workspace` for a modular Apple root with one `.xcworkspace`, `Apps/`, `Packages/`, and optional `Services/` + - choose `generic` when no stronger Swift or Xcode profile applies + - stop if the requested path is not a repository root + - use `scripts/repo-maintenance/` for `generic` and `swift-package`; use `Scripts/repo-maintenance/` for `xcode-app` and `xcode-workspace` so Apple repos keep one standard top-level `Scripts/` directory +3. Explain the architecture boundary before mutating anything: + - this is a durable building-block change because it creates one repo-owned maintainer surface that bootstrap, sync, validation, CI, and release flows can all share + - it removes the pain of CI-only helper scripts and scattered release glue + - the simpler extension path considered first was leaving helper scripts under `.github/scripts/` and adding more workflow-specific wrappers, but that would keep local and CI behavior drifting apart + - preserve machine-level Git defaults such as Gale's fetch pruning, + fast-forward-only pulls, and tracking-branch rebases; the installer does + not write `git config --local` because generated repositories must remain + portable across contributor machines +4. Run `scripts/run_workflow.py` to normalize the inputs and choose the installer path. +5. Apply the managed `maintain-project-repo` files: + - install or refresh the managed repo-maintenance files under the selected profile's toolkit root + - install or refresh the selected profile's `config/profile.env` + - install or refresh the thin workflow wrapper at `.github/workflows/validate-repo-maintenance.yml` unless disabled + - for `xcode-app`, migrate an existing legacy `scripts/repo-maintenance/` toolkit root to `Scripts/repo-maintenance/` when the capitalized root is absent; stop if both roots exist separately so the user can preserve intentional custom files before retrying + - preserve repo-specific scripts or files that are not part of the managed file set +6. Verify the installed `maintain-project-repo` files: + - `scripts/repo-maintenance/validate-all.sh` for `generic` and `swift-package`, or `Scripts/repo-maintenance/validate-all.sh` for `xcode-app` + - `scripts/repo-maintenance/sync-shared.sh` for `generic` and `swift-package`, or `Scripts/repo-maintenance/sync-shared.sh` for `xcode-app` + - `scripts/repo-maintenance/release.sh` for `generic` and `swift-package`, or `Scripts/repo-maintenance/release.sh` for `xcode-app` + - `.github/workflows/validate-repo-maintenance.yml` when workflow installation is enabled + - branch protection, when enabled, requires the GitHub Actions check context `validate`; do not require the display-style string `Validate Repo Maintenance / validate` +7. Hand off GitHub repository settings work: + - use `maintain-github-repository` for repository features, merge methods, + Dependabot, secret scanning, push protection, vulnerability reporting, + sign-off policy, branch protection, and rulesets + - keep settings alignment separate from release choreography +8. Hand off follow-on work cleanly: + - use the selected profile's `validate-all.sh` for local validation + - use the selected profile's `sync-shared.sh` for repo-local shared sync tasks + - use the selected profile's `release.sh --mode standard --operation prepare` from a feature branch or worktree when protected `main` owns the final release line + - for remote CI, review bots, deployment, or GitHub indexing, consume the emitted continuation packet and first reuse the live matching host-native continuation while the gate remains pending and healthy; do not delete/recreate it for an unchanged snapshot. Codex uses a same-thread heartbeat and Hermes uses an updated continuable `cronjob` with `deliver="origin"` and `attach_to_session=true`; pause/delete only when the gate resolves, fails, is cancelled, or changes identity + - on wakeup, run `--operation inspect` first; run `--operation advance` only if the branch, commit, PR, and tag identities still match the continuation packet. Treat pending review-bot status contexts such as CodeRabbit as a wait state, not permission to merge, except when CodeRabbit explicitly reports that a quota, usage, rate, or review limit prevented it from producing a review; that diagnostic is non-blocking while every other check, finding, requested change, and comment remains gated + - use `scripts/repo-maintenance/release.sh --mode submodule` only when the repo is checked out as a submodule and the parent pointer update remains a separate follow-up + - treat SemVer tags with prerelease suffixes such as `vX.Y.Z-alpha.N`, `vX.Y.Z-beta.N`, `vX.Y.Z-rc.N`, or preview-style suffixes as GitHub prereleases; the release script passes `--prerelease` for those tags and rejects existing release objects whose prerelease metadata does not match the tag + - before claiming a release, publish, merge, or cleanup step is done, enumerate every local branch still not contained by the local base branch and account for each one as already preserved elsewhere, intentionally still in progress, newly archived, newly merged, or safe to delete + - verify commit reachability in the exact local repository and remote before saying work is on `main`, merged, recovered, preserved, or safe to clean up + - do not delete local branches, remote branches, worktrees, archive refs, or temporary rescue refs until branch accounting is complete and any non-base history is merged or preserved on an explicit archive ref + +## Inputs + +- `repo_root`: optional absolute or relative path to the repository root; defaults to `.` +- `operation`: `install`, `refresh`, or `report-only` +- `profile`: `generic`, `swift-package`, `xcode-app`, or `xcode-workspace` +- `skip_github_workflow`: optional flag to skip `.github/workflows/validate-repo-maintenance.yml` +- `dry_run`: optional flag to report the managed actions without writing files +- Defaults: + - runtime entrypoint: executable `scripts/run_workflow.py` + - `repo_root=.` when omitted + - `operation=install` + - `profile=generic` + - GitHub workflow installation is enabled unless explicitly skipped + +## Outputs + +- `status` + - `success`: `maintain-project-repo` is installed, refreshed, or reported successfully + - `blocked`: the requested repo root or installer preconditions are invalid + - `failed`: the installer started but did not complete successfully +- `path_type` + - `primary`: the managed installer path completed + - `fallback`: a non-mutating report-only result was returned +- `output` + - resolved repo root + - normalized inputs + - selected profile + - managed file list + - planned or applied actions + - one concise next step + +## Guards and Stop Conditions + +- Stop with `blocked` if the repo root does not exist. +- Stop with `blocked` if the repo root is not a directory. +- Stop with `blocked` if the managed target paths are blocked by non-regular files that cannot be updated safely. +- Stop with `blocked` if the requested operation is unsupported. + +## Fallbacks and Handoffs + +- `report-only` is the non-mutating fallback path. +- The installer preserves repo-specific extra files under the selected profile's repo-maintenance root, `.github/workflows/`, and adjacent surfaces when they are not part of the managed file set. +- The installer keeps the selected `maintain-project-repo` profile explicit via the selected profile's `config/profile.env`. +- The installer does not write repository-local Git defaults. Its release script + uses explicit `git pull --ff-only` where protected-main safety must not depend + on a caller's global configuration. +- Apple profiles install checked-in `.swiftformat` and `.swiftlint.yml` samples so SwiftFormat owns formatting shape while SwiftLint stays focused on complementary safety and clarity checks. +- The generated workflow's branch-protection check context is `validate`; GitHub exposes the job check run by that context, not by the workflow title plus job name. +- The generated GitHub Actions wrapper uses Node 24-compatible Actions versions, with `actions/checkout@v6.0.2` as the current validated floor. Newer stable official action versions are allowed and often preferred after checking release notes and running the relevant validation. Apple profiles report the runner-selected Xcode with shell commands instead of using the Node 20-based `maxim-lobanov/setup-xcode@v1` action. +- Standard release mode has bounded `prepare`, `inspect`, and `advance` operations. It never watches or polls remote state: it reuses a live matching host-native continuation while its gate is pending and healthy, creates/updates one only after it fires or becomes stale, resumes with `inspect`, and advances only after identity checks still match the packet. Every scheduled interval is at least five minutes. +- GitHub release creation preserves prerelease metadata for SemVer prerelease tags and fails clearly when an existing GitHub release object disagrees with the tag. +- GitHub release creation prefers checked-in `docs/releases/vX.Y.Z.md` notes, then `docs/releases/X.Y.Z.md`; it logs and falls back to GitHub-generated notes only when neither file exists. +- Treat branch accounting as a hard completion gate for release and cleanup work, not as follow-up tidying. If `git branch --no-merged ` reports local branches after a merge, account for each branch explicitly before deleting anything or reporting the workflow complete. +- Recommend `bootstrap-swift-package` or `bootstrap-xcode-app-project` when the repo still needs to be created. +- Recommend `sync-swift-package-guidance` or `sync-xcode-project-guidance` when AGENTS alignment is still the missing baseline after `maintain-project-repo` is present. + +## Codex Subagent Fit + +When delegation is explicitly requested or authorized, follow `agent-engineering-skills:orchestrate-agent-work`. This skill is a good fit for read-heavy repo-maintenance discovery before the main workflow installs, refreshes, or reports: inspecting existing validation scripts, checking CI wrapper shape, reading release docs, or inventorying repo-specific commands in separate directories. + +Keep managed file installation, refresh, and release guidance in the main thread unless the user explicitly requests parallel implementation with disjoint write scopes. Subagents should return concise findings and file references so the main thread can make one coherent decision about the managed toolkit. + +## Codex Hooks Fit + +This skill may document Codex Hooks as an adjacent Codex runtime surface, but it should not install or manage Codex Hooks as part of the current `maintain-project-repo` file set. Keep Codex Hooks distinct from git pre-commit hooks, `scripts/repo-maintenance/hooks/`, validation scripts, and GitHub Actions wrappers. + +When a repo needs Codex Hooks guidance, record that hooks are enabled by default, may be disabled with `features.hooks = false`, may live in `hooks.json` or inline `[hooks]` config, and should name the lifecycle event, matcher, stable script path, and expected effect. Recommend a future dedicated `maintain-project-hooks` workflow when the user wants deterministic hook auditing or scaffolding. + +## Customization + +- Use `references/customization-flow.md`. +- `scripts/customization_config.py` stores and reports customization state. +- The current customization surface is one policy-only default for release mode preference. Installation shape, profile selection, standard-mode branch release behavior, and managed file selection are explicit workflow behavior, not durable runtime customization. + +## References + +### Workflow References + +- `references/repo-maintenance-layout.md` +- `references/release-modes.md` +- `references/pre-commit-vs-ci.md` +- `references/trigger-eval.md` + +### Contract References + +- `references/automation-prompts.md` +- `references/customization-flow.md` + +### Support References + +- `assets/repo-maintenance/` +- `assets/github/repo-maintenance-workflows/validate-repo-maintenance.yml` + +### Script Inventory + +- `scripts/run_workflow.py` +- `scripts/install_maintain_project_repo.py` +- `scripts/customization_config.py` diff --git a/skills/maintain-project-repo/agents/openai.yaml b/skills/maintain-project-repo/agents/openai.yaml new file mode 100644 index 000000000..15e25fa44 --- /dev/null +++ b/skills/maintain-project-repo/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Maintain Project Repo" + short_description: "Install repo tooling or run protected-main releases" + default_prompt: "Use $maintain-project-repo to install or refresh repo-owned validation, sync, and release tooling, or to execute an explicitly requested protected-main release, publish, tag, GitHub release, cleanup, and branch-accounting workflow." diff --git a/skills/maintain-project-repo/assets/github/repo-maintenance-workflows/validate-repo-maintenance.yml b/skills/maintain-project-repo/assets/github/repo-maintenance-workflows/validate-repo-maintenance.yml new file mode 100644 index 000000000..e46c46492 --- /dev/null +++ b/skills/maintain-project-repo/assets/github/repo-maintenance-workflows/validate-repo-maintenance.yml @@ -0,0 +1,22 @@ +name: Validate Repo Maintenance + +# Branch protection should require the Actions check context `validate`. +# GitHub exposes the job check run by this job name, not by the workflow title. + +on: + pull_request: + push: + branches: + - main + +jobs: + validate: + name: validate + runs-on: macos-latest + steps: + # This is a validated floor, not a ceiling; update to newer stable official versions when validated. + - uses: actions/checkout@v6.0.2 + - name: Install Swift repo-maintenance tools + run: brew install swiftformat swiftlint + - name: Run repo-maintenance validation + run: bash scripts/repo-maintenance/validate-all.sh diff --git a/skills/maintain-project-repo/assets/profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml b/skills/maintain-project-repo/assets/profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml new file mode 100644 index 000000000..5e055903d --- /dev/null +++ b/skills/maintain-project-repo/assets/profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml @@ -0,0 +1,26 @@ +name: Validate Repo Maintenance + +# Branch protection should require the Actions check context `validate`. +# GitHub exposes the job check run by this job name, not by the workflow title. + +on: + pull_request: + push: + branches: + - main + +jobs: + validate: + name: validate + runs-on: macos-26 + steps: + # This is a validated floor, not a ceiling; update to newer stable official versions when validated. + - uses: actions/checkout@v6.0.2 + - name: Report selected Xcode + run: xcode-select --print-path + - name: Report Swift toolchain + run: xcrun swift --version + - name: Install Swift repo-maintenance tools + run: brew install swiftformat swiftlint + - name: Run repo-maintenance validation + run: bash scripts/repo-maintenance/validate-all.sh diff --git a/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/.swiftformat b/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/.swiftformat new file mode 100644 index 000000000..62ecf8804 --- /dev/null +++ b/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/.swiftformat @@ -0,0 +1,92 @@ +# Exported from Gale's SwiftFormat for Xcode settings and curated for repo use. +# This file is the repository source of truth for formatting. If the host app +# configuration changes later, re-export from the shared SwiftFormat settings +# and review the diff before importing it back into the app. + +--rules andOperator,anyObjectProtocol,applicationMain,assertionFailures,blankLineAfterImports,blankLinesAfterGuardStatements,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenChainedFunctions,blankLinesBetweenImports,blankLinesBetweenScopes,braces,conditionalAssignment,consecutiveBlankLines,consecutiveSpaces,consistentSwitchCaseSpacing,docComments,docCommentsBeforeModifiers,duplicateImports,elseOnSameLine,emptyBraces,emptyExtensions,enumNamespaces,environmentEntry,extensionAccessControl,fileMacro,genericExtensions,headerFileName,hoistAwait,hoistPatternLet,hoistTry,indent,initCoderUnavailable,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,modifierOrder,noForceTryInTests,noForceUnwrapInTests,noGuardInTests,numberFormatting,opaqueGenericParameters,organizeDeclarations,preferFinalClasses,privateStateVariables,redundantAsync,redundantBackticks,redundantBreak,redundantClosure,redundantEquatable,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantInternal,redundantLet,redundantLetError,redundantMemberwiseInit,redundantNilInit,redundantObjc,redundantOptionalBinding,redundantParens,redundantPattern,redundantPublic,redundantRawValues,redundantReturn,redundantSelf,redundantSendable,redundantStaticSelf,redundantSwiftTestingSuite,redundantThrows,redundantType,redundantTypedThrows,redundantVariable,redundantViewBuilder,semicolons,simplifyGenericConstraints,sortDeclarations,sortImports,sortTypealiases,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,strongOutlets,strongifiedSelf,swiftTestingTestCaseNames,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,validateTestCases,void,wrap,wrapArguments,wrapAttributes,wrapLoopBodies,wrapMultilineFunctionChains,wrapSingleLineComments,yodaConditions + +--acronyms ID,URL,UUID +--allow-partial-wrapping true +--anonymous-for-each convert +--asset-literals visual-width +--binary-grouping 4,8 +--line-between-guards false +--category-mark "MARK: %c" +--class-threshold 0 +--closing-paren balanced +--closure-void remove +--complex-attributes preserve +--computed-var-attributes preserve +--conditional-assignment after-property +--date-format system +--decimal-grouping 3,6 +--doc-comments before-declarations +--else-position same-line +--empty-braces no-space +--enum-namespaces always +--equatable-macro none +--exponent-case lowercase +--extension-acl on-extension +--file-macro "#file" +--func-attributes preserve +--group-blank-lines true +--guard-else auto +--header ignore +--hex-grouping 4,8 +--hex-literal-case uppercase +--ifdef outdent +--import-grouping alpha,access-control +--indent 4 +--indent-case true +--indent-strings false +--inferred-types always +--init-coder-nil false +--line-after-marks true +--linebreaks lf +--mark-categories false +--mark-class-threshold 40 +--mark-enum-threshold 40 +--mark-extension-threshold 40 +--mark-struct-threshold 40 +--max-width none +--operator-func spaced +--organization-mode type +--organize-types actor,class,enum,struct +--pattern-let hoist +--prefer-synthesized-init-for-internal-structs never +--property-types infer-locals-only +--ranges no-space +--redundant-async tests-only +--redundant-throws tests-only +--self remove +--semicolons inline-only +--short-optionals preserve-struct-inits +--smart-tabs enabled +--some-any true +--sort-swiftui-properties alphabetize +--stored-var-attributes preserve +--struct-threshold 40 +--strip-unused-args always +--suite-name-format standard-identifiers +--test-case-name-format raw-identifiers +--timezone system +--trailing-commas always +--trim-whitespace always +--type-attributes preserve +--type-blank-lines remove +--type-body-marks preserve +--type-delimiter space-after +--class-threshold 40 +--enum-threshold 40 +--extension-threshold 40 +--void-type Void +--wrap-arguments preserve +--wrap-collections preserve +--wrap-conditions preserve +--wrap-effects preserve +--wrap-return-type preserve +--wrap-string-interpolation default +--wrap-ternary default +--wrap-type-aliases preserve +--xcode-indentation disabled +--yoda-swap always diff --git a/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/.swiftlint.yml b/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/.swiftlint.yml new file mode 100644 index 000000000..715bb5c84 --- /dev/null +++ b/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/.swiftlint.yml @@ -0,0 +1,20 @@ +# Keep SwiftLint focused on non-formatting checks. +# SwiftFormat owns visual shape in this repository. + +excluded: + - .build + - .local + +only_rules: + - duplicate_imports + - empty_count + - fatal_error_message + - force_try + - force_unwrapping + - unused_import + +force_try: + severity: warning + +force_unwrapping: + severity: warning diff --git a/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/hooks/pre-commit.sample b/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/hooks/pre-commit.sample new file mode 100755 index 000000000..8fc8726c2 --- /dev/null +++ b/skills/maintain-project-repo/assets/profiles/apple/repo-maintenance/hooks/pre-commit.sample @@ -0,0 +1,35 @@ +#!/usr/bin/env sh +set -eu + +repo_root="$(git rev-parse --show-toplevel)" +config_file="$repo_root/.swiftformat" +staged_file_list="$(mktemp "${TMPDIR:-/tmp}/swiftformat-staged.XXXXXX")" +trap 'rm -f "$staged_file_list"' EXIT HUP INT TERM + +if ! command -v swiftformat >/dev/null 2>&1; then + echo "SwiftFormat pre-commit hook could not find the \`swiftformat\` CLI on PATH. Install SwiftFormat before committing, or bypass once with --no-verify if you are unblocking an emergency." >&2 + exit 1 +fi + +if [ ! -f "$config_file" ]; then + echo "SwiftFormat pre-commit hook expected a checked-in config at $config_file, but it was missing. Restore the managed .swiftformat file or refresh maintain-project-repo before committing." >&2 + exit 1 +fi + +cd "$repo_root" +git diff --cached --name-only --diff-filter=ACMR -- '*.swift' > "$staged_file_list" + +if [ ! -s "$staged_file_list" ]; then + exit 0 +fi + +echo "Running SwiftFormat on staged Swift sources..." +swiftformat --config "$config_file" --filelist "$staged_file_list" + +while IFS= read -r relative_path; do + [ -n "$relative_path" ] || continue + git add -- "$relative_path" +done < "$staged_file_list" + +echo "Verifying staged Swift sources with SwiftFormat lint..." +swiftformat --lint --config "$config_file" --filelist "$staged_file_list" diff --git a/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh b/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh new file mode 100644 index 000000000..946047d2e --- /dev/null +++ b/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +require_exactly_one_workspace() { + workspace_count=$(find "$REPO_ROOT" -maxdepth 1 -type d -name '*.xcworkspace' -print | wc -l | tr -d ' ') + [ "$workspace_count" -eq 1 ] || die "The xcode-workspace profile requires exactly one root .xcworkspace; found $workspace_count." +} + +require_apps_and_packages() { + [ -d "$REPO_ROOT/Apps" ] || die "The xcode-workspace profile requires Apps/ at the repository root." + [ -d "$REPO_ROOT/Packages" ] || die "The xcode-workspace profile requires Packages/ at the repository root." + + app_count=$(find "$REPO_ROOT/Apps" -type d -name '*.xcodeproj' -print | wc -l | tr -d ' ') + [ "$app_count" -gt 0 ] || die "The xcode-workspace profile requires at least one .xcodeproj under Apps/." + + package_count=$(find "$REPO_ROOT/Packages" -type f -name 'Package.swift' -print | wc -l | tr -d ' ') + [ "$package_count" -gt 0 ] || die "The xcode-workspace profile requires at least one Package.swift under Packages/." +} + +require_exactly_one_workspace +require_apps_and_packages +log "Validated xcode-workspace composition: one root workspace, Apps/, and Packages/." diff --git a/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh b/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh new file mode 100644 index 000000000..b5904d10a --- /dev/null +++ b/skills/maintain-project-repo/assets/profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh + +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$REPO_MAINTENANCE_COMMON_DIR/common.sh" + +run_component_validation() { + component_root=$1 + component_kind=$2 + for candidate in \ + "$component_root/Scripts/repo-maintenance/validate-all.sh" \ + "$component_root/scripts/repo-maintenance/validate-all.sh" + do + if [ -f "$candidate" ]; then + log "Validating $component_kind component at $component_root with ${candidate#"$component_root/"}." + sh "$candidate" + return 0 + fi + done + log "No component-owned repo-maintenance validation found for $component_kind at $component_root; skipping." +} + +find "$REPO_ROOT/Apps" -type d -name '*.xcodeproj' -print | sort | while IFS= read -r project; do + run_component_validation "$(dirname -- "$project")" "app" +done + +find "$REPO_ROOT/Packages" -type f -name 'Package.swift' -print | sort | while IFS= read -r manifest; do + run_component_validation "$(dirname -- "$manifest")" "package" +done + +if [ -d "$REPO_ROOT/Services" ]; then + find "$REPO_ROOT/Services" -mindepth 1 -maxdepth 1 -type d -print | sort | while IFS= read -r service; do + run_component_validation "$service" "service" + done +fi diff --git a/skills/maintain-project-repo/assets/repo-maintenance/config/release.env b/skills/maintain-project-repo/assets/repo-maintenance/config/release.env new file mode 100644 index 000000000..a726d51b1 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/config/release.env @@ -0,0 +1,16 @@ +# Repo-maintenance release defaults. +REPO_MAINTENANCE_DEFAULT_RELEASE_MODE=standard +REPO_MAINTENANCE_RELEASE_BRANCH=main +REPO_MAINTENANCE_RELEASE_OPERATION=prepare +# Require one check by default. A repository that intentionally has no remote +# checks may explicitly set this to 0; do not infer permission to advance. +REPO_MAINTENANCE_MIN_REQUIRED_CHECKS=1 + +# GitHub can accept branch, tag, PR, check, review, and release mutations before +# those surfaces are immediately readable. The release script performs one +# bounded re-read, emits a continuation packet when it is not ready, and exits. +# Agents first reuse a live matching host-native continuation while its gate is +# pending and healthy; do not delete/recreate it after an unchanged snapshot. +# Create/update only after it fires or becomes stale, no sooner than five +# minutes later, then run --operation inspect before any --operation advance. +# Never add a shell poll loop or a shorter agent recheck interval here. diff --git a/skills/maintain-project-repo/assets/repo-maintenance/config/validation.env b/skills/maintain-project-repo/assets/repo-maintenance/config/validation.env new file mode 100644 index 000000000..c85b14789 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/config/validation.env @@ -0,0 +1,2 @@ +# Repo-maintenance validation defaults. +REPO_MAINTENANCE_REQUIRE_AGENTS=true diff --git a/skills/maintain-project-repo/assets/repo-maintenance/hooks/pre-commit.sample b/skills/maintain-project-repo/assets/repo-maintenance/hooks/pre-commit.sample new file mode 100755 index 000000000..5749ac2ae --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/hooks/pre-commit.sample @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +set -eu + +repo_root="$(git rev-parse --show-toplevel)" +exec "$repo_root/scripts/repo-maintenance/validate-all.sh" diff --git a/skills/maintain-project-repo/assets/repo-maintenance/lib/coderabbit.sh b/skills/maintain-project-repo/assets/repo-maintenance/lib/coderabbit.sh new file mode 100644 index 000000000..0ca7931df --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/lib/coderabbit.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +# Return success only when CodeRabbit explicitly reports that a review could +# not be produced because its quota, usage, or review limit was reached. +coderabbit_review_is_unavailable() { + source_name="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + message="$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]')" + + case "$source_name" in + *coderabbit*) + ;; + *) + return 1 + ;; + esac + + case "$message" in + *quota*|*"usage limit"*|*"rate limit"*|*"review limit"*|*"limit reached"*) + return 0 + ;; + *) + return 1 + ;; + esac +} diff --git a/skills/maintain-project-repo/assets/repo-maintenance/lib/common.sh b/skills/maintain-project-repo/assets/repo-maintenance/lib/common.sh new file mode 100755 index 000000000..5d740c46f --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/lib/common.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env sh +set -eu + +COMMON_DIR="${REPO_MAINTENANCE_COMMON_DIR:-}" + +if [ -z "$COMMON_DIR" ]; then + COMMON_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +fi + +REPO_MAINTENANCE_ROOT=$(CDPATH= cd -- "$COMMON_DIR/.." && pwd) +REPO_ROOT=$(CDPATH= cd -- "$REPO_MAINTENANCE_ROOT/../.." && pwd) +REPO_MAINTENANCE_PROFILE="generic" +REPO_MAINTENANCE_PROFILE_DESCRIPTION="Generic repo-maintenance baseline with no Swift or Xcode specialization." + +log() { + printf '%s\n' "$*" +} + +warn() { + printf 'WARN: %s\n' "$*" >&2 +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +load_env_file() { + env_file="$1" + [ -f "$env_file" ] || return 0 + set -a + # shellcheck disable=SC1090 + . "$env_file" + set +a +} + +load_profile_env() { + load_env_file "$REPO_MAINTENANCE_ROOT/config/profile.env" +} + +positive_integer_or_default() { + value="$1" + default_value="$2" + + case "$value" in + ''|*[!0-9]*) + printf '%s\n' "$default_value" + ;; + 0) + printf '%s\n' "$default_value" + ;; + *) + printf '%s\n' "$value" + ;; + esac +} + +is_semver_prerelease_tag() { + tag_name="$1" + case "$tag_name" in + v[0-9]*.[0-9]*.[0-9]*-*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +expected_github_prerelease_value() { + tag_name="$1" + if is_semver_prerelease_tag "$tag_name"; then + printf '%s\n' "true" + else + printf '%s\n' "false" + fi +} + +github_release_create_prerelease_flag() { + tag_name="$1" + if is_semver_prerelease_tag "$tag_name"; then + printf '%s\n' "--prerelease" + fi +} + +verify_github_release_prerelease_metadata() { + tag_name="$1" + expected_value="$(expected_github_prerelease_value "$tag_name")" + + actual_value="$(gh release view "$tag_name" --json isPrerelease --jq .isPrerelease 2>/dev/null || true)" + case "$actual_value" in + true|false) + ;; + *) + die "GitHub release $tag_name exists, but its prerelease metadata was not readable. Confirm gh can read release JSON metadata before rerunning release.sh." + ;; + esac + + [ "$actual_value" = "$expected_value" ] || die "GitHub release $tag_name prerelease metadata mismatch: tag implies isPrerelease=$expected_value but GitHub reports isPrerelease=$actual_value. Update the release metadata or delete and recreate the release before rerunning release.sh." +} + +remote_branch_is_visible() { + branch_name="$1" + git -C "$REPO_ROOT" ls-remote --exit-code --heads origin "$branch_name" >/dev/null 2>&1 +} + +remote_tag_is_visible() { + tag_name="$1" + git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$tag_name" >/dev/null 2>&1 +} + +github_release_is_visible() { + tag_name="$1" + gh release view "$tag_name" >/dev/null 2>&1 +} + +checked_in_release_notes_file() { + tag_name="$1" + version_name="${tag_name#v}" + + for candidate in \ + "$REPO_ROOT/docs/releases/$tag_name.md" \ + "$REPO_ROOT/docs/releases/$version_name.md"; do + if [ -f "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +create_github_release_from_notes_or_generated() { + tag_name="$1" + prerelease_flag="${2:-}" + + if notes_file="$(checked_in_release_notes_file "$tag_name")"; then + log "Creating GitHub release $tag_name from checked-in notes: $notes_file." + # shellcheck disable=SC2086 + gh release create "$tag_name" --verify-tag --notes-file "$notes_file" $prerelease_flag + return 0 + fi + + log "No checked-in release notes found for $tag_name; using GitHub-generated release notes." + # shellcheck disable=SC2086 + gh release create "$tag_name" --verify-tag --generate-notes $prerelease_flag +} + +ensure_git_repo() { + git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "maintain-project-repo must run inside a git worktree rooted at $REPO_ROOT." +} + +run_dispatch_dir() { + dir="$1" + label="$2" + ran_any="false" + + for script in "$dir"/*.sh; do + [ -e "$script" ] || continue + ran_any="true" + log "Running $label step $(basename "$script")" + sh "$script" + done + + if [ "$ran_any" = "false" ]; then + log "No $label steps are currently defined under $dir." + fi +} diff --git a/skills/maintain-project-repo/assets/repo-maintenance/release.sh b/skills/maintain-project-repo/assets/repo-maintenance/release.sh new file mode 100755 index 000000000..5999f0de0 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/release.sh @@ -0,0 +1,540 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/lib" +. "$SELF_DIR/lib/common.sh" +. "$SELF_DIR/lib/coderabbit.sh" + +load_profile_env +load_env_file "$SELF_DIR/config/release.env" + +mode="${REPO_MAINTENANCE_DEFAULT_RELEASE_MODE:-standard}" +release_tag="" +skip_validate="false" +skip_gh_release="false" +skip_version_bump="false" +base_branch="${REPO_MAINTENANCE_RELEASE_BRANCH:-main}" +review_comments_addressed="false" +skip_branch_cleanup="false" +dry_run="false" +operation="${REPO_MAINTENANCE_RELEASE_OPERATION:-prepare}" + +while [ "$#" -gt 0 ]; do + case "$1" in + --mode) + mode="${2:-}" + shift 2 + ;; + --version) + release_tag="${2:-}" + shift 2 + ;; + --skip-validate) + skip_validate="true" + shift + ;; + --skip-gh-release) + skip_gh_release="true" + shift + ;; + --skip-version-bump) + skip_version_bump="true" + shift + ;; + --base-branch) + base_branch="${2:-}" + shift 2 + ;; + --review-comments-addressed) + review_comments_addressed="true" + shift + ;; + --operation) + operation="${2:-}" + shift 2 + ;; + --skip-branch-cleanup) + skip_branch_cleanup="true" + shift + ;; + --dry-run) + dry_run="true" + shift + ;; + -h|--help) + cat <<'USAGE' +Usage: + release.sh --mode standard --version --operation prepare|inspect|advance [--base-branch main] [--skip-validate] [--skip-version-bump] [--skip-gh-release] [--review-comments-addressed] [--skip-branch-cleanup] [--dry-run] + release.sh --mode submodule --version [--skip-validate] [--skip-gh-release] [--dry-run] +USAGE + exit 0 + ;; + *) + die "Unknown release argument: $1" + ;; + esac +done + +[ -n "$release_tag" ] || die "Pass --version vX.Y.Z when running the release workflow." + +export REPO_MAINTENANCE_RELEASE_MODE="$mode" +export RELEASE_TAG="$release_tag" +export REPO_MAINTENANCE_SKIP_GH_RELEASE="$skip_gh_release" +export REPO_MAINTENANCE_DRY_RUN="$dry_run" +export REPO_MAINTENANCE_RELEASE_OPERATION="$operation" + +ensure_clean_worktree() { + status_output="$(git -C "$REPO_ROOT" status --porcelain)" + [ -z "$status_output" ] || die "Release workflow requires committed changes and a clean worktree before it can continue." +} + +ensure_gh_cli() { + command -v gh >/dev/null 2>&1 || die "Standard release mode requires the GitHub CLI gh so it can inspect and advance the pull request, merge, and publish the release." +} + +ensure_semver_tag() { + case "$RELEASE_TAG" in + v[0-9]*.[0-9]*.[0-9]*|v[0-9]*.[0-9]*.[0-9]*-*) + ;; + *) + die "Release tag must use vX.Y.Z SemVer syntax." + ;; + esac +} + +ensure_operation() { + case "$REPO_MAINTENANCE_RELEASE_OPERATION" in + prepare|inspect|advance) + ;; + *) + die "Release operation must be prepare, inspect, or advance. Long-running remote checks must be resumed by a host-native scheduled continuation, never watched from this script." + ;; + esac +} + +current_branch() { + git -C "$REPO_ROOT" symbolic-ref --quiet --short HEAD || true +} + +ensure_branch_release_context() { + branch_name="$(current_branch)" + [ -n "$branch_name" ] || die "Standard release mode requires a named feature branch or worktree instead of detached HEAD." + [ "$branch_name" != "$base_branch" ] || die "Standard release mode must run from a release branch or worktree, not protected $base_branch." + printf '%s\n' "$branch_name" +} + +run_version_bump() { + release_version="${RELEASE_TAG#v}" + version_bump_script="$SELF_DIR/version-bump.sh" + head_subject="$(git -C "$REPO_ROOT" log -1 --format=%s 2>/dev/null || true)" + + if [ "$skip_version_bump" = "true" ]; then + log "Skipping repo version bump because --skip-version-bump was requested." + return 0 + fi + + if [ "$head_subject" = "release: bump versions for $RELEASE_TAG" ]; then + log "Version bump commit for $RELEASE_TAG is already at HEAD; continuing the release resume path." + return 0 + fi + + [ -x "$version_bump_script" ] || die "Standard release mode expected an executable repo-specific version bump hook at $version_bump_script. Add that hook so the repo's version surfaces move together, or rerun with --skip-version-bump when this release intentionally has no version-bearing files." + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would run $version_bump_script $release_version with RELEASE_TAG=$RELEASE_TAG." + return 0 + fi + + RELEASE_VERSION="$release_version" "$version_bump_script" "$release_version" + + if [ -z "$(git -C "$REPO_ROOT" status --porcelain)" ]; then + die "Version bump hook completed without changing files. Update $version_bump_script to edit the repo's version surfaces, or rerun with --skip-version-bump if this release intentionally has no version bump." + fi + + git -C "$REPO_ROOT" add -A + git -C "$REPO_ROOT" commit -m "release: bump versions for $RELEASE_TAG" + log "Committed version bump for $RELEASE_TAG." +} + +create_release_tag() { + head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD)" + tag_sha="$(git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$RELEASE_TAG" 2>/dev/null || true)" + + if [ -n "$tag_sha" ]; then + tag_commit_sha="$(git -C "$REPO_ROOT" rev-list -n 1 "$RELEASE_TAG")" + [ "$tag_commit_sha" = "$head_sha" ] || die "Tag $RELEASE_TAG already exists and does not point at HEAD." + log "Tag $RELEASE_TAG already points at HEAD." + return 0 + fi + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would create annotated tag $RELEASE_TAG at HEAD." + return 0 + fi + + git -C "$REPO_ROOT" tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG" + log "Created annotated tag $RELEASE_TAG." +} + +push_release_branch() { + branch_name="$1" + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would push branch $branch_name to origin." + return 0 + fi + + git -C "$REPO_ROOT" push -u origin "$branch_name" + log "Pushed branch $branch_name." + remote_branch_is_visible "$branch_name" || return 1 +} + +push_release_tag() { + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would push tag $RELEASE_TAG to origin." + return 0 + fi + + git -C "$REPO_ROOT" push origin "$RELEASE_TAG" + log "Pushed tag $RELEASE_TAG." + remote_tag_is_visible "$RELEASE_TAG" || return 1 +} + +create_or_update_pr() { + branch_name="$1" + PR_NUMBER="" + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would create or update a release PR from $branch_name into $base_branch." + PR_NUMBER="DRY-RUN" + return 0 + fi + + body_file="$(mktemp "${TMPDIR:-/tmp}/repo-maintenance-release-pr.XXXXXX")" + trap 'rm -f "$body_file"' EXIT INT TERM + + cat >"$body_file" </dev/null + log "Updated existing release PR #$pr_number at $pr_url." + else + gh pr create --base "$base_branch" --head "$branch_name" --title "release: prepare $RELEASE_TAG" --body-file "$body_file" >/dev/null + pr_number="$(gh pr list --head "$branch_name" --base "$base_branch" --json number --jq '.[0].number // empty' --limit 1)" + [ -n "$pr_number" ] || die "GitHub CLI did not return a release PR number after creating the pull request." + pr_url="$(gh pr view "$pr_number" --json url --jq '.url')" + log "Created release PR #$pr_number at $pr_url." + PR_NUMBER="$pr_number" + return 0 + fi + + PR_NUMBER="$pr_number" +} + +detect_coderabbit_review_unavailable() { + pr_number="$1" + CODERABBIT_REVIEW_UNAVAILABLE="false" + CODERABBIT_UNAVAILABLE_COMMENT_COUNT=0 + + review_lines="$(gh pr view "$pr_number" --json comments,reviews --jq '[.comments[]?, (.reviews[]? | select(.state == "COMMENTED"))] | .[] | [(.author.login // ""), (.body // "")] | @tsv' 2>/dev/null || true)" + while IFS="$(printf '\t')" read -r source_name message; do + [ -n "${source_name:-}" ] || continue + if coderabbit_review_is_unavailable "$source_name" "$message"; then + CODERABBIT_REVIEW_UNAVAILABLE="true" + CODERABBIT_UNAVAILABLE_COMMENT_COUNT=$((CODERABBIT_UNAVAILABLE_COMMENT_COUNT + 1)) + fi + done </dev/null || true)" + if [ -n "$repo_name" ] && [ -n "$head_sha" ]; then + check_lines="$(gh api "repos/$repo_name/commits/$head_sha/check-runs" --paginate --jq '.check_runs[]? | [(.name // ""), ((.output.title // "") + "\\n" + (.output.summary // "") + "\\n" + (.output.text // ""))] | @tsv' 2>/dev/null || true)" + while IFS="$(printf '\t')" read -r source_name message; do + [ -n "${source_name:-}" ] || continue + if coderabbit_review_is_unavailable "$source_name" "$message"; then + CODERABBIT_REVIEW_UNAVAILABLE="true" + fi + done </dev/null)"; then + check_readable="true" + elif [ -n "${CHECK_STATE:-}" ]; then + # gh pr checks exits 8 while pending even when it returned valid JSON output. + check_readable="true" + else + CHECK_STATE="unreadable" + check_readable="false" + fi + if CHECK_BUCKETS="$(gh pr checks "$pr_number" --json bucket --jq 'map(.bucket) | join(",")' 2>/dev/null)"; then + : + elif [ -z "${CHECK_BUCKETS:-}" ]; then + check_readable="false" + fi + if check_count="$(gh pr checks "$pr_number" --json bucket --jq 'length' 2>/dev/null)"; then + : + elif [ -z "${check_count:-}" ]; then + check_readable="false" + fi + REVIEW_DECISION="$(gh pr view "$pr_number" --json reviewDecision --jq '.reviewDecision // ""' 2>/dev/null || printf 'UNREADABLE')" + COMMENT_COUNT="$(gh pr view "$pr_number" --json comments,reviews --jq '([.comments[]?, (.reviews[]? | select(.state == "COMMENTED"))] | length)' 2>/dev/null || printf '1')" + detect_coderabbit_review_unavailable "$pr_number" + if [ "$CODERABBIT_UNAVAILABLE_COMMENT_COUNT" -gt 0 ]; then + COMMENT_COUNT=$((COMMENT_COUNT - CODERABBIT_UNAVAILABLE_COMMENT_COUNT)) + fi + EFFECTIVE_CHECK_BUCKETS="$CHECK_BUCKETS" + if [ "$CODERABBIT_REVIEW_UNAVAILABLE" = "true" ]; then + EFFECTIVE_CHECK_BUCKETS="$(gh pr checks "$pr_number" --json name,bucket --jq 'map(select(.bucket != "pending" or ((.name | ascii_downcase | contains("coderabbit")) | not)) | map(.bucket) | join(",")' 2>/dev/null || printf '%s' "$CHECK_BUCKETS")" + log "CodeRabbit reported that review is unavailable because a limit was reached; ignoring only its pending review context and diagnostic comments." + fi + if [ "$check_readable" != "true" ] || [ "$REVIEW_DECISION" = "UNREADABLE" ]; then + GATE_PHASE="awaiting-github-state" + elif [ "$check_count" -lt "$minimum_check_count" ]; then + GATE_PHASE="awaiting-github-state" + elif case ",$EFFECTIVE_CHECK_BUCKETS," in *,fail,*|*,cancel,*) true ;; *) false ;; esac; then + GATE_PHASE="failed-checks" + elif case ",$EFFECTIVE_CHECK_BUCKETS," in *,pending,*) true ;; *) false ;; esac; then + GATE_PHASE="awaiting-pr-checks" + elif [ "$REVIEW_DECISION" = "CHANGES_REQUESTED" ]; then + GATE_PHASE="changes-requested" + else + GATE_PHASE="ready-to-advance" + fi + log "PR #$pr_number snapshot: phase=$GATE_PHASE; checks=${CHECK_STATE:-none}; review=${REVIEW_DECISION:-none}; comments=$COMMENT_COUNT." +} + +emit_continuation_packet() { + pr_number="$1" + branch_name="$2" + phase="$3" + resume_operation="inspect" + case "$phase" in + not-started|awaiting-branch-visibility) + resume_operation="prepare" + ;; + esac + repo_name="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || printf 'unknown')" + head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD)" + printf '%s\n' "{\"schema\":\"repo-maintenance-continuation/v1\",\"operation\":\"standard-release\",\"repository\":\"$repo_name\",\"release_tag\":\"$RELEASE_TAG\",\"branch\":\"$branch_name\",\"head_commit\":\"$head_sha\",\"pr_number\":\"$pr_number\",\"phase\":\"$phase\",\"minimum_delay_minutes\":5,\"resume_command\":\"scripts/repo-maintenance/release.sh --mode standard --version $RELEASE_TAG --operation $resume_operation\",\"advance_command\":\"scripts/repo-maintenance/release.sh --mode standard --version $RELEASE_TAG --operation advance\"}" + log "Before scheduling, reuse a live matching host-native continuation while this gate is pending and healthy; do not delete/recreate it after an unchanged snapshot. Create or update one only after it fires or becomes stale, no sooner than five minutes. On wakeup run inspect first; run advance only if this branch, commit, PR, and tag still match." +} + +check_pr_comments() { + pr_number="$1" + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would check PR #$pr_number for comments and requested changes." + return 0 + fi + + review_decision="$REVIEW_DECISION" + comment_count="$COMMENT_COUNT" + + if [ "$review_decision" = "CHANGES_REQUESTED" ]; then + gh pr view "$pr_number" --comments + die "PR #$pr_number has requested changes. Address valid concerns in code, or add out-of-scope concerns to ROADMAP.md, resolve the threads, push, and rerun release.sh." + fi + + if [ "$comment_count" != "0" ] && [ "$review_comments_addressed" != "true" ]; then + gh pr view "$pr_number" --comments + die "PR #$pr_number has review or discussion comments. Address and resolve valid concerns, add out-of-scope concerns to ROADMAP.md, then rerun release.sh with --review-comments-addressed once the comment pass is intentionally complete." + fi + + log "PR #$pr_number has no blocking review state." +} + +merge_pr() { + pr_number="$1" + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would merge PR #$pr_number into $base_branch with a merge commit and delete the remote branch." + return 0 + fi + + gh pr merge "$pr_number" --merge --delete-branch + log "Merged PR #$pr_number into $base_branch." +} + +fast_forward_base_branch() { + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would fast-forward local $base_branch from origin/$base_branch." + return 0 + fi + + git -C "$REPO_ROOT" fetch origin "$base_branch" + if git -C "$REPO_ROOT" switch "$base_branch" 2>/dev/null || git -C "$REPO_ROOT" checkout "$base_branch" 2>/dev/null; then + git -C "$REPO_ROOT" pull --ff-only origin "$base_branch" + log "Fast-forwarded local $base_branch." + else + die "Could not check out local $base_branch, likely because another worktree owns it. Fast-forward $base_branch from origin/$base_branch in that checkout, then rerun release.sh so the release tag is created from the reviewed base branch." + fi +} + +create_github_release() { + if [ "$REPO_MAINTENANCE_SKIP_GH_RELEASE" = "true" ]; then + log "Skipping GitHub release creation because --skip-gh-release was requested." + return 0 + fi + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag${prerelease_flag:+ $prerelease_flag}." + return 0 + fi + + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + verify_github_release_prerelease_metadata "$RELEASE_TAG" + log "GitHub release $RELEASE_TAG already exists." + return 0 + fi + + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + create_github_release_from_notes_or_generated "$RELEASE_TAG" "$prerelease_flag" + log "Created GitHub release $RELEASE_TAG." + if ! github_release_is_visible "$RELEASE_TAG"; then + warn "GitHub release $RELEASE_TAG is not readable in this immediate re-read. Schedule a continuation for at least five minutes rather than polling." + return 1 + fi + verify_github_release_prerelease_metadata "$RELEASE_TAG" +} + +cleanup_merged_branches() { + release_branch_name="$1" + + if [ "$skip_branch_cleanup" = "true" ]; then + log "Skipping local merged-branch cleanup because --skip-branch-cleanup was requested." + return 0 + fi + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would prune origin and delete local branches already merged into $base_branch, including $release_branch_name when safe." + return 0 + fi + + git -C "$REPO_ROOT" remote prune origin + for merged_branch in $(git -C "$REPO_ROOT" for-each-ref --format='%(refname:short)' --merged "$base_branch" refs/heads); do + case "$merged_branch" in + "$base_branch") + ;; + *) + git -C "$REPO_ROOT" branch -d "$merged_branch" >/dev/null 2>&1 || warn "Could not delete local merged branch $merged_branch; it may be checked out in another worktree." + ;; + esac + done + log "Cleaned up local branches already merged into $base_branch where safe." +} + +run_standard_release() { + ensure_git_repo + ensure_gh_cli + ensure_semver_tag + ensure_operation + + if [ "$REPO_MAINTENANCE_RELEASE_OPERATION" = "inspect" ]; then + branch_name="$(current_branch)" + if [ -z "$branch_name" ]; then + log "Release inspection state: not-started; no named branch is checked out." + return 0 + fi + pr_number="$(gh pr list --head "$branch_name" --base "$base_branch" --json number --jq '.[0].number // empty' --limit 1)" + if [ -z "$pr_number" ]; then + emit_continuation_packet "pending" "$branch_name" "not-started" + log "Release inspection state: not-started; no release PR exists for branch $branch_name." + return 0 + fi + inspect_pr_gate "$pr_number" + emit_continuation_packet "$pr_number" "$branch_name" "$GATE_PHASE" + return 0 + fi + + branch_name="$(ensure_branch_release_context)" + ensure_clean_worktree + + if [ "$REPO_MAINTENANCE_RELEASE_OPERATION" = "prepare" ] && [ "$skip_validate" != "true" ]; then + sh "$SELF_DIR/validate-all.sh" + fi + + if [ "$REPO_MAINTENANCE_RELEASE_OPERATION" = "prepare" ]; then + run_version_bump + ensure_clean_worktree + if ! push_release_branch "$branch_name"; then + emit_continuation_packet "pending" "$branch_name" "awaiting-branch-visibility" + return 0 + fi + create_or_update_pr "$branch_name" + pr_number="$PR_NUMBER" + inspect_pr_gate "$pr_number" + emit_continuation_packet "$pr_number" "$branch_name" "$GATE_PHASE" + log "Standard release preparation completed for $RELEASE_TAG." + return 0 + fi + + pr_number="$(gh pr list --head "$branch_name" --base "$base_branch" --json number --jq '.[0].number // empty' --limit 1)" + [ -n "$pr_number" ] || die "No release PR exists for branch $branch_name into $base_branch. Run --operation prepare first." + inspect_pr_gate "$pr_number" + case "$GATE_PHASE" in + awaiting-github-state|awaiting-pr-checks) + emit_continuation_packet "$pr_number" "$branch_name" "$GATE_PHASE" + return 0 + ;; + failed-checks|changes-requested) + die "Release PR #$pr_number is in $GATE_PHASE. Resolve the remote gate, push any correction, then use --operation inspect after a scheduled continuation." + ;; + esac + check_pr_comments "$pr_number" + merge_pr "$pr_number" + fast_forward_base_branch + create_release_tag + if ! push_release_tag; then + emit_continuation_packet "$pr_number" "$branch_name" "awaiting-tag-visibility" + return 0 + fi + if ! create_github_release; then + emit_continuation_packet "$pr_number" "$branch_name" "awaiting-github-release-visibility" + return 0 + fi + cleanup_merged_branches "$branch_name" + log "Standard release flow completed successfully for $RELEASE_TAG." +} + +if [ "$mode" = "standard" ]; then + run_standard_release + exit 0 +fi + +if [ "$skip_validate" != "true" ]; then + sh "$SELF_DIR/validate-all.sh" +fi + +log "Running repo-maintenance release flow in $REPO_MAINTENANCE_RELEASE_MODE mode for $RELEASE_TAG with the $REPO_MAINTENANCE_PROFILE profile." +run_dispatch_dir "$SELF_DIR/release" "release" + +if [ "$REPO_MAINTENANCE_RELEASE_MODE" = "submodule" ]; then + log "Submodule release finished. Update the parent repository's submodule pointer in a separate follow-up commit." +fi + +log "Repo-maintenance release flow completed successfully." diff --git a/skills/maintain-project-repo/assets/repo-maintenance/release/10-preflight.sh b/skills/maintain-project-repo/assets/repo-maintenance/release/10-preflight.sh new file mode 100755 index 000000000..1e6a12e45 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/release/10-preflight.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +ensure_git_repo + +case "${REPO_MAINTENANCE_RELEASE_MODE:-}" in + standard|submodule) + ;; + *) + die "Release mode must be standard or submodule." + ;; +esac + +case "${RELEASE_TAG:-}" in + v[0-9]*.[0-9]*.[0-9]*|v[0-9]*.[0-9]*.[0-9]*-*) + ;; + *) + die "Release tag must use vX.Y.Z SemVer syntax." + ;; +esac + +branch_name="$(git -C "$REPO_ROOT" symbolic-ref --quiet --short HEAD || true)" +[ -n "$branch_name" ] || die "Release workflow requires a named branch instead of detached HEAD." + +status_output="$(git -C "$REPO_ROOT" status --porcelain)" +[ -z "$status_output" ] || die "Release workflow requires a clean worktree before tagging." + +if [ "${REPO_MAINTENANCE_RELEASE_MODE:-}" = "submodule" ]; then + superproject_root="$(git -C "$REPO_ROOT" rev-parse --show-superproject-working-tree || true)" + [ -n "$superproject_root" ] || die "Submodule release mode requires this repository to be checked out as a git submodule." +fi diff --git a/skills/maintain-project-repo/assets/repo-maintenance/release/20-tag-release.sh b/skills/maintain-project-repo/assets/repo-maintenance/release/20-tag-release.sh new file mode 100755 index 000000000..80e147ba0 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/release/20-tag-release.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD)" +tag_sha="$(git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$RELEASE_TAG" 2>/dev/null || true)" + +if [ -n "$tag_sha" ]; then + [ "$tag_sha" = "$head_sha" ] || die "Tag $RELEASE_TAG already exists and does not point at HEAD." + log "Tag $RELEASE_TAG already points at HEAD." + exit 0 +fi + +if [ "${REPO_MAINTENANCE_DRY_RUN:-false}" = "true" ]; then + log "Would create annotated tag $RELEASE_TAG at HEAD." + exit 0 +fi + +git -C "$REPO_ROOT" tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG" +log "Created annotated tag $RELEASE_TAG." diff --git a/skills/maintain-project-repo/assets/repo-maintenance/release/30-push-release.sh b/skills/maintain-project-repo/assets/repo-maintenance/release/30-push-release.sh new file mode 100755 index 000000000..148ba4769 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/release/30-push-release.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +branch_name="$(git -C "$REPO_ROOT" symbolic-ref --quiet --short HEAD)" + +if [ "${REPO_MAINTENANCE_DRY_RUN:-false}" = "true" ]; then + log "Would push branch $branch_name and tag $RELEASE_TAG to origin." + exit 0 +fi + +git -C "$REPO_ROOT" push -u origin "$branch_name" +remote_branch_is_visible "$branch_name" || die "Remote branch origin/$branch_name is not visible in this immediate re-read. Do not poll; schedule a host-native continuation for at least five minutes, then re-run the release step." +git -C "$REPO_ROOT" push origin "$RELEASE_TAG" +remote_tag_is_visible "$RELEASE_TAG" || die "Remote tag $RELEASE_TAG is not visible in this immediate re-read. Do not poll; schedule a host-native continuation for at least five minutes, then re-run the release step." +log "Pushed branch $branch_name and tag $RELEASE_TAG." diff --git a/skills/maintain-project-repo/assets/repo-maintenance/release/40-github-release.sh b/skills/maintain-project-repo/assets/repo-maintenance/release/40-github-release.sh new file mode 100755 index 000000000..e78221fe8 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/release/40-github-release.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +if [ "${REPO_MAINTENANCE_SKIP_GH_RELEASE:-false}" = "true" ]; then + log "Skipping GitHub release creation because --skip-gh-release was requested." + exit 0 +fi + +if ! command -v gh >/dev/null 2>&1; then + warn "gh is unavailable, so the release tag was pushed without creating a GitHub release object." + exit 0 +fi + +if [ "${REPO_MAINTENANCE_DRY_RUN:-false}" = "true" ]; then + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag${prerelease_flag:+ $prerelease_flag}." + exit 0 +fi + +if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + verify_github_release_prerelease_metadata "$RELEASE_TAG" + log "GitHub release $RELEASE_TAG already exists." + exit 0 +fi + +prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" +create_github_release_from_notes_or_generated "$RELEASE_TAG" "$prerelease_flag" +log "Created GitHub release $RELEASE_TAG." +github_release_is_visible "$RELEASE_TAG" || die "GitHub release $RELEASE_TAG is not readable in this immediate re-read. Do not poll; schedule a host-native continuation for at least five minutes, then re-run the release step." +verify_github_release_prerelease_metadata "$RELEASE_TAG" diff --git a/skills/maintain-project-repo/assets/repo-maintenance/sync-shared.sh b/skills/maintain-project-repo/assets/repo-maintenance/sync-shared.sh new file mode 100755 index 000000000..5a00c94aa --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/sync-shared.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/lib" +. "$SELF_DIR/lib/common.sh" + +load_profile_env +ensure_git_repo +log "Running repo-maintenance shared sync from $REPO_ROOT with the $REPO_MAINTENANCE_PROFILE profile." +run_dispatch_dir "$SELF_DIR/syncing" "sync" +log "Repo-maintenance shared sync completed successfully." diff --git a/skills/maintain-project-repo/assets/repo-maintenance/syncing/README.md b/skills/maintain-project-repo/assets/repo-maintenance/syncing/README.md new file mode 100644 index 000000000..66ff612c3 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/syncing/README.md @@ -0,0 +1,31 @@ +# Repo-Maintenance Syncing Steps + +Small helper surface for deterministic repo-maintenance sync hooks. + +## Overview + +This directory holds repo-specific shell hooks that the shared repo-maintenance sync entrypoint can discover and run. + +### Motivation + +It exists so a repository can keep local sync follow-up steps in one predictable place without forking the shared sync entrypoint itself. + +## Setup + +Add repo-specific executable `.sh` files here only when the repository needs deterministic shared-sync follow-up steps. + +## Usage + +The top-level `scripts/repo-maintenance/sync-shared.sh` entrypoint discovers and runs every `*.sh` file in this directory in lexical order. + +## Development + +Keep each hook small, deterministic, and specific to the owning repository's guidance or packaging sync needs. + +## Verification + +Run the owning repository's shared sync entrypoint and confirm the expected repo-specific hooks execute in lexical order. + +## License + +Covered by the parent repository license. diff --git a/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh b/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh new file mode 100755 index 000000000..fdc434748 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/validate-all.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/lib" +. "$SELF_DIR/lib/common.sh" + +load_profile_env +load_env_file "$SELF_DIR/config/validation.env" +ensure_git_repo +log "Running repo-maintenance validation from $REPO_ROOT with the $REPO_MAINTENANCE_PROFILE profile." +run_dispatch_dir "$SELF_DIR/validations" "validation" +if [ "$REPO_MAINTENANCE_PROFILE" = "xcode-workspace" ]; then + "$SELF_DIR/workspace/validate-components.sh" +fi +log "Repo-maintenance validation completed successfully." diff --git a/skills/maintain-project-repo/assets/repo-maintenance/validations/10-toolkit-layout.sh b/skills/maintain-project-repo/assets/repo-maintenance/validations/10-toolkit-layout.sh new file mode 100755 index 000000000..7103b2036 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/validations/10-toolkit-layout.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +for required in \ + "$REPO_MAINTENANCE_ROOT/validate-all.sh" \ + "$REPO_MAINTENANCE_ROOT/sync-shared.sh" \ + "$REPO_MAINTENANCE_ROOT/release.sh" \ + "$REPO_MAINTENANCE_ROOT/lib/common.sh" \ + "$REPO_MAINTENANCE_ROOT/config/profile.env" +do + [ -f "$required" ] || die "maintain-project-repo is missing the required file $required." +done diff --git a/skills/maintain-project-repo/assets/repo-maintenance/validations/20-agents-guidance.sh b/skills/maintain-project-repo/assets/repo-maintenance/validations/20-agents-guidance.sh new file mode 100755 index 000000000..2f775a7d1 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/validations/20-agents-guidance.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +if [ "${REPO_MAINTENANCE_REQUIRE_AGENTS:-true}" != "true" ]; then + log "Skipping AGENTS.md validation because REPO_MAINTENANCE_REQUIRE_AGENTS is disabled." + exit 0 +fi + +agents_path="$REPO_ROOT/AGENTS.md" +[ -f "$agents_path" ] || die "Expected $agents_path to exist so maintain-project-repo has repo guidance to complement." +[ -s "$agents_path" ] || die "Expected $agents_path to be non-empty." + +for needle in \ + "scripts/repo-maintenance/validate-all.sh" \ + "scripts/repo-maintenance/sync-shared.sh" \ + "scripts/repo-maintenance/release.sh" +do + grep -F "$needle" "$agents_path" >/dev/null 2>&1 || die "Expected $agents_path to mention $needle so the maintainer validation, sync, and release entrypoints stay discoverable." +done diff --git a/skills/maintain-project-repo/assets/repo-maintenance/validations/30-ci-wrapper.sh b/skills/maintain-project-repo/assets/repo-maintenance/validations/30-ci-wrapper.sh new file mode 100755 index 000000000..e6815be15 --- /dev/null +++ b/skills/maintain-project-repo/assets/repo-maintenance/validations/30-ci-wrapper.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export REPO_MAINTENANCE_COMMON_DIR="$SELF_DIR/../lib" +. "$SELF_DIR/../lib/common.sh" + +workflow_path="$REPO_ROOT/.github/workflows/validate-repo-maintenance.yml" + +if [ ! -f "$workflow_path" ]; then + log "Skipping CI wrapper validation because $workflow_path is not present." + exit 0 +fi + +grep -Fq "scripts/repo-maintenance/validate-all.sh" "$workflow_path" || die "Expected $workflow_path to call scripts/repo-maintenance/validate-all.sh." diff --git a/skills/maintain-project-repo/references/automation-prompts.md b/skills/maintain-project-repo/references/automation-prompts.md new file mode 100644 index 000000000..42a1f73b2 --- /dev/null +++ b/skills/maintain-project-repo/references/automation-prompts.md @@ -0,0 +1,12 @@ +# Repo Maintenance Toolkit Automation Prompts + +- Install `maintain-project-repo` into `` and keep the GitHub workflow wrapper enabled. +- Refresh `maintain-project-repo` in `` without deleting repo-specific custom scripts. +- Report what `maintain-project-repo` would install into `` without mutating files. +- Explain when to use `scripts/repo-maintenance/validate-all.sh`, `scripts/repo-maintenance/sync-shared.sh`, and `scripts/repo-maintenance/release.sh`. +- Explain that standard release mode runs from a feature branch or worktree, opens a PR against protected `main`, takes bounded CI/review snapshots through `prepare`, `inspect`, and `advance`, gates on PR comments, merges, fast-forwards local `main`, creates and pushes the tag from that reviewed `main`, creates the GitHub release, accounts for every local branch not contained by `main`, and only then cleans up branches that are proven safe to delete. +- Explain that branch cleanup is gated by commit reachability: do not call work on `main`, merged, recovered, preserved, or safe to clean up until the exact local repository and remote prove it, and do not delete local branches, remote branches, worktrees, archive refs, or temporary rescue refs until any non-base history is merged or explicitly archived. +- Explain that standard release mode uses bounded `prepare`, `inspect`, and `advance` operations. For a remote gate, consume its continuation packet and reuse a live matching host-native wakeup while the gate remains pending and healthy; do not delete/recreate it after an unchanged snapshot. Create/update only after it fires or is stale, and pause/delete it when resolved, failed, cancelled, or identity-drifted. Every scheduled interval is at least five minutes. Run `inspect` first, and run `advance` only when packet identities still match. Codex uses heartbeat; Hermes uses a continuable `cronjob` with `deliver="origin"` and `attach_to_session=true`. +- Explain that pending review-bot contexts such as CodeRabbit are not a clean merge signal. Codex should wake, inspect the review and comments, address valid findings, and only merge after the review/comment gate is clear. An explicit CodeRabbit quota, usage, rate, or review-limit diagnostic is the narrow exception: it is non-blocking because no review was produced, while every other CodeRabbit finding, failed check, requested change, or comment remains blocking. +- Explain that GitHub release creation uses checked-in `docs/releases/vX.Y.Z.md` notes first, then `docs/releases/X.Y.Z.md`; it logs a clear fallback to GitHub-generated notes only when neither file exists. +- Explain that protected branches should require the GitHub Actions check context `validate` for the managed repo-maintenance workflow. diff --git a/skills/maintain-project-repo/references/customization-flow.md b/skills/maintain-project-repo/references/customization-flow.md new file mode 100644 index 000000000..04e370fd6 --- /dev/null +++ b/skills/maintain-project-repo/references/customization-flow.md @@ -0,0 +1,31 @@ +# Repo Maintenance Toolkit Customization Contract + +## Purpose + +Record lightweight default preferences for `maintain-project-repo` without turning its managed file set into a wide runtime customization surface. + +## Knobs + +| Knob | Default | Status | Effect | +| --- | --- | --- | --- | +| `defaultReleaseMode` | `standard` | `policy-only` | Sets the default planning posture when the user asks for a release flow without saying whether the repo is standalone or a submodule. Standard mode assumes releases run from a branch or worktree into protected `main`. | + +## Runtime Behavior + +- `scripts/customization_config.py` reads, writes, resets, and reports customization state. +- `scripts/install_maintain_project_repo.py` and `scripts/run_workflow.py` do not currently read these customization knobs. +- The managed file set, GitHub workflow wrapper, and release script surfaces are fixed workflow behavior rather than durable runtime customization. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Update `SKILL.md` and the affected references to reflect the approved default-policy change. +3. Persist the metadata change with `scripts/customization_config.py apply --input `. +4. Re-run `scripts/customization_config.py effective` and confirm the stored values match the docs. +5. Verify the workflow references still describe the same install and release behavior. + +## Validation + +1. Verify `references/repo-maintenance-layout.md` still matches the managed asset tree. +2. Verify `references/release-modes.md` still matches `assets/repo-maintenance/release.sh`. +3. Verify every customization knob is described consistently across `SKILL.md`, this file, and `references/automation-prompts.md`. diff --git a/skills/maintain-project-repo/references/customization.template.yaml b/skills/maintain-project-repo/references/customization.template.yaml new file mode 100644 index 000000000..ed649883a --- /dev/null +++ b/skills/maintain-project-repo/references/customization.template.yaml @@ -0,0 +1,4 @@ +schemaVersion: 1 +isCustomized: false +settings: + defaultReleaseMode: "standard" diff --git a/skills/maintain-project-repo/references/pre-commit-vs-ci.md b/skills/maintain-project-repo/references/pre-commit-vs-ci.md new file mode 100644 index 000000000..02c068974 --- /dev/null +++ b/skills/maintain-project-repo/references/pre-commit-vs-ci.md @@ -0,0 +1,16 @@ +# Pre-Commit vs CI + +Use `maintain-project-repo` with a local-first split: + +- `scripts/repo-maintenance/validate-all.sh` + - the full local validation command + - the same command CI should call +- `.github/workflows/validate-repo-maintenance.yml` + - a thin wrapper that calls the local script + - install SwiftFormat and SwiftLint before validation so Apple-profile checks are reproducible on fresh macOS runners + - keep workflow logic limited to runner bootstrap and the local validation call +- `scripts/repo-maintenance/hooks/pre-commit.sample` + - an opt-in sample for cheap local checks + - do not turn it into the only validation surface + +Keep expensive or repo-shaping logic in the repo-owned scripts, not in GitHub workflow YAML. diff --git a/skills/maintain-project-repo/references/release-modes.md b/skills/maintain-project-repo/references/release-modes.md new file mode 100644 index 000000000..b8cc44bad --- /dev/null +++ b/skills/maintain-project-repo/references/release-modes.md @@ -0,0 +1,67 @@ +# Release Modes + +Use these modes only when the current task is actually a release, publish, merge, tag, or protected-main release preparation task. They are not the default completion path for ordinary questions, investigations, local edits, documentation maintenance, or targeted validation. + +## `standard` + +Use this mode for an ordinary standalone repository whose release line is a protected `main` branch. + +Run it from a feature branch or worktree. Do not run standard release mode from `main`; the script treats `main` as the protected integration branch that receives the release through a pull request. + +- run `--operation prepare` for local validation, the version bump, branch push, PR creation, one remote snapshot, and a continuation packet +- require committed changes and a clean worktree +- run the repo-specific version bump hook at the selected profile root: `scripts/repo-maintenance/version-bump.sh` for generic/SwiftPM profiles or `Scripts/repo-maintenance/version-bump.sh` for the xcode-app profile +- commit the version bump as `release: bump versions for vX.Y.Z` +- push the branch +- perform one immediate branch-visibility re-read; if it is not visible, emit a continuation packet instead of polling +- open or update a pull request against `main` +- use `--operation inspect` for one PR/check/review snapshot; it emits a continuation packet for unknown or pending remote state +- create one host-native continuation no sooner than five minutes later, then reuse that same matching scheduler item while the gate stays pending and healthy; do not delete/recreate it after an unchanged snapshot. Codex uses heartbeat, Hermes uses an updated continuable `cronjob` with `deliver="origin"` and `attach_to_session=true` +- on wakeup run `inspect` first, then use `--operation advance` only if the packet's branch, commit, PR, and tag identities still match +- stop with a clear message if CI fails, changes are requested, or unresolved comments remain; an explicit CodeRabbit quota, usage, rate, or review-limit diagnostic is non-blocking because it produced no review, but all other CodeRabbit contexts and comments still block +- stop on requested changes or comments so the maintainer can address valid concerns, add out-of-scope concerns to `ROADMAP.md`, resolve the threads, push, and rerun the same script +- merge the PR with a merge commit once CI is green and the comment pass is clear +- fast-forward local `main` from `origin/main` +- create the annotated release tag locally from the reviewed local `main` +- push the tag +- perform one immediate tag-visibility re-read; if it is not visible, emit a continuation packet instead of polling +- create the GitHub release unless skipped, preferring `docs/releases/vX.Y.Z.md` and then `docs/releases/X.Y.Z.md` as its checked-in body; when neither exists, log the fallback and use GitHub-generated notes. Pass `--prerelease` for SemVer prerelease tags such as `vX.Y.Z-alpha.N`, `vX.Y.Z-beta.N`, `vX.Y.Z-rc.N`, or preview-style suffixes +- perform one immediate GitHub-release re-read; if it is not readable, emit a continuation packet instead of polling +- verify the GitHub release object's prerelease metadata matches the release tag before calling release publication complete +- verify `git log origin/main..main` or the repository's equivalent base/remote comparison is empty before claiming the local base branch is synchronized +- enumerate every local branch still not contained by `main` and account for each branch as already preserved elsewhere, intentionally still in progress, newly archived, newly merged, or safe to delete +- prune stale remote tracking refs and delete only local branches already merged into `main` after branch accounting proves they are safe + +Treat branch accounting as a hard completion gate, not optional cleanup. Use `git branch --no-merged ` or the repository's equivalent branch inventory before cleanup, and do not say a release, publish, merge, or cleanup step is done until every local branch not contained by the local base branch has been accounted for. Do not say work is on `main`, merged, recovered, preserved, or safe to clean up until commit reachability has been verified in the exact local repository and remote that statement refers to. Do not delete local branches, remote branches, worktrees, archive refs, or temporary rescue refs until branch accounting is complete and any non-base history is either merged or preserved on an explicit archive ref. + +Example: + +```bash +bash scripts/repo-maintenance/release.sh --mode standard --version v1.2.0 --operation prepare +``` + +When a release intentionally has no repo version surfaces, pass `--skip-version-bump`. When the PR comment pass has already been handled and only historical comments remain visible through GitHub, rerun with `--review-comments-addressed`. + +Remote waiting is never a release-script operation. `prepare`, `inspect`, and `advance` each take one bounded snapshot and either make an immediate safe transition or emit a continuation packet. Agents create one host-native wakeup no sooner than five minutes later, reuse that same matching scheduler item while the gate remains pending and healthy, and pause/delete it only on resolution, failure, cancellation, or identity drift. Create/update a replacement only after the prior item fires or becomes stale. Resume with `inspect`, and use `advance` only after packet identities match. Do not use shell `sleep`, `gh pr checks --watch`, timer loops, or one-to-four-minute rechecks. + +## `submodule` + +Use this mode when the current repository is checked out as a git submodule inside a larger parent repository: + +- run local validation first +- require a clean worktree +- require an actual superproject relationship +- create the release tag locally +- push the branch and tag in the submodule repository +- perform one immediate branch and tag visibility re-read; if either is absent, create or reuse the matching host-native continuation no sooner than five minutes later, then inspect before another release action rather than polling +- create the GitHub release when `gh` is available, passing `--prerelease` for SemVer prerelease tags such as `vX.Y.Z-alpha.N`, `vX.Y.Z-beta.N`, `vX.Y.Z-rc.N`, or preview-style suffixes +- perform one immediate GitHub release re-read after creation; if it is absent, create or reuse the matching host-native continuation no sooner than five minutes later, then inspect before another release action rather than polling +- verify the GitHub release object's prerelease metadata matches the release tag before calling release publication complete +- verify the submodule branch and tag are visible on the intended remote before calling that work preserved or released +- leave the parent-repo pointer update as a separate explicit follow-up step + +Example: + +```bash +bash scripts/repo-maintenance/release.sh --mode submodule --version v1.2.0 +``` diff --git a/skills/maintain-project-repo/references/repo-maintenance-layout.md b/skills/maintain-project-repo/references/repo-maintenance-layout.md new file mode 100644 index 000000000..6563759db --- /dev/null +++ b/skills/maintain-project-repo/references/repo-maintenance-layout.md @@ -0,0 +1,44 @@ +# Repo Maintenance Layout + +The managed target layout is: + +```text +scripts/ + repo-maintenance/ + validate-all.sh + sync-shared.sh + release.sh + version-bump.sh (optional repo-specific hook) + lib/ + common.sh + validations/ + 10-toolkit-layout.sh + 20-agents-guidance.sh + 30-ci-wrapper.sh + syncing/ + release/ + 10-preflight.sh + 20-tag-release.sh + 30-push-release.sh + 40-github-release.sh + config/ + validation.env + release.env + hooks/ + pre-commit.sample +.github/ + workflows/ + validate-repo-maintenance.yml +.swiftformat (Apple profiles) +.swiftlint.yml (Apple profiles) +``` + +## Design Rules + +- Top-level scripts are stable entrypoints. +- Ordered `validations/*.sh`, `syncing/*.sh`, and `release/*.sh` are discovered automatically. +- Managed files are safe to refresh in place. +- Repo-specific extra scripts are allowed as long as they do not reuse the managed filenames. +- Apple profiles install `.swiftformat` and `.swiftlint.yml` samples together; SwiftFormat remains the formatting authority and SwiftLint stays scoped to complementary non-formatting checks. +- Standard release mode uses the optional repo-specific `version-bump.sh` hook when it exists and requires either that hook or an explicit `--skip-version-bump` decision. +- The managed GitHub workflow exposes `validate` as the required branch-protection check context. Do not configure protected branches to require the display-style string `Validate Repo Maintenance / validate`. diff --git a/skills/maintain-project-repo/references/trigger-eval.md b/skills/maintain-project-repo/references/trigger-eval.md new file mode 100644 index 000000000..b193c7378 --- /dev/null +++ b/skills/maintain-project-repo/references/trigger-eval.md @@ -0,0 +1,31 @@ +# Trigger Evaluation + +## Should Trigger + +- Release version 1.4.0. +- Publish this package. +- Tag this commit and create the GitHub release. +- Prepare this branch for a protected-main release. +- Run the release workflow and clean up the merged branch. +- Install the repo-maintenance toolkit. +- Refresh our validation, sync, and release scripts. +- Use the checked-in release notes for this patch release. + +## Should Not Trigger + +- Explain this function. +- Fix this local test failure. +- Update the README. +- Commit and push this ordinary edit. +- Write the pull-request body. +- Audit branch protection and Dependabot settings. +- Apply my normal GitHub repository settings. +- Update `CONTRIBUTING.md`. + +## Routing + +- GitHub repository settings audit and alignment route to + `maintain-github-repository`. +- Ordinary documentation work routes to the owning document-maintenance skill. +- Ordinary local Git work follows repo-local Git guidance without invoking the + full release lifecycle. diff --git a/skills/maintain-project-repo/scripts/customization_config.py b/skills/maintain-project-repo/scripts/customization_config.py new file mode 100755 index 000000000..e932fb806 --- /dev/null +++ b/skills/maintain-project-repo/scripts/customization_config.py @@ -0,0 +1,213 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "PyYAML>=6.0.2,<7", +# ] +# /// +"""Load and persist per-skill customization state.""" + +from __future__ import annotations + +import argparse +import copy +import os +import re +import sys +from pathlib import Path + +import yaml + +SCHEMA_VERSION = 1 +SKILL_NAME = "maintain-project-repo" +CONFIG_HOME_ENV = "APPLE_DEV_SKILLS_CONFIG_HOME" +DEFAULT_CONFIG_ROOT = "~/.config/gaelic-ghost/apple-dev-skills" +ALLOWED_TOP_LEVEL = {"schemaVersion", "isCustomized", "settings"} + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def quote_string(value: str) -> str: + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + +def encode_scalar(value) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + if value is None: + return quote_string("") + return quote_string(str(value)) + + +def parse_yaml(path: Path) -> dict: + if not path.exists(): + fail(f"Missing YAML file: {path}") + + try: + loaded = yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + fail(f"Invalid YAML in {path}: {exc}") + + if loaded is None: + return {} + if not isinstance(loaded, dict): + fail(f"Top-level YAML document must be a mapping in {path}") + + if isinstance(loaded.get("settings"), dict): + loaded["settings"] = { + key: ("" if value is None else value) for key, value in loaded["settings"].items() + } + + return loaded + + +def validate_config(config: dict, *, allow_partial: bool) -> None: + unknown = set(config.keys()) - ALLOWED_TOP_LEVEL + if unknown: + fail(f"Unknown top-level keys: {', '.join(sorted(unknown))}") + + if not allow_partial: + for required in ("schemaVersion", "isCustomized", "settings"): + if required not in config: + fail(f"Missing required key: {required}") + + if "schemaVersion" in config and config["schemaVersion"] != SCHEMA_VERSION: + fail(f"schemaVersion must be {SCHEMA_VERSION}") + + if "isCustomized" in config and not isinstance(config["isCustomized"], bool): + fail("isCustomized must be boolean") + + if "settings" in config: + if not isinstance(config["settings"], dict): + fail("settings must be a mapping") + for key, value in config["settings"].items(): + if not re.fullmatch(r"[A-Za-z0-9_]+", key): + fail(f"Invalid settings key: {key}") + if isinstance(value, (dict, list)): + fail(f"settings values must be scalar: {key}") + + +def merge_configs(base: dict, overlay: dict) -> dict: + merged = { + "schemaVersion": base.get("schemaVersion", SCHEMA_VERSION), + "isCustomized": base.get("isCustomized", False), + "settings": copy.deepcopy(base.get("settings", {})), + } + + if "schemaVersion" in overlay: + merged["schemaVersion"] = overlay["schemaVersion"] + if "isCustomized" in overlay: + merged["isCustomized"] = overlay["isCustomized"] + if "settings" in overlay: + merged["settings"].update(overlay["settings"]) + + return merged + + +def dump_yaml(config: dict) -> str: + lines = [ + f"schemaVersion: {int(config['schemaVersion'])}", + f"isCustomized: {'true' if config['isCustomized'] else 'false'}", + "settings:", + ] + for key in sorted(config["settings"].keys()): + lines.append(f" {key}: {encode_scalar(config['settings'][key])}") + return "\n".join(lines) + "\n" + + +def template_path() -> Path: + return Path(__file__).resolve().parents[1] / "references" / "customization.template.yaml" + + +def config_root() -> Path: + root = os.environ.get(CONFIG_HOME_ENV, DEFAULT_CONFIG_ROOT) + return Path(root).expanduser() + + +def durable_path() -> Path: + return config_root() / SKILL_NAME / "customization.yaml" + + +def load_template() -> dict: + cfg = parse_yaml(template_path()) + validate_config(cfg, allow_partial=False) + return cfg + + +def load_durable() -> dict: + path = durable_path() + if not path.exists(): + return {} + cfg = parse_yaml(path) + validate_config(cfg, allow_partial=False) + return cfg + + +def cmd_path(_: argparse.Namespace) -> None: + print(durable_path()) + + +def cmd_effective(_: argparse.Namespace) -> None: + effective = merge_configs(load_template(), load_durable()) + validate_config(effective, allow_partial=False) + print(dump_yaml(effective), end="") + + +def cmd_apply(args: argparse.Namespace) -> None: + template = load_template() + current = merge_configs(template, load_durable()) + incoming = parse_yaml(Path(args.input)) + validate_config(incoming, allow_partial=True) + + updated = merge_configs(current, incoming) + updated["schemaVersion"] = SCHEMA_VERSION + updated["isCustomized"] = True + validate_config(updated, allow_partial=False) + + target = durable_path() + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(dump_yaml(updated), encoding="utf-8") + print(target) + + +def cmd_reset(_: argparse.Namespace) -> None: + target = durable_path() + if target.exists(): + target.unlink() + print(target) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Manage per-skill customization config") + subparsers = parser.add_subparsers(dest="command", required=True) + + parser_path = subparsers.add_parser("path", help="Print durable config path") + parser_path.set_defaults(func=cmd_path) + + parser_effective = subparsers.add_parser("effective", help="Print merged effective config") + parser_effective.set_defaults(func=cmd_effective) + + parser_apply = subparsers.add_parser("apply", help="Apply and persist config overrides") + parser_apply.add_argument("--input", required=True, help="Path to YAML overrides") + parser_apply.set_defaults(func=cmd_apply) + + parser_reset = subparsers.add_parser("reset", help="Delete durable config for this skill") + parser_reset.set_defaults(func=cmd_reset) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/skills/maintain-project-repo/scripts/install_maintain_project_repo.py b/skills/maintain-project-repo/scripts/install_maintain_project_repo.py new file mode 100755 index 000000000..86e978c29 --- /dev/null +++ b/skills/maintain-project-repo/scripts/install_maintain_project_repo.py @@ -0,0 +1,405 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# /// +"""Install or refresh the managed maintain-project-repo files.""" + +from __future__ import annotations + +import argparse +import json +import shutil +from pathlib import Path + + +PROFILE_CHOICES = { + "generic": "Generic repo-maintenance baseline with no Swift or Xcode specialization.", + "swift-package": "Swift Package Manager repo-maintenance profile for library, tool, and package repos.", + "xcode-app": "Xcode app repo-maintenance profile for native Apple app repositories.", + "xcode-workspace": "Xcode workspace repo-maintenance profile for Apps, Packages, and optional Services roots.", +} +PROFILE_TOOLKIT_ROOTS = { + "generic": Path("scripts/repo-maintenance"), + "swift-package": Path("scripts/repo-maintenance"), + "xcode-app": Path("Scripts/repo-maintenance"), + "xcode-workspace": Path("Scripts/repo-maintenance"), +} +PROFILE_OVERLAY_FILES = { + "swift-package": [ + ("profiles/apple/repo-maintenance/.swiftformat", ".swiftformat"), + ("profiles/apple/repo-maintenance/.swiftlint.yml", ".swiftlint.yml"), + ( + "profiles/apple/repo-maintenance/hooks/pre-commit.sample", + "scripts/repo-maintenance/hooks/pre-commit.sample", + ), + ], + "xcode-app": [ + ("profiles/apple/repo-maintenance/.swiftformat", ".swiftformat"), + ("profiles/apple/repo-maintenance/.swiftlint.yml", ".swiftlint.yml"), + ( + "profiles/apple/repo-maintenance/hooks/pre-commit.sample", + "scripts/repo-maintenance/hooks/pre-commit.sample", + ), + ], + "xcode-workspace": [ + ("profiles/apple/repo-maintenance/.swiftformat", ".swiftformat"), + ("profiles/apple/repo-maintenance/.swiftlint.yml", ".swiftlint.yml"), + ( + "profiles/apple/repo-maintenance/hooks/pre-commit.sample", + "scripts/repo-maintenance/hooks/pre-commit.sample", + ), + ( + "profiles/xcode-workspace/repo-maintenance/validations/40-xcode-workspace-layout.sh", + "scripts/repo-maintenance/validations/40-xcode-workspace-layout.sh", + ), + ( + "profiles/xcode-workspace/repo-maintenance/workspace/validate-components.sh", + "scripts/repo-maintenance/workspace/validate-components.sh", + ), + ], +} +PROFILE_WORKFLOW_FILES = { + "swift-package": "profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", + "xcode-app": "profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", + "xcode-workspace": "profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", +} +MANAGED_TOOLKIT_FILES = [ + ("repo-maintenance/validate-all.sh", "scripts/repo-maintenance/validate-all.sh"), + ("repo-maintenance/sync-shared.sh", "scripts/repo-maintenance/sync-shared.sh"), + ("repo-maintenance/release.sh", "scripts/repo-maintenance/release.sh"), + ("repo-maintenance/lib/common.sh", "scripts/repo-maintenance/lib/common.sh"), + ("repo-maintenance/lib/coderabbit.sh", "scripts/repo-maintenance/lib/coderabbit.sh"), + ("repo-maintenance/validations/10-toolkit-layout.sh", "scripts/repo-maintenance/validations/10-toolkit-layout.sh"), + ("repo-maintenance/validations/20-agents-guidance.sh", "scripts/repo-maintenance/validations/20-agents-guidance.sh"), + ("repo-maintenance/validations/30-ci-wrapper.sh", "scripts/repo-maintenance/validations/30-ci-wrapper.sh"), + ("repo-maintenance/syncing/README.md", "scripts/repo-maintenance/syncing/README.md"), + ("repo-maintenance/release/10-preflight.sh", "scripts/repo-maintenance/release/10-preflight.sh"), + ("repo-maintenance/release/20-tag-release.sh", "scripts/repo-maintenance/release/20-tag-release.sh"), + ("repo-maintenance/release/30-push-release.sh", "scripts/repo-maintenance/release/30-push-release.sh"), + ("repo-maintenance/release/40-github-release.sh", "scripts/repo-maintenance/release/40-github-release.sh"), + ("repo-maintenance/config/validation.env", "scripts/repo-maintenance/config/validation.env"), + ("repo-maintenance/config/release.env", "scripts/repo-maintenance/config/release.env"), + ("repo-maintenance/hooks/pre-commit.sample", "scripts/repo-maintenance/hooks/pre-commit.sample"), +] +MANAGED_WORKFLOW_FILE = ".github/workflows/validate-repo-maintenance.yml" +DEFAULT_TOOLKIT_ROOT = Path("scripts/repo-maintenance") +EXECUTABLE_SUFFIXES = {".sh", ".py", ".sample"} + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", required=True) + parser.add_argument("--operation", choices=("install", "refresh", "report-only"), default="install") + parser.add_argument("--profile", choices=sorted(PROFILE_CHOICES), default="generic") + parser.add_argument("--skip-github-workflow", action="store_true") + parser.add_argument("--dry-run", action="store_true") + return parser + + +def assets_root() -> Path: + return Path(__file__).resolve().parents[1] / "assets" + + +def toolkit_root(profile: str) -> Path: + return PROFILE_TOOLKIT_ROOTS[profile] + + +def profile_file(profile: str) -> Path: + return toolkit_root(profile) / "config/profile.env" + + +def profile_target_path(profile: str, target_relative: str) -> Path: + target = Path(target_relative) + try: + suffix = target.relative_to(DEFAULT_TOOLKIT_ROOT) + except ValueError: + return target + return toolkit_root(profile) / suffix + + +def target_pairs(profile: str, skip_github_workflow: bool) -> list[tuple[Path, Path]]: + root = assets_root() + pairs: list[tuple[Path, Path]] = [] + + def add_pair(source_relative: str, target_relative: str) -> None: + target = profile_target_path(profile, target_relative) + for index, (_, existing_target) in enumerate(pairs): + if existing_target == target: + pairs[index] = (root / source_relative, target) + return + pairs.append((root / source_relative, target)) + + for source_relative, target_relative in MANAGED_TOOLKIT_FILES: + add_pair(source_relative, target_relative) + for source_relative, target_relative in PROFILE_OVERLAY_FILES.get(profile, []): + add_pair(source_relative, target_relative) + if not skip_github_workflow: + workflow_source = PROFILE_WORKFLOW_FILES.get( + profile, + "github/repo-maintenance-workflows/validate-repo-maintenance.yml", + ) + pairs.append( + ( + root / workflow_source, + Path(MANAGED_WORKFLOW_FILE), + ) + ) + return pairs + + +def ensure_safe_target(repo_root: Path, relative_target: Path) -> None: + target = repo_root / relative_target + if target.exists() and not target.is_file(): + raise RuntimeError( + f"The managed target path {target} exists but is not a regular file." + ) + + +def xcode_workspace_findings(repo_root: Path) -> list[str]: + findings: list[str] = [] + workspaces = list(repo_root.glob("*.xcworkspace")) + if len(workspaces) != 1: + findings.append(f"expected exactly one root .xcworkspace, found {len(workspaces)}") + + apps_root = repo_root / "Apps" + if not apps_root.is_dir(): + findings.append("expected Apps/ at the repository root") + elif not any(apps_root.glob("**/*.xcodeproj")): + findings.append("expected at least one .xcodeproj under Apps/") + + packages_root = repo_root / "Packages" + if not packages_root.is_dir(): + findings.append("expected Packages/ at the repository root") + elif not any(packages_root.glob("**/Package.swift")): + findings.append("expected at least one Package.swift under Packages/") + return findings + + +def ensure_profile_shape(repo_root: Path, profile: str) -> None: + if profile != "xcode-workspace": + return + findings = xcode_workspace_findings(repo_root) + if findings: + raise RuntimeError( + "The xcode-workspace profile requires a modular Apple workspace: " + + "; ".join(findings) + + ". Use xcode-app, swift-package, or generic when this is not the workspace root." + ) + + +def legacy_xcode_toolkit_migration(repo_root: Path, profile: str) -> tuple[Path, Path] | None: + if profile not in {"xcode-app", "xcode-workspace"}: + return None + + legacy_root = repo_root / DEFAULT_TOOLKIT_ROOT + desired_root = repo_root / toolkit_root(profile) + if not legacy_root.exists(): + return None + + if desired_root.exists(): + try: + if legacy_root.samefile(desired_root): + return None + except OSError: + pass + raise RuntimeError( + f"The {profile} profile expects repo-maintenance under " + f"{toolkit_root(profile).as_posix()}, but both {DEFAULT_TOOLKIT_ROOT.as_posix()} " + f"and {toolkit_root(profile).as_posix()} already exist as separate paths. " + "Choose the intentional toolkit root, preserve any repo-specific custom files, " + "and rerun maintain-project-repo." + ) + + if not legacy_root.is_dir(): + raise RuntimeError( + f"The {profile} profile expects repo-maintenance under " + f"{toolkit_root(profile).as_posix()}, but the legacy path " + f"{DEFAULT_TOOLKIT_ROOT.as_posix()} exists and is not a directory." + ) + + return legacy_root, desired_root + + +def apply_legacy_xcode_toolkit_migration(repo_root: Path, profile: str) -> str | None: + migration = legacy_xcode_toolkit_migration(repo_root, profile) + if migration is None: + return None + + legacy_root, desired_root = migration + desired_root.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(legacy_root), str(desired_root)) + try: + legacy_root.parent.rmdir() + except OSError: + pass + return ( + f"migrated legacy {DEFAULT_TOOLKIT_ROOT.as_posix()} to " + f"{toolkit_root(profile).as_posix()} for {profile} profile" + ) + + +def copy_file(source: Path, target: Path, profile: str) -> None: + target.parent.mkdir(parents=True, exist_ok=True) + if profile in {"xcode-app", "xcode-workspace"}: + content = source.read_text(encoding="utf-8") + content = content.replace("scripts/repo-maintenance", "Scripts/repo-maintenance") + target.write_text(content, encoding="utf-8") + else: + shutil.copyfile(source, target) + if source.suffix in EXECUTABLE_SUFFIXES: + target.chmod(0o755) + + +def render_profile_env(profile: str) -> str: + description = PROFILE_CHOICES[profile] + return ( + "# Managed by maintain-project-repo. Do not hand-edit unless you also control the installer contract.\n" + f'REPO_MAINTENANCE_PROFILE="{profile}"\n' + f'REPO_MAINTENANCE_PROFILE_DESCRIPTION="{description}"\n' + ) + + +def write_profile_env(repo_root: Path, profile: str) -> None: + target = repo_root / profile_file(profile) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(render_profile_env(profile), encoding="utf-8") + + +def main() -> int: + args = build_parser().parse_args() + repo_root = Path(args.repo_root).expanduser().resolve() + actions: list[str] = [] + managed_files = [relative.as_posix() for _, relative in target_pairs(args.profile, args.skip_github_workflow)] + managed_profile_file = profile_file(args.profile) + managed_files.append(managed_profile_file.as_posix()) + + if not repo_root.exists(): + print( + json.dumps( + { + "status": "blocked", + "path_type": "primary", + "repo_root": str(repo_root), + "managed_files": managed_files, + "actions": actions, + "stderr": "The requested repo root does not exist.", + "next_step": "Create or choose an existing repository root and rerun the workflow.", + }, + indent=2, + sort_keys=True, + ) + ) + return 1 + + if not repo_root.is_dir(): + print( + json.dumps( + { + "status": "blocked", + "path_type": "primary", + "repo_root": str(repo_root), + "managed_files": managed_files, + "actions": actions, + "stderr": "The requested repo root is not a directory.", + "next_step": "Use a directory path for --repo-root and rerun the workflow.", + }, + indent=2, + sort_keys=True, + ) + ) + return 1 + + try: + ensure_profile_shape(repo_root, args.profile) + planned_migration = legacy_xcode_toolkit_migration(repo_root, args.profile) + for _, relative_target in target_pairs(args.profile, args.skip_github_workflow): + ensure_safe_target(repo_root, relative_target) + ensure_safe_target(repo_root, managed_profile_file) + except RuntimeError as exc: + print( + json.dumps( + { + "status": "blocked", + "path_type": "primary", + "repo_root": str(repo_root), + "managed_files": managed_files, + "actions": actions, + "stderr": str(exc), + "next_step": "Resolve the conflicting target path and rerun the workflow.", + }, + indent=2, + sort_keys=True, + ) + ) + return 1 + + if args.operation == "report-only" or args.dry_run: + if planned_migration is not None: + actions.append( + f"migrate legacy {DEFAULT_TOOLKIT_ROOT.as_posix()} to " + f"{toolkit_root(args.profile).as_posix()} for {args.profile} profile" + ) + for source, relative_target in target_pairs(args.profile, args.skip_github_workflow): + target = repo_root / relative_target + if target.exists(): + actions.append(f"refresh {relative_target.as_posix()} from {source.relative_to(assets_root()).as_posix()}") + else: + actions.append(f"install {relative_target.as_posix()} from {source.relative_to(assets_root()).as_posix()}") + profile_target = repo_root / managed_profile_file + if profile_target.exists(): + actions.append(f"refresh {managed_profile_file.as_posix()} for {args.profile} profile") + else: + actions.append(f"install {managed_profile_file.as_posix()} for {args.profile} profile") + print( + json.dumps( + { + "status": "success", + "path_type": "fallback", + "repo_root": str(repo_root), + "profile": args.profile, + "managed_files": managed_files, + "actions": actions, + "validation_result": "skipped (--dry-run)" if args.dry_run else "skipped (report-only)", + "next_step": "Run without --dry-run or report-only to install or refresh maintain-project-repo.", + }, + indent=2, + sort_keys=True, + ) + ) + return 0 + + migration_action = apply_legacy_xcode_toolkit_migration(repo_root, args.profile) + if migration_action is not None: + actions.append(migration_action) + + for source, relative_target in target_pairs(args.profile, args.skip_github_workflow): + target = repo_root / relative_target + action = "refreshed" if target.exists() else "installed" + copy_file(source, target, args.profile) + actions.append(f"{action} {relative_target.as_posix()}") + profile_target = repo_root / managed_profile_file + profile_action = "refreshed" if profile_target.exists() else "installed" + write_profile_env(repo_root, args.profile) + actions.append(f"{profile_action} {managed_profile_file.as_posix()} for {args.profile} profile") + + print( + json.dumps( + { + "status": "success", + "path_type": "primary", + "repo_root": str(repo_root), + "profile": args.profile, + "managed_files": managed_files, + "actions": actions, + "validation_result": "managed files synced", + "next_step": f"Use {toolkit_root(args.profile).as_posix()}/validate-all.sh locally and keep CI as a thin wrapper around that command.", + }, + indent=2, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/maintain-project-repo/scripts/run_workflow.py b/skills/maintain-project-repo/scripts/run_workflow.py new file mode 100755 index 000000000..3f8110bd4 --- /dev/null +++ b/skills/maintain-project-repo/scripts/run_workflow.py @@ -0,0 +1,75 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "PyYAML>=6.0.2,<7", +# ] +# /// +"""Unified runtime entrypoint for maintain-project-repo.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +from pathlib import Path + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root") + parser.add_argument("--operation", choices=("install", "refresh", "report-only")) + parser.add_argument("--profile", choices=("generic", "swift-package", "xcode-app", "xcode-workspace")) + parser.add_argument("--skip-github-workflow", action="store_true") + parser.add_argument("--dry-run", action="store_true") + return parser + + +def main() -> int: + args = build_parser().parse_args() + repo_root = str(Path(args.repo_root or ".").expanduser().resolve()) + operation = args.operation or "install" + profile = args.profile or "generic" + normalized_inputs = { + "repo_root": repo_root, + "operation": operation, + "profile": profile, + "skip_github_workflow": args.skip_github_workflow, + "dry_run": args.dry_run, + } + + helper_path = Path(__file__).with_name("install_maintain_project_repo.py") + command = [ + str(helper_path), + "--repo-root", + repo_root, + "--operation", + operation, + "--profile", + profile, + ] + if args.skip_github_workflow: + command.append("--skip-github-workflow") + if args.dry_run: + command.append("--dry-run") + + proc = subprocess.run(command, capture_output=True, text=True, check=False) + payload = json.loads(proc.stdout) if proc.stdout.strip() else { + "status": "failed", + "path_type": "primary", + "repo_root": repo_root, + "normalized_inputs": normalized_inputs, + "managed_files": [], + "actions": [], + "validation_result": None, + "stdout": proc.stdout, + "stderr": proc.stderr, + "next_step": "Fix the maintain-project-repo workflow error and rerun the workflow.", + } + payload.setdefault("normalized_inputs", normalized_inputs) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 if proc.returncode == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py b/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py new file mode 100644 index 000000000..0519e9d97 --- /dev/null +++ b/skills/maintain-project-repo/tests/test_maintain_project_repo_workflow.py @@ -0,0 +1,509 @@ +from __future__ import annotations + +import json +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = ROOT / "skills/maintain-project-repo/scripts/run_workflow.py" + + +class RepoMaintenanceToolkitWorkflowTests(unittest.TestCase): + def run_script(self, *args: str, env: dict | None = None) -> tuple[int, dict]: + command_env = dict(env or os.environ) + command_env.setdefault("UV_CACHE_DIR", str(Path(tempfile.gettempdir()) / "repository-skills-uv-cache")) + proc = subprocess.run( + [str(SCRIPT), *args], + cwd="/tmp", + env=command_env, + capture_output=True, + text=True, + check=False, + ) + return proc.returncode, json.loads(proc.stdout) + + def test_report_only_lists_managed_files(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "report-only") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertEqual(payload["path_type"], "fallback") + self.assertIn("scripts/repo-maintenance/validate-all.sh", payload["managed_files"]) + self.assertIn(".github/workflows/validate-repo-maintenance.yml", payload["managed_files"]) + self.assertIn("scripts/repo-maintenance/config/profile.env", payload["managed_files"]) + self.assertEqual(payload["profile"], "generic") + + def test_install_writes_toolkit_files(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install", "--profile", "swift-package") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertTrue(Path(tmpdir, "scripts/repo-maintenance/validate-all.sh").is_file()) + self.assertTrue(Path(tmpdir, "scripts/repo-maintenance/release.sh").is_file()) + self.assertTrue(Path(tmpdir, "scripts/repo-maintenance/config/profile.env").is_file()) + self.assertTrue(Path(tmpdir, ".swiftformat").is_file()) + self.assertTrue(Path(tmpdir, ".swiftlint.yml").is_file()) + self.assertIn('REPO_MAINTENANCE_PROFILE="swift-package"', Path(tmpdir, "scripts/repo-maintenance/config/profile.env").read_text(encoding="utf-8")) + swiftlint_text = Path(tmpdir, ".swiftlint.yml").read_text(encoding="utf-8") + self.assertIn("SwiftFormat owns visual shape", swiftlint_text) + self.assertIn("fatal_error_message", swiftlint_text) + hook_text = Path(tmpdir, "scripts/repo-maintenance/hooks/pre-commit.sample").read_text(encoding="utf-8") + self.assertIn("swiftformat --lint", hook_text) + self.assertTrue(Path(tmpdir, ".github/workflows/validate-repo-maintenance.yml").is_file()) + workflow_text = Path(tmpdir, ".github/workflows/validate-repo-maintenance.yml").read_text(encoding="utf-8") + self.assertIn("Branch protection should require the Actions check context `validate`.", workflow_text) + self.assertIn(" validate:\n name: validate\n", workflow_text) + self.assertIn("runs-on: macos-26", workflow_text) + self.assertIn("actions/checkout@v6.0.2", workflow_text) + self.assertIn("xcode-select --print-path", workflow_text) + self.assertNotIn("actions/checkout@v4", workflow_text) + self.assertNotIn("maxim-lobanov/setup-xcode@v1", workflow_text) + self.assertIn("xcrun swift --version", workflow_text) + self.assertIn("brew install swiftformat swiftlint", workflow_text) + + def test_xcode_profile_uses_standard_capital_scripts_directory(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install", "--profile", "xcode-app") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertIn("Scripts/repo-maintenance/validate-all.sh", payload["managed_files"]) + self.assertIn("Scripts/repo-maintenance/config/profile.env", payload["managed_files"]) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validate-all.sh").is_file()) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/release.sh").is_file()) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/config/profile.env").is_file()) + Path(tmpdir, "case-probe").touch() + if not Path(tmpdir, "CASE-PROBE").exists(): + self.assertFalse(Path(tmpdir, "scripts/repo-maintenance/validate-all.sh").exists()) + self.assertIn( + 'REPO_MAINTENANCE_PROFILE="xcode-app"', + Path(tmpdir, "Scripts/repo-maintenance/config/profile.env").read_text(encoding="utf-8"), + ) + workflow_text = Path(tmpdir, ".github/workflows/validate-repo-maintenance.yml").read_text(encoding="utf-8") + self.assertIn("bash Scripts/repo-maintenance/validate-all.sh", workflow_text) + self.assertNotIn("scripts/repo-maintenance", workflow_text) + + def test_xcode_profile_migrates_legacy_lowercase_toolkit_root(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + Path(tmpdir, "case-probe").touch() + if Path(tmpdir, "CASE-PROBE").exists(): + self.skipTest("legacy lowercase-to-capital migration requires a case-sensitive filesystem") + + legacy_custom = Path(tmpdir, "scripts/repo-maintenance/validations/90-custom.sh") + legacy_custom.parent.mkdir(parents=True) + legacy_custom.write_text("#!/usr/bin/env sh\nexit 0\n", encoding="utf-8") + + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "refresh", "--profile", "xcode-app") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertIn( + "migrated legacy scripts/repo-maintenance to Scripts/repo-maintenance for xcode-app profile", + payload["actions"], + ) + self.assertFalse(Path(tmpdir, "scripts/repo-maintenance").exists()) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validations/90-custom.sh").is_file()) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validate-all.sh").is_file()) + + def test_xcode_workspace_profile_installs_workspace_validation_and_dispatches_components(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + (root / "Product.xcworkspace").mkdir() + (root / "Apps/ProductApp.xcodeproj").mkdir(parents=True) + package_root = root / "Packages/ProductCore" + package_root.mkdir(parents=True) + (package_root / "Package.swift").write_text("// package\n", encoding="utf-8") + component_validation = package_root / "scripts/repo-maintenance/validate-all.sh" + component_validation.parent.mkdir(parents=True) + component_validation.write_text("#!/usr/bin/env sh\nprintf '%s\\n' package-validated\n", encoding="utf-8") + (root / "AGENTS.md").write_text( + "# AGENTS.md\n\n- Scripts/repo-maintenance/validate-all.sh\n- Scripts/repo-maintenance/sync-shared.sh\n- Scripts/repo-maintenance/release.sh\n", + encoding="utf-8", + ) + + code, payload = self.run_script( + "--repo-root", tmpdir, "--operation", "install", "--profile", "xcode-workspace" + ) + + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertIn("Scripts/repo-maintenance/workspace/validate-components.sh", payload["managed_files"]) + self.assertTrue(Path(tmpdir, "Scripts/repo-maintenance/validations/40-xcode-workspace-layout.sh").is_file()) + profile_env = Path(tmpdir, "Scripts/repo-maintenance/config/profile.env").read_text(encoding="utf-8") + self.assertIn('REPO_MAINTENANCE_PROFILE="xcode-workspace"', profile_env) + + subprocess.run(["git", "init"], cwd=tmpdir, check=True, capture_output=True, text=True) + proc = subprocess.run( + ["sh", "Scripts/repo-maintenance/validate-all.sh"], + cwd=tmpdir, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr or proc.stdout) + self.assertIn("Validated xcode-workspace composition", proc.stdout) + self.assertIn("package-validated", proc.stdout) + + def test_xcode_workspace_profile_rejects_invalid_workspace_layout(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + Path(tmpdir, "AGENTS.md").write_text( + "# AGENTS.md\n\n- Scripts/repo-maintenance/validate-all.sh\n- Scripts/repo-maintenance/sync-shared.sh\n- Scripts/repo-maintenance/release.sh\n", + encoding="utf-8", + ) + code, payload = self.run_script( + "--repo-root", tmpdir, "--operation", "install", "--profile", "xcode-workspace" + ) + self.assertEqual(code, 1) + self.assertEqual(payload["status"], "blocked") + self.assertIn("requires a modular Apple workspace", payload["stderr"]) + self.assertIn("expected exactly one root .xcworkspace", payload["stderr"]) + + def test_generic_profile_uses_generic_macos_latest_workflow(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + workflow_text = Path(tmpdir, ".github/workflows/validate-repo-maintenance.yml").read_text(encoding="utf-8") + self.assertIn("runs-on: macos-latest", workflow_text) + self.assertIn("actions/checkout@v6.0.2", workflow_text) + self.assertNotIn("actions/checkout@v4", workflow_text) + self.assertNotIn("maxim-lobanov/setup-xcode@v1", workflow_text) + + def test_managed_workflows_avoid_node20_action_versions(self) -> None: + workflow_assets = [ + ROOT / "skills/maintain-project-repo/assets/github/repo-maintenance-workflows/validate-repo-maintenance.yml", + ROOT + / "skills/maintain-project-repo/assets/profiles/apple/github/repo-maintenance-workflows/validate-repo-maintenance.yml", + ] + for workflow_asset in workflow_assets: + with self.subTest(workflow=workflow_asset.name): + workflow_text = workflow_asset.read_text(encoding="utf-8") + self.assertIn("actions/checkout@v6.0.2", workflow_text) + self.assertNotIn("actions/checkout@v4", workflow_text) + self.assertNotIn("maxim-lobanov/setup-xcode@v1", workflow_text) + + def test_generic_profile_keeps_generic_pre_commit_hook(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertFalse(Path(tmpdir, ".swiftformat").exists()) + self.assertFalse(Path(tmpdir, ".swiftlint.yml").exists()) + hook_text = Path(tmpdir, "scripts/repo-maintenance/hooks/pre-commit.sample").read_text(encoding="utf-8") + self.assertNotIn("swiftformat --lint", hook_text) + self.assertIn('exec "$repo_root/scripts/repo-maintenance/validate-all.sh"', hook_text) + + def test_generated_validation_uses_repo_maintenance_self_dir(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + + Path(tmpdir, "AGENTS.md").write_text( + "\n".join( + [ + "# AGENTS.md", + "", + "- scripts/repo-maintenance/validate-all.sh", + "- scripts/repo-maintenance/sync-shared.sh", + "- scripts/repo-maintenance/release.sh", + "", + ] + ), + encoding="utf-8", + ) + subprocess.run(["git", "init"], cwd=tmpdir, check=True, capture_output=True, text=True) + + proc = subprocess.run( + ["sh", "scripts/repo-maintenance/validate-all.sh"], + cwd=tmpdir, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr or proc.stdout) + self.assertIn("Repo-maintenance validation completed successfully.", proc.stdout) + + def test_release_script_encodes_protected_main_standard_flow(self) -> None: + release_script = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/release.sh").read_text( + encoding="utf-8" + ) + self.assertIn("Standard release mode must run from a release branch or worktree", release_script) + self.assertIn("version-bump.sh", release_script) + self.assertIn("ensure_operation", release_script) + self.assertIn("--operation prepare|inspect|advance", release_script) + self.assertIn("Version bump commit for $RELEASE_TAG is already at HEAD", release_script) + self.assertIn("emit_continuation_packet", release_script) + self.assertIn("minimum_delay_minutes", release_script) + self.assertIn("reuse a live matching host-native continuation", release_script) + self.assertIn("inspect_pr_gate", release_script) + self.assertIn("--json name,bucket", release_script) + self.assertIn("REPO_MAINTENANCE_MIN_REQUIRED_CHECKS", release_script) + self.assertIn("gh pr checks exits 8 while pending", release_script) + self.assertIn('not-started|awaiting-branch-visibility', release_script) + self.assertIn("remote_branch_is_visible", release_script) + self.assertIn("remote_tag_is_visible", release_script) + self.assertIn("github_release_is_visible", release_script) + self.assertIn("push_release_branch", release_script) + self.assertIn("push_release_tag", release_script) + self.assertIn('rev-list -n 1 "$RELEASE_TAG"', release_script) + self.assertNotIn('gh pr checks "$pr_number" --watch', release_script) + self.assertNotIn('sleep "$poll_seconds"', release_script) + self.assertIn('select(.state == "COMMENTED")', release_script) + self.assertIn("valid concerns in code, or add out-of-scope concerns to ROADMAP.md", release_script) + self.assertIn('gh pr merge "$pr_number" --merge --delete-branch', release_script) + self.assertIn('pull --ff-only origin "$base_branch"', release_script) + self.assertNotIn("release tag `$RELEASE_TAG` was created locally before this PR", release_script) + standard_flow = release_script[release_script.index("run_standard_release()") :] + self.assertLess(standard_flow.index("inspect_pr_gate \"$pr_number\""), standard_flow.index("create_release_tag")) + self.assertLess(standard_flow.index("check_pr_comments \"$pr_number\""), standard_flow.index("create_release_tag")) + self.assertLess(standard_flow.index("fast_forward_base_branch"), standard_flow.index("create_release_tag")) + + def test_common_release_helpers_cover_delayed_github_state(self) -> None: + common_script = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/lib/common.sh").read_text( + encoding="utf-8" + ) + self.assertIn("remote_branch_is_visible", common_script) + self.assertIn("remote_tag_is_visible", common_script) + self.assertIn("github_release_is_visible", common_script) + self.assertNotIn("github_wait_timeout", common_script) + self.assertNotIn("sleep", common_script) + + push_step = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/release/30-push-release.sh").read_text( + encoding="utf-8" + ) + self.assertIn('remote_branch_is_visible "$branch_name"', push_step) + self.assertIn('remote_tag_is_visible "$RELEASE_TAG"', push_step) + + release_step = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/release/40-github-release.sh").read_text( + encoding="utf-8" + ) + self.assertIn('github_release_is_visible "$RELEASE_TAG"', release_step) + + def test_release_helpers_preserve_prerelease_github_metadata(self) -> None: + common_script = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/lib/common.sh").read_text( + encoding="utf-8" + ) + release_script = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/release.sh").read_text( + encoding="utf-8" + ) + release_step = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/release/40-github-release.sh").read_text( + encoding="utf-8" + ) + + self.assertIn("is_semver_prerelease_tag", common_script) + self.assertIn("expected_github_prerelease_value", common_script) + self.assertIn("github_release_create_prerelease_flag", common_script) + self.assertIn("verify_github_release_prerelease_metadata", common_script) + self.assertIn("--json isPrerelease --jq .isPrerelease", common_script) + self.assertIn("prerelease metadata mismatch", common_script) + + for release_text in (release_script, release_step): + with self.subTest(surface=release_text[:32]): + self.assertIn('prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")"', release_text) + self.assertIn('create_github_release_from_notes_or_generated "$RELEASE_TAG" "$prerelease_flag"', release_text) + self.assertIn('verify_github_release_prerelease_metadata "$RELEASE_TAG"', release_text) + + def test_release_notes_helper_prefers_checked_in_notes_then_falls_back(self) -> None: + common_script = ROOT / "skills/maintain-project-repo/assets/repo-maintenance/lib/common.sh" + with tempfile.TemporaryDirectory() as tmpdir: + repo_root = Path(tmpdir) + notes_dir = repo_root / "docs/releases" + notes_dir.mkdir(parents=True) + tagged_notes = notes_dir / "v1.2.3.md" + versioned_notes = notes_dir / "1.2.3.md" + tagged_notes.write_text("# Tagged notes\n", encoding="utf-8") + versioned_notes.write_text("# Versioned notes\n", encoding="utf-8") + fake_bin = repo_root / "bin" + fake_bin.mkdir() + fake_gh = fake_bin / "gh" + fake_gh.write_text('#!/usr/bin/env sh\nprintf "%s\\n" "$*" >> "$GH_LOG"\n', encoding="utf-8") + fake_gh.chmod(0o755) + log_path = repo_root / "gh.log" + + script = '\n'.join( + [ + f'. "{common_script}"', + f'REPO_ROOT="{repo_root}"', + 'create_github_release_from_notes_or_generated v1.2.3 ""', + ] + ) + env = dict(os.environ, PATH=f"{fake_bin}:{os.environ['PATH']}", GH_LOG=str(log_path)) + subprocess.run(["sh", "-c", script], check=True, capture_output=True, text=True, env=env) + self.assertIn(f"--notes-file {tagged_notes}", log_path.read_text(encoding="utf-8")) + + tagged_notes.unlink() + log_path.unlink() + subprocess.run(["sh", "-c", script], check=True, capture_output=True, text=True, env=env) + self.assertIn(f"--notes-file {versioned_notes}", log_path.read_text(encoding="utf-8")) + + versioned_notes.unlink() + log_path.unlink() + subprocess.run(["sh", "-c", script], check=True, capture_output=True, text=True, env=env) + self.assertIn("--generate-notes", log_path.read_text(encoding="utf-8")) + + def test_coderabbit_review_unavailable_fixtures_are_narrow(self) -> None: + helper = ROOT / "skills/maintain-project-repo/assets/repo-maintenance/lib/coderabbit.sh" + fixtures = [ + ("CodeRabbit", "Review unavailable: monthly quota reached.", 0), + ("coderabbitai", "Rate limit reached; no review was produced.", 0), + ("CodeRabbit", "Found a potential nil dereference.", 1), + ("GitHub Actions", "Rate limit reached.", 1), + ] + for source, message, expected_returncode in fixtures: + with self.subTest(source=source, message=message): + proc = subprocess.run( + ["sh", "-c", f'. "{helper}"; coderabbit_review_is_unavailable "$1" "$2"', "sh", source, message], + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(proc.returncode, expected_returncode, proc.stderr) + + def test_release_gate_only_exempts_explicit_coderabbit_unavailability(self) -> None: + release_script = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/release.sh").read_text( + encoding="utf-8" + ) + self.assertIn("detect_coderabbit_review_unavailable", release_script) + self.assertIn("repos/$repo_name/commits/$head_sha/check-runs", release_script) + self.assertIn("CODERABBIT_UNAVAILABLE_COMMENT_COUNT", release_script) + self.assertIn('contains("coderabbit")', release_script) + self.assertIn("ignoring only its pending review context and diagnostic comments", release_script) + + def test_release_env_documents_scheduled_continuation_default(self) -> None: + release_env = (ROOT / "skills/maintain-project-repo/assets/repo-maintenance/config/release.env").read_text( + encoding="utf-8" + ) + self.assertIn("REPO_MAINTENANCE_RELEASE_OPERATION=prepare", release_env) + self.assertIn("REPO_MAINTENANCE_MIN_REQUIRED_CHECKS=1", release_env) + self.assertIn("host-native continuation", release_env) + self.assertIn("five", release_env) + self.assertIn("Never add a shell poll loop", release_env) + self.assertIn("do not delete/recreate it after an unchanged snapshot", release_env) + + def test_release_guidance_reuses_healthy_pending_continuations(self) -> None: + skill_text = (ROOT / "skills/maintain-project-repo/SKILL.md").read_text(encoding="utf-8") + release_modes = (ROOT / "skills/maintain-project-repo/references/release-modes.md").read_text( + encoding="utf-8" + ) + prompts = (ROOT / "skills/maintain-project-repo/references/automation-prompts.md").read_text( + encoding="utf-8" + ) + + for text in (skill_text, release_modes, prompts): + with self.subTest(surface=text[:32]): + self.assertIn("matching", text) + self.assertIn("pending and healthy", text) + self.assertIn("do not delete/recreate", text) + + def test_continuation_plan_matches_emitted_packet_schema(self) -> None: + socket_root = ROOT.parents[1] + plan = (socket_root / "docs/maintainers/deferred-work-wakeup-policy-plan.md").read_text( + encoding="utf-8" + ) + self.assertIn('"minimum_delay_minutes": 5', plan) + self.assertIn('"pr_number": "123"', plan) + self.assertNotIn('"not_before"', plan) + self.assertNotIn('"observed_at"', plan) + self.assertIn("Superseded by the live", plan) + + live_policy = (socket_root / "docs/maintainers/deferred-work-wakeup-policy.md").read_text( + encoding="utf-8" + ) + self.assertIn("Reuse that item unchanged", live_policy) + self.assertIn("minimum delay is five minutes", live_policy) + + def test_branch_accounting_guidance_is_documented(self) -> None: + skill_text = (ROOT / "skills/maintain-project-repo/SKILL.md").read_text(encoding="utf-8") + release_modes = (ROOT / "skills/maintain-project-repo/references/release-modes.md").read_text( + encoding="utf-8" + ) + automation_prompts = (ROOT / "skills/maintain-project-repo/references/automation-prompts.md").read_text( + encoding="utf-8" + ) + + for text in (skill_text, release_modes): + with self.subTest(surface=text[:32]): + self.assertIn("branch accounting", text) + self.assertIn("git branch --no-merged ", text) + self.assertIn("commit reachability", text) + self.assertIn("temporary rescue refs", text) + self.assertIn("explicit archive ref", text) + + self.assertIn("accounts for every local branch not contained by `main`", automation_prompts) + self.assertIn("do not delete local branches, remote branches, worktrees, archive refs", automation_prompts) + + def test_release_and_publish_triggers_are_documented(self) -> None: + skill_text = (ROOT / "skills/maintain-project-repo/SKILL.md").read_text(encoding="utf-8") + trigger_reference = (ROOT / "skills/maintain-project-repo/references/trigger-eval.md").read_text( + encoding="utf-8" + ) + openai_yaml = (ROOT / "skills/maintain-project-repo/agents/openai.yaml").read_text(encoding="utf-8") + + self.assertIn("references/trigger-eval.md", skill_text) + self.assertIn("maintain-github-repository", skill_text) + + for expected in ( + "release or publish a version", + "bump and tag a release", + "create the GitHub release", + "protected-main release", + "release cleanup and branch accounting", + ): + with self.subTest(expected=expected): + self.assertIn(expected, skill_text) + + for expected in ( + "Release version 1.4.0.", + "Publish this package.", + "Tag this commit and create the GitHub release.", + "Prepare this branch for a protected-main release.", + "Apply my normal GitHub repository settings.", + ): + with self.subTest(expected=expected): + self.assertIn(expected, trigger_reference) + + self.assertIn("protected-main release, publish, tag, GitHub release", openai_yaml) + + def test_prerelease_release_metadata_guidance_is_documented(self) -> None: + skill_text = (ROOT / "skills/maintain-project-repo/SKILL.md").read_text(encoding="utf-8") + release_modes = (ROOT / "skills/maintain-project-repo/references/release-modes.md").read_text( + encoding="utf-8" + ) + + for text in (skill_text, release_modes): + with self.subTest(surface=text[:32]): + self.assertIn("SemVer prerelease", text) + self.assertIn("--prerelease", text) + self.assertIn("prerelease metadata", text) + + def test_refresh_preserves_repo_specific_extra_script(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "install") + self.assertEqual(code, 0) + + custom_script = Path(tmpdir, "scripts/repo-maintenance/validations/90-custom.sh") + custom_script.parent.mkdir(parents=True, exist_ok=True) + custom_script.write_text("#!/usr/bin/env sh\nexit 0\n", encoding="utf-8") + + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "refresh") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertTrue(custom_script.is_file()) + + def test_report_only_can_select_xcode_profile(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + code, payload = self.run_script("--repo-root", tmpdir, "--operation", "report-only", "--profile", "xcode-app") + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "success") + self.assertEqual(payload["profile"], "xcode-app") + self.assertIn("Scripts/repo-maintenance/validate-all.sh", payload["managed_files"]) + joined = "\n".join(payload["actions"]) + self.assertIn("profile.env", joined) + self.assertIn("xcode-app profile", joined) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/orchestrate-agent-work/SKILL.md b/skills/orchestrate-agent-work/SKILL.md new file mode 100644 index 000000000..3a2899ff7 --- /dev/null +++ b/skills/orchestrate-agent-work/SKILL.md @@ -0,0 +1,52 @@ +--- +name: orchestrate-agent-work +description: Coordinate bounded worker tasks with a launch envelope, report-back, escalation, and synthesis contract. Use before spawning, resuming, steering, cancelling, or closing subagents. +--- + +# Orchestrate Agent Work + +Use this skill at the delegation boundary. The coordinator owns the user goal, +decomposition, authority, final decision, validation, and closure; workers own +only their assigned slice. + +## Workflow + +1. Confirm delegation is requested or required by the active workflow. Prefer a + single agent when the work is sequential or one small write target. +2. Split only independent work. Assign one coordinator and one owner per + artifact, worktree, branch, validation lane, and external side effect. +3. Give every worker a launch envelope: + - task and coordinator identity; + - objective, bounded scope, inputs, and source-of-truth locations; + - allowed and forbidden actions, including write authority; + - acceptance criteria, validation owner, and output location; + - required terminal report and escalation conditions. +4. State whether the coordinator waits for all results, consumes partial + reports, or hands work to a durable task surface. +5. Track each worker until it is `completed`, `blocked`, `needs-decision`, + `cancelled`, or `failed`. A worker must not silently stop. +6. Acknowledge each result: consume it, request one bounded follow-up, mark it + superseded, or cancel it explicitly. Then synthesize and verify the result + in the coordinator thread. + +## Worker Report Contract + +Require the final report to contain the terminal state, outcome, evidence or +artifacts, validation run or intentionally skipped, uncertainty, and one +recommended next action. Workers escalate rather than improvise when scope, +authority, ownership, evidence, or acceptance criteria change. + +## Boundaries + +- Keep user-facing decisions, final edits, merge, release, and final validation + with the coordinator unless a user-approved disjoint ownership plan says + otherwise. +- Default to one delegation layer. Nested orchestration needs an explicit + reason, bounded fan-out/depth, and host support. +- Use `coordinate-external-agents` when work crosses a host, session, or + durable ownership boundary. +- Use `coordinate-worktrees-and-threads` when workers need repository state. + When the selected host is Codex and the worktree mode, local environment, or + app-owned thread association needs a decision, compose it with + `repository-skills:codex-gui-worktree-workflow`. On Hermes or another host, + keep the ownership contract host-neutral and use ordinary Git worktrees. diff --git a/skills/orchestrate-agent-work/agents/openai.yaml b/skills/orchestrate-agent-work/agents/openai.yaml new file mode 100644 index 000000000..d1bc8af65 --- /dev/null +++ b/skills/orchestrate-agent-work/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Orchestrate Agent Work" + short_description: "Launch and close bounded worker tasks." + default_prompt: "Use $orchestrate-agent-work to coordinate these workers with a launch envelope, report-back contract, and explicit closure." diff --git a/skills/repository-operations-workflow/SKILL.md b/skills/repository-operations-workflow/SKILL.md new file mode 100644 index 000000000..f7d8821e7 --- /dev/null +++ b/skills/repository-operations-workflow/SKILL.md @@ -0,0 +1,48 @@ +--- +name: repository-operations-workflow +description: Route repository work to focused Git, GitHub, worktree, settings, or release workflows. Use first when a request spans several repository operations or the correct owner is unclear. +--- + +# Repository Operations Workflow + +## Purpose + +Provide one entry point for `repository-skills` without turning it into a +catch-all implementation surface. Classify the request, inspect only the state +needed for that classification, then hand work to the narrowest owner. + +## Routing + +1. Read the closest `AGENTS.md`, inspect `git status --short --branch`, and + identify the repository root and remote only when the request needs them. +2. Route by the primary outcome: + + | Outcome | Owner | + | --- | --- | + | Status, branch, commit, history, rebase, conflict, recovery, or Git-default alignment | `git-workflow` | + | Pull request, review, issue, CI, or collaboration handoff | `github-collaboration-workflow` | + | Codex worktree mode, local environment, or app-owned worktree behavior | `codex-gui-worktree-workflow` | + | Parallel worker branch, worktree, write, or integration ownership | `coordinate-worktrees-and-threads` | + | GitHub settings, rulesets, security automation, or repository policy | `maintain-github-repository` | + | Version bump, tag, release, publication, or branch-accounting cleanup | `maintain-project-repo` | +3. When the request crosses multiple outcomes, name the sequence and keep each + mutation with its owning workflow. For example: select a worktree, make a + focused Git change, prepare a PR, then use the release workflow only after a + release is explicitly requested. +4. Report the selected owner, observed repository state, required authority, + and one next action. + +## Guardrails + +- Do not use a release workflow for ordinary local edits or a GitHub settings + workflow for pull-request collaboration. +- Do not infer permission to push, merge, tag, delete, publish, or change + GitHub settings from a read-only request. +- Keep branch/worktree ownership explicit before parallel work; a branch does + not grant permission to mutate shared repository state. +- Treat Gale's fetch-prune, fast-forward-only pull, and tracking-branch rebase + preferences as machine-level defaults. Route their inspection and any + repository-specific override to `git-workflow`; do not install them as + repository-local configuration. +- On Hermes or another non-Codex host, treat Codex GUI worktree association as + unavailable and use ordinary Git worktree guidance instead. diff --git a/skills/repository-operations-workflow/agents/openai.yaml b/skills/repository-operations-workflow/agents/openai.yaml new file mode 100644 index 000000000..6a8ad9752 --- /dev/null +++ b/skills/repository-operations-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Repository Operations" + short_description: "Route Git, GitHub, worktree, settings, and release work" + default_prompt: "Use $repository-operations-workflow to classify this repository task, inspect the minimum relevant state, and hand it to the focused Git, GitHub, worktree, settings, or release workflow." diff --git a/skills/repository-operations-workflow/tests/test_repository_operations_workflows.py b/skills/repository-operations-workflow/tests/test_repository_operations_workflows.py new file mode 100644 index 000000000..95199b80a --- /dev/null +++ b/skills/repository-operations-workflow/tests/test_repository_operations_workflows.py @@ -0,0 +1,81 @@ +from pathlib import Path + +import yaml + + +SKILL_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_SKILLS_ROOT = SKILL_ROOT.parent + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def metadata(path: Path) -> dict: + return yaml.safe_load(read(path).split("---", 2)[1]) + + +def test_router_names_each_repository_owner_and_its_mutation_boundaries() -> None: + router = read(SKILL_ROOT / "SKILL.md") + router_metadata = metadata(SKILL_ROOT / "SKILL.md") + agent = yaml.safe_load(read(SKILL_ROOT / "agents" / "openai.yaml"))["interface"] + + assert router_metadata["name"] == "repository-operations-workflow" + for expected in ( + "`git-workflow`", + "`github-collaboration-workflow`", + "`codex-gui-worktree-workflow`", + "`coordinate-worktrees-and-threads`", + "`maintain-github-repository`", + "`maintain-project-repo`", + "machine-level defaults", + "Do not infer permission to push, merge, tag, delete, publish, or change", + ): + assert expected in router + assert "$repository-operations-workflow" in agent["default_prompt"] + + +def test_git_and_github_workflows_keep_local_collaboration_and_release_owners_separate() -> None: + git_skill = REPOSITORY_SKILLS_ROOT / "git-workflow" / "SKILL.md" + github_skill = REPOSITORY_SKILLS_ROOT / "github-collaboration-workflow" / "SKILL.md" + + assert metadata(git_skill)["name"] == "git-workflow" + assert metadata(github_skill)["name"] == "github-collaboration-workflow" + + git_text = read(git_skill) + github_text = read(github_skill) + for expected in ( + "`git worktree list`", + "reflog", + "Push, force push, merge, tag, and destructive recovery actions require clear", + "`github-collaboration-workflow`", + "`maintain-project-repo`", + "fetch.prune=true", + "pull.ff=only", + "branch.autoSetupRebase=always", + "repository-local config", + ): + assert expected in git_text + for expected in ( + "`gh` authentication", + "one bounded snapshot", + "`cronjob`", + "`maintain-github-repository`", + "`maintain-project-repo`", + ): + assert expected in github_text + + +def test_git_baseline_is_shared_machine_policy_without_local_template_writes() -> None: + baseline = read(REPOSITORY_SKILLS_ROOT / "git-workflow" / "references" / "gale-git-baseline.md") + repo_maintenance = read(REPOSITORY_SKILLS_ROOT / "maintain-project-repo" / "SKILL.md") + + for expected in ( + "fetch.prune = true", + "pull.ff = only", + "branch.autoSetupRebase = always", + "git config --show-origin --get-regexp", + "git config --local", + ): + assert expected in baseline + assert "not write `git config --local`" in repo_maintenance diff --git a/skills/sync-xcode-workspace-guidance/SKILL.md b/skills/sync-xcode-workspace-guidance/SKILL.md index 880572ae9..fc13b1d0b 100644 --- a/skills/sync-xcode-workspace-guidance/SKILL.md +++ b/skills/sync-xcode-workspace-guidance/SKILL.md @@ -30,7 +30,12 @@ guidance section when needed. 6. Route app-specific guidance to `sync-xcode-project-guidance`, package-specific guidance to `sync-swift-package-guidance`, and active execution to `xcode-build-run-workflow` or `xcode-testing-workflow`. -7. Keep optional services under `Services/` as their own deployment and runtime +7. When the workspace root needs shared validation, release, or GitHub Actions + maintenance, compose it with `repository-skills:maintain-project-repo` using + the `xcode-workspace` profile. Its root dispatcher validates workspace shape + and runs any component-owned maintenance entrypoints serially; it does not + replace app, package, or service-local build policy. +8. Keep optional services under `Services/` as their own deployment and runtime boundary. Use the selected server skill for service-local policy. ## Audit Contract diff --git a/skills/sync-xcode-workspace-guidance/assets/append-section.md b/skills/sync-xcode-workspace-guidance/assets/append-section.md index 4a393cae2..2871e6b57 100644 --- a/skills/sync-xcode-workspace-guidance/assets/append-section.md +++ b/skills/sync-xcode-workspace-guidance/assets/append-section.md @@ -15,3 +15,8 @@ - Use `sync-xcode-project-guidance` for an app project, `sync-swift-package-guidance` for a package, and `xcode-build-run-workflow` or `xcode-testing-workflow` for active Xcode work. +- Use `repository-skills:maintain-project-repo` with the `xcode-workspace` + profile when the workspace root needs shared validation, release, or GitHub + Actions maintenance. It validates the root composition and serially delegates + to component-owned maintenance entrypoints; it does not replace app, package, + or service-local build policy. diff --git a/uv.lock b/uv.lock index eab9e9079..1509f5473 100644 --- a/uv.lock +++ b/uv.lock @@ -286,7 +286,7 @@ wheels = [ [[package]] name = "socket-maintenance" -version = "9.31.2" +version = "9.32.0" source = { virtual = "." } [package.dev-dependencies]