From 81d8bce3dbc4fd00fce48fdeb6fa90a10e10a3b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:06:37 +0000 Subject: [PATCH 01/11] Bump the actions-deps group with 4 updates (#107) Bumps the actions-deps group with 4 updates: [ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/validate-task.yml](https://github.com/ptr727/projecttemplate) and [ptr727/ProjectTemplate/.github/workflows/build-release-task.yml](https://github.com/ptr727/projecttemplate). Updates `ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml` from 2.0.357 to 2.0.380 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/5d0fa7f26833f9c932fdc2b191922044efad191e...74ef727a7aa96493bcd5bc2ce49b2977721678a3) Updates `ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml` from 2.0.357 to 2.0.380 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/5d0fa7f26833f9c932fdc2b191922044efad191e...74ef727a7aa96493bcd5bc2ce49b2977721678a3) Updates `ptr727/ProjectTemplate/.github/workflows/validate-task.yml` from 2.0.376 to 2.0.380 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/537b1837fe90e2288f71034ef96cc4e949937712...74ef727a7aa96493bcd5bc2ce49b2977721678a3) Updates `ptr727/ProjectTemplate/.github/workflows/build-release-task.yml` from 2.0.376 to 2.0.380 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/537b1837fe90e2288f71034ef96cc4e949937712...74ef727a7aa96493bcd5bc2ce49b2977721678a3) --- updated-dependencies: - dependency-name: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml dependency-version: 2.0.380 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml dependency-version: 2.0.380 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/validate-task.yml dependency-version: 2.0.380 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml dependency-version: 2.0.380 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index ca9b84b..b400fa3 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@5d0fa7f26833f9c932fdc2b191922044efad191e # 2.0.357 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ec7e08d..e46c08a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@5d0fa7f26833f9c932fdc2b191922044efad191e # 2.0.357 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@537b1837fe90e2288f71034ef96cc4e949937712 # 2.0.376 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@537b1837fe90e2288f71034ef96cc4e949937712 # 2.0.376 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index b574fc7..4e6e975 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@537b1837fe90e2288f71034ef96cc4e949937712 # 2.0.376 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 permissions: contents: read From 3229f4474c6289a03648d374e30264e09669f18d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 06:17:38 +0000 Subject: [PATCH 02/11] Bump the actions-deps group with 4 updates (#109) Bumps the actions-deps group with 4 updates: [ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/validate-task.yml](https://github.com/ptr727/projecttemplate) and [ptr727/ProjectTemplate/.github/workflows/build-release-task.yml](https://github.com/ptr727/projecttemplate). Updates `ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml` from 2.0.380 to 2.0.416 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/74ef727a7aa96493bcd5bc2ce49b2977721678a3...f3bfdb43aef34750e1a997e68ddfb1364b2bd097) Updates `ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml` from 2.0.380 to 2.0.416 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/74ef727a7aa96493bcd5bc2ce49b2977721678a3...f3bfdb43aef34750e1a997e68ddfb1364b2bd097) Updates `ptr727/ProjectTemplate/.github/workflows/validate-task.yml` from 2.0.380 to 2.0.416 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/74ef727a7aa96493bcd5bc2ce49b2977721678a3...f3bfdb43aef34750e1a997e68ddfb1364b2bd097) Updates `ptr727/ProjectTemplate/.github/workflows/build-release-task.yml` from 2.0.380 to 2.0.416 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/74ef727a7aa96493bcd5bc2ce49b2977721678a3...f3bfdb43aef34750e1a997e68ddfb1364b2bd097) --- updated-dependencies: - dependency-name: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml dependency-version: 2.0.416 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml dependency-version: 2.0.416 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/validate-task.yml dependency-version: 2.0.416 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml dependency-version: 2.0.416 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index b400fa3..1bc6f73 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e46c08a..5ea78b9 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 4e6e975..db66144 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@74ef727a7aa96493bcd5bc2ce49b2977721678a3 # 2.0.380 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 permissions: contents: read From 4d6ef8f567d096b9e7e0b99e31540203fabbb37e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 06:09:08 +0000 Subject: [PATCH 03/11] Bump the actions-deps group with 4 updates (#110) Bumps the actions-deps group with 4 updates: [ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/validate-task.yml](https://github.com/ptr727/projecttemplate) and [ptr727/ProjectTemplate/.github/workflows/build-release-task.yml](https://github.com/ptr727/projecttemplate). Updates `ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml` from 2.0.416 to 2.0.440 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/f3bfdb43aef34750e1a997e68ddfb1364b2bd097...30b93226820112ebf8bd93e69c8a80932e0c4b39) Updates `ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml` from 2.0.416 to 2.0.440 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/f3bfdb43aef34750e1a997e68ddfb1364b2bd097...30b93226820112ebf8bd93e69c8a80932e0c4b39) Updates `ptr727/ProjectTemplate/.github/workflows/validate-task.yml` from 2.0.416 to 2.0.440 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/f3bfdb43aef34750e1a997e68ddfb1364b2bd097...30b93226820112ebf8bd93e69c8a80932e0c4b39) Updates `ptr727/ProjectTemplate/.github/workflows/build-release-task.yml` from 2.0.416 to 2.0.440 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/f3bfdb43aef34750e1a997e68ddfb1364b2bd097...30b93226820112ebf8bd93e69c8a80932e0c4b39) --- updated-dependencies: - dependency-name: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml dependency-version: 2.0.440 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml dependency-version: 2.0.440 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/validate-task.yml dependency-version: 2.0.440 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml dependency-version: 2.0.440 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 1bc6f73..0a01892 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 5ea78b9..57da4c0 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index db66144..7333407 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@f3bfdb43aef34750e1a997e68ddfb1364b2bd097 # 2.0.416 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 permissions: contents: read From 2d36fefd269e82e762ac551c2d381bfbe669317d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:08:36 +0000 Subject: [PATCH 04/11] Bump the actions-deps group with 4 updates (#111) Bumps the actions-deps group with 4 updates: [ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/validate-task.yml](https://github.com/ptr727/projecttemplate) and [ptr727/ProjectTemplate/.github/workflows/build-release-task.yml](https://github.com/ptr727/projecttemplate). Updates `ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml` from 2.0.440 to 2.0.453 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/30b93226820112ebf8bd93e69c8a80932e0c4b39...be4d3bf0a6c87adebe3aa27d32a57291c8a919b9) Updates `ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml` from 2.0.440 to 2.0.453 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/30b93226820112ebf8bd93e69c8a80932e0c4b39...be4d3bf0a6c87adebe3aa27d32a57291c8a919b9) Updates `ptr727/ProjectTemplate/.github/workflows/validate-task.yml` from 2.0.440 to 2.0.453 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/30b93226820112ebf8bd93e69c8a80932e0c4b39...be4d3bf0a6c87adebe3aa27d32a57291c8a919b9) Updates `ptr727/ProjectTemplate/.github/workflows/build-release-task.yml` from 2.0.440 to 2.0.453 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/30b93226820112ebf8bd93e69c8a80932e0c4b39...be4d3bf0a6c87adebe3aa27d32a57291c8a919b9) --- updated-dependencies: - dependency-name: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml dependency-version: 2.0.453 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml dependency-version: 2.0.453 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/validate-task.yml dependency-version: 2.0.453 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml dependency-version: 2.0.453 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 0a01892..a1ad043 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 57da4c0..4d60423 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 7333407..3901859 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@30b93226820112ebf8bd93e69c8a80932e0c4b39 # 2.0.440 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 permissions: contents: read From 747531b7aec49b3563e510216d79a112bc07b39f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:07:36 +0000 Subject: [PATCH 05/11] Bump the actions-deps group with 4 updates (#112) Bumps the actions-deps group with 4 updates: [ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/validate-task.yml](https://github.com/ptr727/projecttemplate) and [ptr727/ProjectTemplate/.github/workflows/build-release-task.yml](https://github.com/ptr727/projecttemplate). Updates `ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml` from 2.0.453 to 2.0.465 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/be4d3bf0a6c87adebe3aa27d32a57291c8a919b9...fa63163f4400830a6d86d306a2f9b542ca1a5c4a) Updates `ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml` from 2.0.453 to 2.0.465 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/be4d3bf0a6c87adebe3aa27d32a57291c8a919b9...fa63163f4400830a6d86d306a2f9b542ca1a5c4a) Updates `ptr727/ProjectTemplate/.github/workflows/validate-task.yml` from 2.0.453 to 2.0.465 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/be4d3bf0a6c87adebe3aa27d32a57291c8a919b9...fa63163f4400830a6d86d306a2f9b542ca1a5c4a) Updates `ptr727/ProjectTemplate/.github/workflows/build-release-task.yml` from 2.0.453 to 2.0.465 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/be4d3bf0a6c87adebe3aa27d32a57291c8a919b9...fa63163f4400830a6d86d306a2f9b542ca1a5c4a) --- updated-dependencies: - dependency-name: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml dependency-version: 2.0.465 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml dependency-version: 2.0.465 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/validate-task.yml dependency-version: 2.0.465 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml dependency-version: 2.0.465 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index a1ad043..d957f62 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4d60423..728ee6b 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 3901859..836f6f2 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@be4d3bf0a6c87adebe3aa27d32a57291c8a919b9 # 2.0.453 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 permissions: contents: read From ed7c5a31717ac319ad5ea1463d5dd978deea1f1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:10:05 +0000 Subject: [PATCH 06/11] Bump the actions-deps group with 4 updates (#113) Bumps the actions-deps group with 4 updates: [ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml](https://github.com/ptr727/projecttemplate), [ptr727/ProjectTemplate/.github/workflows/validate-task.yml](https://github.com/ptr727/projecttemplate) and [ptr727/ProjectTemplate/.github/workflows/build-release-task.yml](https://github.com/ptr727/projecttemplate). Updates `ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml` from 2.0.465 to 2.0.483 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/fa63163f4400830a6d86d306a2f9b542ca1a5c4a...a33d7d77ffa7f6af52cddce01f7250be282546ed) Updates `ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml` from 2.0.465 to 2.0.483 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/fa63163f4400830a6d86d306a2f9b542ca1a5c4a...a33d7d77ffa7f6af52cddce01f7250be282546ed) Updates `ptr727/ProjectTemplate/.github/workflows/validate-task.yml` from 2.0.465 to 2.0.483 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/fa63163f4400830a6d86d306a2f9b542ca1a5c4a...a33d7d77ffa7f6af52cddce01f7250be282546ed) Updates `ptr727/ProjectTemplate/.github/workflows/build-release-task.yml` from 2.0.465 to 2.0.483 - [Release notes](https://github.com/ptr727/projecttemplate/releases) - [Changelog](https://github.com/ptr727/ProjectTemplate/blob/main/HISTORY.md) - [Commits](https://github.com/ptr727/projecttemplate/compare/fa63163f4400830a6d86d306a2f9b542ca1a5c4a...a33d7d77ffa7f6af52cddce01f7250be282546ed) --- updated-dependencies: - dependency-name: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml dependency-version: 2.0.483 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml dependency-version: 2.0.483 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/validate-task.yml dependency-version: 2.0.483 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml dependency-version: 2.0.483 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index d957f62..359dac0 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 728ee6b..e68b0c9 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 836f6f2..c298400 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@fa63163f4400830a6d86d306a2f9b542ca1a5c4a # 2.0.465 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 permissions: contents: read From cc261d5c6ca87336698f84fcea90ec36ad7aff98 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 29 Aug 2026 10:33:11 -0700 Subject: [PATCH 07/11] Resync carried fleet content with the hub (#115) * Resync carried instruction set and intent files with the hub Re-vendors the stale verbatim sections of AGENTS.md and GOVERNANCE.md, adds the missing CLAUDE.md entry point, and reconciles CODESTYLE.md, WORKFLOW.md, and .github/copilot-instructions.md against the current hub canonical while preserving this repo's own local content (the C++ and Python-subtree CODESTYLE.md sections, the widened spelling gate, and the empty Disproved Claims ledger). Carries the hub's full .editorconfig template, which fixes #114: the repo's copy was an ad hoc 11-line file missing the whole fleet template, including the *.bat/*.cmd CRLF exception that was blocking PR #108's merge. Rewrites AUDIT.md's General Settings/Rulesets and Secrets sections to check against the hub's checkout rather than local repo-config/ and spec/secrets.json copies, and deletes those five hub-only files per spec/divergences.json's retire disposition, sweeping every reference to them in OPERATIONS.md and the validate action. * Carry the current .github/skills tree from the hub Adds the three skills the hub added since this repo's last carry (drive-pr, local-strict-review, merge-and-release) and re-vendors the 20 that had drifted, via scripts/carry.py apply against hub commit 3258284. Fully hub-owned content, mechanically applied. * Restore Running the Linters Locally and fix the Repository Layout carry Nests the hub's "Running the Linters Locally" content back under GOVERNANCE.md "Workflow YAML Conventions" as a subsection, matching the fleet's established carry convention: my earlier verbatim re-vendor of that section dropped it, since the hub's own copy holds it as an independent top-level section positioned after "Workflow YAML Conventions" rather than nested inside it, breaking the anchor AGENTS.md and CODESTYLE.md both route to. Also fixes GOVERNANCE.md "Repository Layout" to stop describing the now-deleted repo-config/ and spec/secrets.json as present locally. * Strip template-repo hyperlinks from the carried AUDIT.md AUDIT.md is a carried file per comment-and-doc-style's carried-doc references rule, which bans a link to the template repo outside a hub-hosted-tool instruction. Removes the browsable github.com/ptr727/ ProjectTemplate links to docs/repo-config.md and registry/repos.json, keeping the plain-text mentions that name what a hub checkout runs. * Flag docker_lint.py's --root against the live checkout in OPERATIONS.md GOVERNANCE.md's newly-restored "Running the Linters Locally" section documents python3 scripts/docker_lint.py --root "$PWD", but the wrapper's read-only bind mount doesn't exclude secrets.yaml the way this repo's own snapshot pattern does. Extends the existing "Never mount the live checkout" hazard bullet to cover it, since that's this repo's own doc rather than hub-carried content. Filed the wrapper gap upstream as ptr727/ProjectTemplate#1090. --- .editorconfig | 198 ++++++++++++++- .github/actions/validate/action.yml | 1 - .github/copilot-instructions.md | 2 + .github/skills/agent-conduct/SKILL.md | 5 +- .github/skills/audit-a-repo/SKILL.md | 2 +- .github/skills/comment-and-doc-style/SKILL.md | 34 ++- .../references/carried-doc-references.md | 2 +- .../references/line-endings.md | 5 +- .../copilot-instructions-keeper/SKILL.md | 14 +- .github/skills/dotnet-codestyle/SKILL.md | 9 +- .../references/conventions.md | 12 +- .../references/project-config.md | 4 + .github/skills/drive-pr/SKILL.md | 131 ++++++++++ .../skills/git-commit-conventions/SKILL.md | 4 +- .github/skills/local-strict-review/SKILL.md | 71 ++++++ .github/skills/merge-and-release/SKILL.md | 225 ++++++++++++++++++ .../operational-vs-release-workflow/SKILL.md | 1 + .../branch-protection-and-promotion.md | 34 ++- .github/skills/pr-review-conduct/SKILL.md | 31 ++- .github/skills/python-codestyle/SKILL.md | 43 ++-- .../python-codestyle/references/code-style.md | 13 + .../python-codestyle/references/testing.md | 4 + .github/skills/repo-worktree/SKILL.md | 35 ++- .github/skills/resync-a-repo/SKILL.md | 28 +-- .github/skills/shell-codestyle/SKILL.md | 11 +- .github/skills/skill-lifecycle/SKILL.md | 2 +- .github/skills/standup-a-repo/SKILL.md | 20 +- .../upstream-contribution-workflow/SKILL.md | 6 +- .github/skills/workflow-ci-contract/SKILL.md | 2 +- AGENTS.md | 6 + AUDIT.md | 101 +++----- CLAUDE.md | 5 + CODESTYLE.md | 4 +- GOVERNANCE.md | 99 ++++---- OPERATIONS.md | 10 +- WORKFLOW.md | 13 +- repo-config/README.md | 69 ------ repo-config/main.json | 58 ----- repo-config/operational/develop.json | 24 -- repo-config/settings.json | 10 - spec/secrets.json | 10 - 41 files changed, 948 insertions(+), 410 deletions(-) create mode 100644 .github/skills/drive-pr/SKILL.md create mode 100644 .github/skills/local-strict-review/SKILL.md create mode 100644 .github/skills/merge-and-release/SKILL.md create mode 100644 CLAUDE.md delete mode 100644 repo-config/README.md delete mode 100644 repo-config/main.json delete mode 100644 repo-config/operational/develop.json delete mode 100644 repo-config/settings.json delete mode 100644 spec/secrets.json diff --git a/.editorconfig b/.editorconfig index 089a22d..67cd1d5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,207 @@ +# https://editorconfig.org + +# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names +# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview + +# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md +# https://github.com/dotnet/runtime/blob/main/.editorconfig + +# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format +# Verify with: dotnet format style --verify-no-changes --severity=info --verbosity=detailed + +# Root config root = true +# Defaults: LF is the default, and only the CRLF exception below is declared. +# `.gitattributes` mirrors these two defaults as Git's normalization fallback. +# CI verifies the committed bytes against this file. [*] charset = utf-8 end_of_line = lf +indent_size = 4 +indent_style = space insert_final_newline = true trim_trailing_whitespace = true -# Markdown uses trailing whitespace (two spaces) for hard line breaks. +# Markdown files [*.md] trim_trailing_whitespace = false + +# Xml files +[*.{xml,csproj,props,targets}] +indent_size = 2 + +# Yaml files +[*.{yml,yaml}] +indent_size = 2 + +# Windows batch and command scripts: the one CRLF exception to the `[*]` LF default above. +[*.{bat,cmd}] +end_of_line = crlf + +# .NET-only below, covering C# and ReSharper style. +# Everything above is the line-ending governance every derived repo carries, and a non-.NET repo carries this section unused, per CODESTYLE.md's whole-file model. + +# C# files +[*.cs] +# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions". +# Prefer a [SuppressMessage] attribute, or the owning project's .editorconfig. +# Relax a rule repo-wide here only when it applies to every project, never for a brownfield batch. +dotnet_diagnostic.IDE0055.severity = none +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_prefer_braces = true +csharp_prefer_simple_default_expression = true +csharp_prefer_simple_using_statement = true +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_prefer_system_threading_lock = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,abstract,sealed,virtual,override,readonly,unsafe,volatile,async,extern,new,partial:warning +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true +csharp_style_conditional_delegate_call = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = true +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_operators = true +csharp_style_expression_bodied_properties = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_namespace_declarations = file_scoped +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true +csharp_style_prefer_switch_expression = true +csharp_style_prefer_top_level_statements = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false +csharp_using_directive_placement = outside_namespace +dotnet_code_quality_unused_parameters = all +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_prefer_system_hash_code = true +dotnet_property_generation_behavior = prefer_throwing_properties +dotnet_remove_unnecessary_suppression_exclusions = none +dotnet_search_reference_assemblies = true +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false +dotnet_style_readonly_field = true +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# ReSharper settings +resharper_csharp_trailing_comma_in_multiline_lists = true +resharper_csharp_var_for_built_in_types = false +resharper_csharp_var_when_type_is_apparent = false +resharper_csharp_var_when_type_is_not_apparent = false diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml index a1a5c19..d8e171c 100644 --- a/.github/actions/validate/action.yml +++ b/.github/actions/validate/action.yml @@ -12,7 +12,6 @@ runs: DEVICES.md easystart/README.md easystart/python/README.md - repo-config/README.md incremental_files_only: false - name: Setup uv step diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5fa3acd..2aa42ea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -48,6 +48,8 @@ diff coverage. A refusal, partial or absent coverage statement, unrecognized out unresolved thread, or body-only finding blocks the review loop. Re-run the loop after every fix push. Never infer review completion from `mergeStateStatus: CLEAN`. +Review effort is user-controlled. The automation observes `Lite`, `Balanced`, or `Max`, including an inherited `Default ()`, and never selects or changes the setting. Effort does not determine coverage or completion. A request can complete without a `copilot_work_started` event, so absence of that event is not a stalled-review verdict. When `wait` returns `PENDING` with `requested=yes`, report the state and rerun `wait` for another bounded interval by default. Do not clear the request automatically because it may be active. If the maintainer directs a retry, remove Copilot in the pull request UI, add it again, and rerun `wait`. This recovery replaces only the review request and never changes the effort setting. + ### Disproved Claims **A disproof is proof about this repository, and the thread it was written in is not where the next round looks.** [GOVERNANCE.md "PR Review Etiquette"](../GOVERNANCE.md#pr-review-etiquette), which routes to the `pr-review-conduct` Skill, closes a false finding by disproving it in the thread, addressed to the reviewer so it does not raise the same thing again, and while the pull request is open that is the right place for it. Afterwards it is the wrong one. The pull request merges, the next round begins with no memory of the last, and the second occurrence reaches a maintainer with no way to tell it from a first. Each entry below is a claim that was tested against this repository and found false, kept so the proof is read rather than built twice. diff --git a/.github/skills/agent-conduct/SKILL.md b/.github/skills/agent-conduct/SKILL.md index 7d189ca..060267c 100644 --- a/.github/skills/agent-conduct/SKILL.md +++ b/.github/skills/agent-conduct/SKILL.md @@ -19,9 +19,12 @@ Read `GOVERNANCE.md` "Verification Discipline" before reporting success on anyth - **Run the repo's whole lint gate before every push**, not the parts that look relevant, because the tool most likely to catch a change is often the one it seems least about. - **A launched process is not a result.** Report the output the wait produced, and where it produced none, that absence is the report. Never name an external cause the record does not carry. - **A local clone is not the branch it names.** Fetch immediately before reading, or read the live ref, and name the ref and commit in any finding a local read produced. +- **A checkout this session did not create is not ground truth.** One found already sitting on disk may belong to another concurrent session, sit on a stale fetch or an unexpected branch, or hold unreviewed uncommitted edits. Clone fresh or read the live API instead of trusting `git status`/`git remote -v` run against a pre-existing checkout. - **A "does not exist" claim names the branch it was checked against.** A worktree's default branch is not necessarily the one the content lives on: in-flight content on a `release`-model repo lands on `develop` before `main`, per `GOVERNANCE.md` "Branching Model," so check that branch before reporting anything absent repo-wide. +- **A `raw.githubusercontent.com` 404 does not distinguish a private repository from a missing file.** Where visibility is not confirmed public, read content via `gh api "repos///contents/?ref="`, capturing the result before decoding it (`content=$(gh api ... --jq '.content') && printf '%s' "$content" | base64 -d`) rather than piping straight into `base64 -d`, whose own exit status is all a direct pipe reports, letting a failed fetch decode as an empty success. Never `2>&1` either form, which corrupts the decode with the error text instead of the payload. Verify the ref resolves before reading either failure as proof the content itself does not exist. - **A test asserts the mechanism it names, and a gate has to be watched failing.** A case that passes for an incidental reason is worse than no case, because it is later cited as evidence. - **Platform-specific code is verified only on the platform it runs on.** Reasoning about PowerShell, macOS, or WSL-specific behavior from a different host is not verification, however closely it matches an already-tested equivalent elsewhere. State an untested structural match as exactly that, never in the words used for a tested fact, and when no agent in the loop has access to the target platform, say so and defer or ship it labeled unverified. +- **PR-bound work runs `local-strict-review` before the claim.** Claiming a unit of work done, verified, green, or fixed for work that will become, or already is, a pull request means running `local-strict-review` against the branch's diff first, before a PR-hosted reviewer finds the same gap. Claims about a pull request being reviewed, clean, or mergeable are owned by the `pr-review-conduct` skill, and claims that a commit landed by `git-commit-conventions`. @@ -35,7 +38,7 @@ Claims about a pull request being reviewed, clean, or mergeable are owned by the ## When a Failure Surfaces a Lesson - **Durable knowledge lands in the committed docs, not in agent memory**, as part of the change that surfaced it, per `GOVERNANCE.md` "Durable Knowledge and Self-Improvement". Memory does not survive a new session or machine, so it holds only environment nuance and in-flight state. -- **Where the governing doc is carried from the hub, propose the fix upstream** rather than only patching it locally, since a local fix leaves every sibling repo with the same trap. +- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`** rather than only patching it locally. A local fix leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. - **A review flags an instance, so fix the class**: sweep for the siblings before replying, because reviewers sample rather than enumerate. - **A rule that keeps needing restating** is usually a stale or missing skills install, so run `python3 scripts/skills_install.py --report` from a hub checkout (the `fleet-conformance-check` skill) before concluding the rule does not exist. diff --git a/.github/skills/audit-a-repo/SKILL.md b/.github/skills/audit-a-repo/SKILL.md index 01ee0b2..5cd0c13 100644 --- a/.github/skills/audit-a-repo/SKILL.md +++ b/.github/skills/audit-a-repo/SKILL.md @@ -22,7 +22,7 @@ The audit is the fleet's measurement procedure, and the two failure shapes it gu - **Know what the runner does and does not prove.** `spec/audit.py` mechanizes the deterministic subset only: settings, rulesets, secret names, file and section presence, verbatim hashing, interface wiring, Dependabot coverage, branch facts. It evaluates no check under a type in `spec/project-types.json`, so every per-type check is judged by hand, and a clean run is no evidence for them (`AUDIT.md` section 4). Silence from a tool that was never looking reads exactly like a pass. - **Judge letter and intent per check** and keep the vocabulary: letter miss with intent satisfied is a drift finding, both missing is a defect, and operational is binary over the applicable set (`AUDIT.md` sections 4 and 7). Do not invent a parallel scheme. - **Assert the Actions implement `WORKFLOW.md`** by outcome, not by matching catalog snippets byte for byte: the 5A static audit with a `file:line` citation per applicable guarantee, then the 5B trace scenarios (`AUDIT.md` section 5). The `workflow-ci-contract` skill summarizes that contract. -- **Diff live settings, rulesets, and secrets** against `repo-config/` with the normalized comparisons `AUDIT.md` section 6 gives, using its commands rather than hand-rolled ones, since the normalization details (rule sorting, `bypass_actors` deliberately excluded, the operational develop payload) are where hand-rolled diffs go wrong. +- **Check live settings, rulesets, and secrets from a hub checkout at `main`** with `AUDIT.md` section 6. Run `repo-config/configure.sh check` with the target repository and model for settings and rulesets, and `spec/audit.py [RepoName]` for secrets, rather than constructing a local comparison. The hub payloads are the only repository-configuration source. ## Reporting diff --git a/.github/skills/comment-and-doc-style/SKILL.md b/.github/skills/comment-and-doc-style/SKILL.md index dc834f2..af574e9 100644 --- a/.github/skills/comment-and-doc-style/SKILL.md +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -55,6 +55,26 @@ Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.N for what a machine reads: a tool or package name (`markdownlint`), a settings key, a heading anchor, a file extension. +## Docker lint authorization + +A restricted executor treats Docker socket access, image fetching, and repository exposure as +separate permissions. Repository exposure needs explicit maintainer approval even when the mount +is read-only. Use the hub's `scripts/docker_lint.py` wrapper for the standard lint shape. It +discovers targets, pulls images in a separate phase, resolves each digest, and announces the +boundary before repository mounts begin. Each Docker command has a timeout and visible result. +Lint containers disable networking and mount the checkout read-only. Persist approval only when +the executor constrains that whole shape. Never allow an unconstrained `docker run` prefix. +PSScriptAnalyzer downloads its pinned module in a separate container that has network access and +no repository mount. `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)" +owns the exact invocation and full authorization model. + +Agent-specific authorization stays in provider-labeled bullets so one agent's configuration does +not read as a shared requirement: + +- **Codex:** rules cannot safely cover changing worktree paths and digests. Smart Approvals can + prompt per task. No-prompt operation is supported only inside an external sandbox because it + removes command-wide protection. + ## Markdown formatting - **Reference-style links everywhere**, except the four files read one section at a time rather @@ -194,7 +214,7 @@ file type, operational-repo overrides, extensionless-script pins, and auditing, ## Carried files reference no coordination machinery `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md` never reference the template repo +the `spec/` files and the carried `AUDIT.md` never reference the template repo (in prose or a link), and never name a sibling fleet repo as an illustrative example. State the behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies the destination out of band. A contextually relevant link to a related project (the image this @@ -204,8 +224,8 @@ hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. ## PR titles and commit messages -- **Format**: an imperative subject, 72 characters or fewer, no trailing period ("Add 24-hour - PM2.5 average sensor", not "Added X" or "Adds X"). An optional body, blank-line separated, +- **Format**: an imperative subject, 72 characters or fewer, no trailing period ("Add 24-Hour + PM2.5 Average Sensor", not "Added X" or "Adds X"). An optional body, blank-line separated, explains *why* the change is being made when that is non-obvious, the diff already shows *what*. - **Rules**: no vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` titles are fine as-is. No `Co-Authored-By:` lines unless the developer explicitly asks. No @@ -217,11 +237,11 @@ hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. *EPA-Corrected*, *24-Hour*). ```text -Add structured logging extensions to library -Pin softprops/action-gh-release to commit SHA -Drop net8.0 multi-targeting from console project +Add Structured Logging Extensions to Library +Pin softprops/action-gh-release to Commit SHA +Drop net8.0 Multi-Targeting from Console Project Bump xunit.v3 from 3.2.2 to 3.3.0 -Clarify devcontainer setup steps in README +Clarify devcontainer Setup Steps in README ``` ## Quantitative claims diff --git a/.github/skills/comment-and-doc-style/references/carried-doc-references.md b/.github/skills/comment-and-doc-style/references/carried-doc-references.md index 972c63f..125bf0f 100644 --- a/.github/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.github/skills/comment-and-doc-style/references/carried-doc-references.md @@ -7,7 +7,7 @@ doc. ## Which files this governs `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md`, the files the fleet carries +the `spec/` files and the carried `AUDIT.md`, the files the fleet carries verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template content only. A repo's own `README.md` and topical docs are its own content, never carried verbatim, and this rule does not reach them. diff --git a/.github/skills/comment-and-doc-style/references/line-endings.md b/.github/skills/comment-and-doc-style/references/line-endings.md index bdf0846..8337cbc 100644 --- a/.github/skills/comment-and-doc-style/references/line-endings.md +++ b/.github/skills/comment-and-doc-style/references/line-endings.md @@ -71,9 +71,10 @@ tool-owned format outside `.bat`/`.cmd`, or a byte-preserve data directory whose consumer may depend on), still pair a `.gitattributes` pin with a matching `.editorconfig` override, since the git pin alone is not enough there, `.gitattributes` governs git while the editor follows `.editorconfig`. For a byte-preserve directory, disable all editor normalization, -not just EOL: `[/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = +not just EOL: `[/**]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = false`, `trim_trailing_whitespace = false` (`unset` is EditorConfig's spec-defined special value -that removes an inherited property). +that removes an inherited property, and `**` is needed rather than `*` so a nested file under the +directory is covered too, since `*` excludes `/` and only matches one path component). ## Editing discipline diff --git a/.github/skills/copilot-instructions-keeper/SKILL.md b/.github/skills/copilot-instructions-keeper/SKILL.md index 88f3c38..9ee43bc 100644 --- a/.github/skills/copilot-instructions-keeper/SKILL.md +++ b/.github/skills/copilot-instructions-keeper/SKILL.md @@ -23,13 +23,13 @@ description: >- intact in every repo that carries it. This skill maintains that carried copy, it does not replace the bootstrap. -`spec/files.json` declares it `intent` fidelity, `whole: true`, covering three named sections +`spec/files.json` declares it `intent` fidelity, `whole: true`, covering four named sections (`Commit Messages and Pull Request Titles`, `Reviewing Carried Fleet Content`, `GitHub Copilot -Review Runbook`), with ``, ``, and `` placeholders filled per repo. **The fleet -audit checks an `intent` file for file presence and each named section's heading, never for -content drift inside a section.** A section that is present but has fallen out of date against -the hub, the exact gap this skill exists to catch, produces no finding anywhere in the mechanical -audit. Noticing that has to happen in a live session like this one. +Review Runbook`, `When in Doubt`), with ``, ``, and `` placeholders filled per +repo. **The fleet audit checks an `intent` file for file presence and each named section's +heading, never for content drift inside a section.** A section that is present but has fallen out +of date against the hub, the exact gap this skill exists to catch, produces no finding anywhere in +the mechanical audit. Noticing that has to happen in a live session like this one. ## The one thing this file has that others don't: repo-local ledger entries @@ -51,7 +51,7 @@ repo's copy is wrong in both directions: 1. Read the current "Disproved Claims" section in that repo's copy, if it has one, and preserve every entry that names a file or behavior that repo actually carries. -2. Update everything else, the runbook mechanics, the three named sections, the rule text, to +2. Update everything else, the runbook mechanics, the four named sections, the rule text, to match the hub canonical. 3. Never carry the hub's own repo-specific "Disproved Claims" entries downstream. They name `ProjectTemplate`'s own files and revisions, not the target repo's. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index ee8d9a9..f3565a5 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,9 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - Git hooks are optional, and a repo may wire a local runner (Husky.Net, with `dotnet husky run` - as a style step) for pre-commit enforcement, but CI is the gate that matters. + A working local hook is strongly suggested, not optional: wire Husky.Net from the canonical + `catalog/snippets/husky/` config. See GOVERNANCE.md "Running the Linters Locally" for what the + hook must cover and what its absence means. **A new port is not a license to silence diagnostics.** Brownfield or just-ported status never justifies relaxing analyzer severities or muting newly surfaced warnings. Fix them. (The only @@ -94,8 +95,8 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. Local pre-commit hooks are optional, wire Husky.Net (or - another runner) if you want local enforcement. +- CI is the authoritative lint backstop. A local pre-commit hook is strongly suggested: wire + Husky.Net from `catalog/snippets/husky/` for local enforcement, including the shared doc gates. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.github/skills/dotnet-codestyle/references/conventions.md b/.github/skills/dotnet-codestyle/references/conventions.md index 5eb4854..46897c8 100644 --- a/.github/skills/dotnet-codestyle/references/conventions.md +++ b/.github/skills/dotnet-codestyle/references/conventions.md @@ -122,5 +122,15 @@ parameters, return values, exceptions, and crefs. /// /// Thrown when is not a supported value. /// -public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} +public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) +{ + if (category is not ("motivational" or "humor")) + { + throw new ArgumentException($"Unsupported category: {category}", nameof(category)); + } + + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1, cancellationToken); + return $"Quote for {category}"; +} ``` diff --git a/.github/skills/dotnet-codestyle/references/project-config.md b/.github/skills/dotnet-codestyle/references/project-config.md index 8f6e838..42b8fd1 100644 --- a/.github/skills/dotnet-codestyle/references/project-config.md +++ b/.github/skills/dotnet-codestyle/references/project-config.md @@ -15,3 +15,7 @@ ``` + +5. **Nullable and XML documentation**: `enable`, + `true` (see `references/conventions.md` + for the XML documentation format every public surface needs). diff --git a/.github/skills/drive-pr/SKILL.md b/.github/skills/drive-pr/SKILL.md new file mode 100644 index 0000000..1e3fb1f --- /dev/null +++ b/.github/skills/drive-pr/SKILL.md @@ -0,0 +1,131 @@ +--- +name: drive-pr +description: >- + Drives a ptr727/ProjectTemplate fleet pull request through its review loop, feature branch into + develop and, when asked, on to a mergeable develop -> main promotion PR, applying the + pr-review-conduct disposition to every reviewer finding along the way: fix it, decline it with + evidence, defer it behind a filed issue, or put the call to the maintainer and wait for an + explicit answer in the same turn. Use this whenever asked to drive, land, take, chase, or push + a PR toward develop or main, or to run the review loop hands off instead of narrating each + round. When the request does not say how far ("drive this PR", "land it"), ask once whether the + target is develop or a mergeable main promotion PR, rather than guessing. Triggers even when + only one PR is named, because a finding raised against the develop -> main promotion PR + routinely needs its own feature -> develop fix cycle before the promotion PR can go green, and + stopping at the first promotion-PR finding is the early exit this skill exists to prevent. Ends + at develop merged, or at a promotion PR meeting the pr-review-conduct Merge Gate, never merges + main itself, that is the separate merge-and-release skill, its own go-ahead. +--- + +# Drive PR + +## Why This Exists + +The same request repeats every time a change is ready: drive it through review, resolve whatever +a reviewer raises, and keep going until develop, or main, actually has it. Re-explaining the +finding-disposition policy and the promotion-PR wrinkle each time is the cost this skill removes. +The wrinkle: a finding raised against the develop -> main promotion PR usually cannot be fixed on +that PR directly, its diff is develop's diff against main, so the fix lands as its own +feature -> develop PR first. Stopping at the first such finding, or forgetting to loop back to the +promotion PR once the fix lands, is the early exit this skill exists to prevent. + +## How Far to Drive + +- Read the invocation for an explicit target first. "To develop" or "to dev" means stop once + merged into develop. "To main", "through to main", or "all the way" means continue to a + mergeable promotion PR. Act on either without asking. +- When the request names no target ("drive this PR", "land it", "take this PR"), ask once, + before the first push: develop only, or all the way to a mergeable main promotion PR. Recommend + "all the way to main" as the default, a promotion PR left to go stale once develop is ready is + the more common regret than driving one step too far. +- A repo on the operational workflow model (registry `workflowModel: operational`) has no + standing promotion PR expectation, confirm whether a promotion PR is even wanted before opening + one, per operational-vs-release-workflow's "Operational repositories" delta. + +## What Invoking This Skill Authorizes + +- Naming this skill, and answering its how-far question, is the maintainer's explicit, current + go-ahead for every feature -> develop squash merge the drive performs to reach that target. +- It is never authorization to merge the develop -> main promotion PR, or to dispatch a release. + Those stay in merge-and-release, invoked on its own so the maintainer keeps a checkpoint before + the harder-to-reverse step. +- The pr-review-conduct Merge Gate still gates every merge this skill performs on its own. The + go-ahead removes the "may I merge to develop" question, not the gate itself, a feature PR with + an open finding does not merge regardless of target. + +## The Drive Loop + +1. Isolate into a worktree per repo-worktree, based on develop, before the first edit. +2. Run `local-strict-review` against the branch's current diff, then push the branch and open + the feature -> develop PR if it does not exist yet. +3. Drive pr-review-conduct's review loop on it to the Merge Gate, disposing of every finding per + "Disposing of Every Finding" below. +4. Capture the branch's own tip before merging, `gh pr view [number] --json headRefOid --jq + .headRefOid`, needed for the verify-then-delete step below since `gh pr merge` itself reports + the resulting squash commit on `develop`, not the PR's `headRefOid`. Merge the feature PR into + develop, `gh pr merge [number] --squash --repo owner/repo`. Never `--delete-branch` on this + call, it is run from inside the task's own worktree per step 1, where the feature branch is + checked out, and `gh pr merge --delete-branch` needs to switch that worktree to the base branch + to delete it, which fails when `develop` is already checked out somewhere else, the ordinary + case in this layout. Instead run repo-worktree's post-merge cleanup from the base clone: remove + the worktree, delete the now-merged local task branch, then verify before deleting the remote + one, `git ls-remote --heads --exit-code -- origin "refs/heads/"` matches the + `headRefOid` captured above, `--` before `origin` and the fully-qualified ref. `--heads origin + ""` alone still tail-matches a differently-prefixed branch sharing the same suffix, and + `--` placed after `origin` instead of before it is not equivalent either, verified empirically + against a `refs/heads/other/--` ref: after-origin also matched it, before-origin matched only + the one intended. `--exit-code` distinguishes exit `2`, branch genuinely gone, from any other + non-zero exit, a failed query, an unreachable remote and a gone branch both print nothing to + stdout otherwise. Stop and report either a mismatch or a failed query rather than deleting, + someone could have pushed to the branch after the merge, or the name could have been reused. + `` is the real value, substituted as its own quoted argument (a shell variable + expansion such as `"$branch"`, or an argv element), never handed to `eval` or `sh -c` for a + second round of shell parsing, the only way an embedded `$()` or backtick would actually run. + A valid ref can start with `-` or carry a shell metacharacter, which is why it stays quoted + regardless. Only once it matches, `git push origin --delete -- ""`. Never + `--force-with-lease` here, git-commit-conventions forbids it + unconditionally, this plain verify-then-delete is the safety gate, not a compare-and-swap at + delete time. The + repo's auto-delete-head-branches setting is kept off fleet-wide (to protect `develop` and + `main` from it, GitHub has no per-branch exception), so nothing deletes an ordinary feature + branch automatically. Stop here and report the merged PR when the target is develop only. +5. Open the develop -> main promotion PR if it does not exist yet, or find the existing one. +6. Drive its review loop the same way. A finding that needs a code change never gets pushed to + the promotion PR directly, its head is develop, so land the fix as a fresh pass through steps + 1 to 4 in its own worktree and branch, then return here. +7. The fix landing on develop updates the promotion PR's diff and head SHA on its own, re-request + a review on the new head and continue the loop. +8. Repeat 6 and 7 until the promotion PR itself carries no open finding and its checks are green + on the current head. +9. Report the promotion PR number and its ready state. Do not merge it. + +## Disposing of Every Finding + +pr-review-conduct's five outcomes are the actual rule, this is the mapping to use while driving: + +- Real, so fix it. Run `local-strict-review` against the branch's current diff, push it, reply + with its commit SHA (outcome 1). +- Not real, or real but out of scope here, so decline in the thread with evidence: the command + and its output, the code path, or the rule that governs it. An assertion never closes a finding + on its own (outcome 2). +- Real and worth doing, but later, so file the issue first, then reply with its link (outcome 4). +- Real, fixable, but a value call rather than a scope boundary, or the agent genuinely does not + know which of the above applies, so ask the maintainer directly, whatever the runtime's own + interactive-question mechanism is, and get an explicit answer in the same turn, a plan to ask + later is resolution by silence (outcome 3). +- The same finding keeps recurring against correct code, fix the class, sharpen a name, add a + comment, or take the rule itself to the maintainer, rather than re-arguing the instance every + round (outcome 5). + +## Mechanics Live Elsewhere + +- Review loop mechanics, the Merge Gate, and `scripts/pr_review.py`: pr-review-conduct. +- Branch rules, never delete develop, the EOL-only conflict, issue-closing keywords belonging on + the promotion PR: operational-vs-release-workflow. +- Worktree isolation and post-merge cleanup: repo-worktree. + +## Stop and Ask, Beyond the How-Far Question + +- A genuine design trade-off, a recurring finding pattern, or an architectural redesign proposal + each escalate per pr-review-conduct's own list, restated there, not duplicated here. +- An unrecognized review shape blocks the gate on its own, file an issue naming it and ask, never + guess what new wording probably meant. diff --git a/.github/skills/git-commit-conventions/SKILL.md b/.github/skills/git-commit-conventions/SKILL.md index 85e46eb..5c1454f 100644 --- a/.github/skills/git-commit-conventions/SKILL.md +++ b/.github/skills/git-commit-conventions/SKILL.md @@ -164,6 +164,4 @@ any rewrite. See `references/history-rewrite.md` for the full two-gate rule. ## Never run destructive git commands without being asked -`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and anything else that -discards uncommitted work runs only on explicit developer instruction, never as a convenience step -inside a larger task. +`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`, and other commands that discard work require explicit developer instruction. Never use them as a convenience inside a larger task. One narrow cleanup exception applies to `git branch -D ` after a squash merge. It requires live proof that the pull request for that exact branch merged and a clean worktree at the verified head SHA, per `repo-worktree`. The exception never applies to `develop`, an unmerged branch, an unresolved pull request, or a branch with uncommitted work. diff --git a/.github/skills/local-strict-review/SKILL.md b/.github/skills/local-strict-review/SKILL.md new file mode 100644 index 0000000..cad9a06 --- /dev/null +++ b/.github/skills/local-strict-review/SKILL.md @@ -0,0 +1,71 @@ +--- +name: local-strict-review +description: >- + Runs one read-only, adversarial review pass against this branch's current diff against its + target branch, full file context included, on the strongest model tier the session can reach, + before a unit of work is pushed toward a pull request or claimed done. Use this whenever staged, + committed, or untracked work is about to be pushed on a PR-bound branch, and whenever + `agent-conduct`'s "about to claim work is done, verified, green, or fixed" trigger fires for + PR-bound work. Triggers even when the change looks small or the same session already judged its + own diff ready, because a self-review pass judging its own diff inherits its own blind spots, + the exact gap this skill exists to close before a PR-hosted reviewer closes it instead. Reuses + `code-review`'s "Review the Change" criteria rather than restating them, and owns only this + local, pre-PR moment. Once a pull request exists, `pr-review-conduct` and `drive-pr` own + triaging and disposing of what a PR-hosted reviewer finds. +--- + +# Local Strict Review + +## Why This Exists + +A coding agent that finishes a unit of work, judges it ready, and opens the pull request is judging its own diff with the model, and often the blind spots, that wrote it. CodeRabbit, Qodo, and Copilot routinely find real defects that a local pass missed, and each round costs review latency and, for a rate-limited reviewer, shared account-wide quota. A local, full-file-context adversarial pass before the pull request exists catches the same class of defect for a fixed, smaller cost, the same reasoning that already runs local lint before a push instead of waiting for CI. + +## What It Does + +Dispatches one read-only subagent against this branch's full diff since it forked from its target branch. Resolve `` once, `develop` unless `repo-worktree`'s base-branch rule put this branch on `main` instead, then fetch it, `git fetch origin `, and diff against the merge-base, `git diff "$(git merge-base origin/ HEAD)"`. Stop and report a failed fetch rather than running the merge-base or diff commands anyway: an existing local `origin/` ref can still resolve after a failed fetch, and reviewing against it silently trades the current target for a stale one. Use the same resolved `` in every command below, never a literal `develop` alongside it. Naming the target branch explicitly matters: the branch's own `@{u}` tracking ref points at the branch's own remote once it has been pushed, not at the branch it targets, so anchoring there silently narrows a later run to only the diff since the last push instead of the full accumulated diff. That merge-base diff covers every commit already on the branch plus whatever is currently staged or unstaged, so it is never empty and never reviews only the latest increment, at any of the moments this skill is invoked from. A fresh review of the full accumulated diff is what catches what per-push review misses, the exact evidence this skill exists to act on. + +`git diff` never reports a path `git add` has not touched, so a newly created file sitting untracked would otherwise go unread. List it explicitly, `git ls-files --others --exclude-standard`, and read each result in full alongside the diff, the same as any other file the diff touches. + +The subagent reads the full content of every file the diff and the untracked-file list touch, not just the hunks, since cross-file and whole-file context is exactly what incremental review misses. It reports findings only. It never fixes, stages, or commits anything. + +Review criteria are `code-review`'s "Review the Change" section, reused rather than restated here, plus three traps worth calling out explicitly for a pass that runs before a human or a PR-hosted reviewer ever sees the diff: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. `code-review`'s separate "Publish Every Finding" section does not apply here: this skill has no PR to post a comment on and no coverage marker to close a review with, so its own report contract below replaces that section rather than extending it. + +## Running It + +Follow `AGENTS.md` "Context and Delegation Discipline"'s subagent briefing shape: + +```text +Task: adversarial review of this branch's diff against its merge-base with its target branch, + read full surrounding files where the diff hunks alone do not give enough context. +Paths: the files `git diff --name-only "$(git merge-base origin/ HEAD)"` and + `git ls-files --others --exclude-standard` list, mandatory floor. Reading a specific + unchanged caller or consumer beyond that list is in bounds only where a candidate finding's + proof actually depends on it, per code-review's own "follow data and control flow beyond the + edited lines" instruction below, never as an open-ended exploration. +Rules that bind this task: quote `code-review`'s "Review the Change" section into the prompt, + plus flag unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior + differences explicitly. Do not quote "Publish Every Finding", this task's report contract is + the Return line below, not a PR comment or a coverage marker. +Return: one finding per line, file:line, the concrete failure scenario, no severity theater. +Bounds: read-only. No edit, no stage, no commit, no push, no PR-hosted write of any kind. + +``` + +**Model tier:** the strongest tier this session can reach, per `AGENTS.md` "Match the model tier to the judgment" and "Never tier down the seat holding the judgment", applied here to the reviewer rather than the author. Run the pass on the same tier that authored the change when only one tier is reachable, a second, adversarially-prompted look still catches what the authoring pass's own "looks ready" judgment did not. + +## Disposing of Findings + +Every finding maps to one of `pr-review-conduct`'s five outcomes before the pull request opens: fixed, evidence-disproven, filed as a deferred issue, escalated to the maintainer for an explicit call, or, if it keeps recurring, taken as a signal to fix the class. A finding this pass raised and not fixed is never the agent's own call to just leave. Per outcome 3, that decision needs the maintainer's explicit answer, the same way a PR-hosted finding would. Running this pass is expected before every push toward a pull request, per `agent-conduct`. Its findings stay advisory: a finding it raises does not by itself block `git commit` or `gh pr create`, the disposition above is what closes it, the same posture local lint holds today. It posts nothing to GitHub, it only reports to the session driving the work. A finding raised here and not fixed is not thereby resolved: the same finding shape reaching a PR-hosted reviewer later still gets its own fresh disposition, per `pr-review-conduct`'s "a disposition decided on one PR does not carry to the next." + +## When to Run It + +- Before the first push toward a pull request (`drive-pr`'s Drive Loop step 2, `pr-review-conduct`'s Expected review loop step 1). +- Before pushing a fix for a reviewer finding, the same self-review blind spot applies to a fix as to the original diff (`drive-pr`'s "Disposing of Every Finding", `pr-review-conduct`'s outcome 1). +- Whenever `agent-conduct`'s "about to claim work is done, verified, green, or fixed" trigger fires for work that will become, or already is, a pull request. + +## Mechanics Live Elsewhere + +- Review criteria: `code-review`. +- Delegation shape and model-tier discipline: `AGENTS.md` "Context and Delegation Discipline". +- Branch base rule (`develop` unless the task is explicitly `main`-only): `repo-worktree`. +- Finding disposition once a pull request exists, the Merge Gate, `scripts/pr_review.py`: `pr-review-conduct`, `drive-pr`. diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md new file mode 100644 index 0000000..3eb31c3 --- /dev/null +++ b/.github/skills/merge-and-release/SKILL.md @@ -0,0 +1,225 @@ +--- +name: merge-and-release +description: >- + Merges a ready develop -> main promotion PR for any ptr727/ProjectTemplate fleet repo and, when + asked, dispatches the release, in this hub always refreshing this machine's installed Skills + from the newly promoted content as part of that release step, never as a separate ask. Use this + whenever asked to merge main, ship a release, cut a release, or finish a promotion once its PR + is already green and fully resolved (produced by drive-pr or by hand). When the request does + not say how far ("merge main", "ship it"), ask once whether to merge only or merge and release, + rather than guessing which the maintainer wants this time. Triggers even when the phrasing is + as short as "merge main and release", because that already states the scope and is itself the + explicit, current go-ahead this skill acts on without asking again, though it never substitutes + for the pr-review-conduct Merge Gate, a promotion PR that is not actually green and fully + resolved gets reported and stopped on, not merged. +--- + +# Merge and Release + +## Why This Exists + +Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same two steps follow +every time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can +also change `.agents/skills` content this very session depends on, so the release step always +carries a Skills refresh with it there, never a separate branch to ask about, an ambiguous "merge +and release" on the hub must not leave the maintainer unsure whether Skills got refreshed. One +skill covers all of it, scoped down by what the maintainer actually asks for. + +## How Far to Go + +- Read the invocation for an explicit scope first. "Just merge" or "merge only" means stop after + the merge. "Merge and release", "ship it", or "cut a release" means also dispatch, and in this + hub also refresh Skills as part of that same step. Act on either without asking. +- When the request names no scope ("merge main"), ask once, before merging: merge only, or merge + and release. Recommend "merge and release" as the default on a release-model repo, a promotion + merged without its release is the more common regret there. Recommend "merge only" as the + default on an operational repo (registry `workflowModel: operational`), where a release is a + separate, deliberate dispatch rather than an automatic follow-on to a promotion, per + operational-vs-release-workflow's "Operational repositories" delta. +- Detect the hub automatically, `git remote get-url origin` or `gh repo view --json + nameWithOwner` naming `ptr727/ProjectTemplate`. There the release scope silently includes the + Skills refresh, a downstream repo never sees it, it has no `.agents/skills` of its own to + refresh. + +## What Invoking This Skill Authorizes + +- Naming this skill, and answering its how-far question, is the maintainer's explicit, current + go-ahead to merge the promotion PR and to perform the scope chosen, for the one repo and PR in + front of the agent. It is never a standing mode carried to the next PR. +- It is never permission to merge a PR that fails the Merge Gate. Re-verify the gate at + invocation time, a check from earlier in the session can be stale. + +## The Procedure + +1. Identify the open develop -> main promotion PR for this repo, stop and report if none is open. +2. From a hub checkout, `scripts/` is not carried into downstream repos, run `scripts/pr_review.py + status [number] --repo owner/repo` on it and confirm the pr-review-conduct Merge Gate. Stop + and report exactly what is missing rather than merging on a partial gate. +3. `gh pr merge [number] --merge --repo owner/repo`. Never `--delete-branch`, the promotion PR's + head is `develop`. +4. Confirm the merge landed, `mergedAt` set, `main`'s tip matching the merge commit. +5. When the chosen scope includes a release, first bring the hub checkout used for this procedure + current, `git fetch origin main`, and read this repo's `releaseTrigger` from that fetched tip + rather than a possibly-stale working tree copy, relevant when the target repo is the hub itself + and this exact promotion changed its own registry entry. Select the one matching entry + explicitly, falling back to the registry's own default when that entry sets no + `releaseTrigger` of its own, and stop and report rather than guessing when selection is not + exactly one match, on a non-1 count exit non-zero rather than returning empty with success, an + ambiguous or missing match must fail loud, not read as an empty value still safe to act on: `git + show origin/main:registry/repos.json | jq -r --arg name '' '(.repos | map(select(.name + == $name))) as $m | if ($m | length) == 1 then ($m[0].releaseTrigger // .defaults.releaseTrigger) + else error("expected exactly one registry entry for \($name), got \($m | length)") end'`. Two + cases, `none` versus anything else. When it + reads `none`, report that no + release is configured, dispatch and run-correlation (step 6) do not apply. Otherwise (`two-phase`, + `dispatch-only`, or `publish-on-merge` alike), dispatch explicitly, `gh workflow run + publish-release.yml --ref main --repo owner/repo`, or `--ref develop` only when the maintainer + explicitly asked for a prerelease dispatch instead. `publish-on-merge`'s automatic publish is + gated on the actor being the codegen App merging a Dependabot or codegen PR + (operational-vs-release-workflow's publishing rules), so an ordinary human promotion merge, + exactly what step 3 just did, never triggers it, this step's explicit dispatch is what actually + ships the release here, not a side effect of the merge. +6. Correlate the specific run this dispatch produced rather than assuming the newest one is it. + `gh run list --repo owner/repo --workflow publish-release.yml --branch main --event + workflow_dispatch --json databaseId,createdAt,headSha` (or `--branch develop` for a prerelease + dispatch), matched by `headSha` against the dispatched ref's tip (`main`'s tip confirmed in + step 4, or `develop`'s current tip for a prerelease) and by `createdAt` against the dispatch + time. `gh run list` can momentarily omit a just-created run, so a single query reporting zero + candidates is not yet "never started". Poll the list itself, within a bounded interval, until + exactly one candidate matches. A concurrent run of a different event on the same branch must + never be mistaken for this one, more than one candidate is as inconclusive as zero. A run whose + `headSha` does not match the expected tip at all, rather than simply being absent, means the + dispatched ref moved between step 4's confirmation and the dispatch itself, report that + distinctly, the ref changed mid-dispatch, rather than folding it into an ordinary absent-run + timeout. Report and stop rather than guessing once the interval elapses with zero or more than + one candidate still matching. Only once exactly one candidate is confirmed, poll that one run + id to completion in one further bounded background wait with an explicit, finite timeout, + 2700 seconds (45 minutes, matching `scripts/pr_review.py`'s own default) unless the maintainer + states a different bound for this specific release: `timeout 2700 gh run watch --repo + owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent bounded-wait + mechanism enforcing the same bound on a host without it (macOS without coreutils, native + Windows). Report a timeout separately from a completed run's own conclusion, the tag or + version it produced. A run that fails, times out, or never starts is reported, never silently + retried. +7. In the hub, when the chosen scope includes a release, bring this checkout to the merged + content without discarding or mixing in anything local. First assert `git status --porcelain + --untracked-files=all --ignored -- .agents/skills/ .claude-plugin/` is empty, and stop and + report rather than proceeding over any uncommitted content there, tracked, untracked, or + gitignored, since `skills_install.py` reads both paths: `shutil.copytree()` installs each + `.agents/skills/` skill directory for Codex/opencode, and `claude plugin marketplace add` + installs from `.claude-plugin/` for Claude Code, so a gitignored stray file under either rides + along the same as any other, and the plain porcelain form (silent on ignored paths) would pass + this preflight while one still rides into an install. Scoped to those two paths rather than + the whole tree, matching `skills_install.py`'s own `source_ref()` dirty check (`watched = + [SKILLS_SRC, CLAUDE_PLUGIN_DIR]`), since an ignored file elsewhere in the checkout (a build + cache, a lockfile) is not this preflight's concern and should not block the refresh on it. + Then `git fetch origin main`, `git checkout main` + (or `git checkout -b main origin/main` the first time this checkout carries no local `main` at + all, `checkout` rather than `switch` since the fleet's own `git` floor is undeclared and + `checkout` needs no minimum version for this), and `git merge --ff-only origin/main`. + `checkout` still refuses a `main` checked out in another worktree, and `--ff-only` refuses + anything but a clean fast-forward, so either stops and reports on top of what the preflight + already ruled out, per Repository Boundaries and Write Safety. `--ff-only` does not fail when + local `main` is already ahead of `origin/main`, since a strict superset needs no fast-forward + and reports up to date, so assert `git rev-parse main` equals `git rev-parse origin/main` + afterward and stop and report on a mismatch, a local-only commit this checkout never pushed is + exactly the case a bare "up to date" would hide. `skills_install.py` stamps and installs from + whatever this checkout's HEAD already is, so running it against a stale, unrefreshed, or + locally-diverged `main` skips the refresh silently. Only then run `python3 scripts/skills_install.py --report`, then + `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, + regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated + only on the chosen scope, never on the release outcome. This refreshes only the machine running + this session, per skill-lifecycle, every other machine still refreshes on its own next run or + `docs/host-setup.md` "Fleet Skills Install" cadence. +8. Run cleanup regardless of how steps 5 through 7 ended, no release configured, a dispatch + failure, an ambiguous run match, a timeout, a failed run, or a hub Skills refresh all still + reach this step, the merge in step 3 already landed by then. Two parts, both required, neither + optional: + - The promotion PR's own worktree: fetch and prune, remove the worktree, then fast-forward the + base clone to `develop`. Removing first, not after, matters: the base clone cannot check out + `develop` while the promotion worktree still has it checked out, one branch checked out in + two worktrees at once is refused outright. Never delete `develop`, it is the promotion PR's + own head, and the repo's auto-delete-head-branches setting is kept off fleet-wide for exactly + this reason, so nothing does this automatically. + - A defensive sweep for anything drive-pr's own cleanup should already have removed but might + not have, an interrupted loop, a fix landed by hand outside that skill, or a maintainer + merge in the GitHub UI. `git worktree list` for any worktree still registered under this + task's feature branches, `git branch -vv` for any local feature branch, `git ls-remote + --heads origin` for any matching remote feature branch. For each, verify it finished by + reading GitHub's own state with the exact fields this check needs, not a bare listing, and + stop and report rather than guessing when selection is not exactly one match, on a non-1 + count exit non-zero rather than returning empty with success, an ambiguous or missing match + must fail loud, not read as an empty value still safe to act on: `gh pr list --head + "" --state merged --repo owner/repo --json + number,baseRefName,mergedAt,headRefOid,headRefName,headRepository --jq 'if length == 1 then + .[0] else error("expected exactly one merged PR for this head, got \(length)") end'`. + `--head` is expected to match exactly (verified against `gh` 2.97.0 on this repo, a bare + prefix of a real branch name returned nothing), but confirming `headRefName` equals `` + costs one field and is cheap insurance against a future `gh` behavior change, not a workaround + for a known partial-match case. Confirm `headRepository` is non-null and its `nameWithOwner` equals + `owner/repo`, the owner alone is not enough, a same-owner PR against an identically named + branch in a different repository must never pass this check either. Confirm `baseRefName` + is `develop` (a different merged pull request can share the same head branch name against a + different base, and that is never this sweep's target) and `mergedAt` is set. Compare tips + only where a remote branch actually exists. `git ls-remote --heads --exit-code -- origin + "refs/heads/"` is the exact-match form and must be, in that argument order. `--heads + origin ""` alone still tail-matches, a bare `topic/x` pattern also returns an unrelated + `other/topic/x` if one exists. `--` placed after `origin` instead of before it is not + equivalent either, verified empirically: with a `refs/heads/other/--` ref present, `--heads + origin -- "refs/heads/"` matched both that ref and the intended one, while `--heads -- + origin "refs/heads/"` matched only the one intended. Exit status is a tri-state, not a + stdin-emptiness check: `--exit-code` makes exit `2` mean query succeeded, branch gone, most + likely a prior cleanup attempt got interrupted after the remote delete but before the local + one, so skip straight to the local-tip check below and never attempt the remote delete a + second time. Exit `0` means it matched. Anything else is a failed query, a network or auth + problem, and stops and reports rather than being read as absence, an unreachable remote and a + genuinely gone branch both print nothing to stdout, only the exit code tells them apart. + Where the remote branch does exist, its tip must match that exact pull request's `headRefOid` + before its own delete proceeds, proving nothing landed on it since. Where a local branch + still exists too, its tip (`git rev-parse --verify "refs/heads/"`) must independently + match `headRefOid` before its own delete proceeds. Neither side needs the other to exist, a + prior interrupted attempt may have deleted one side already and left only the other, so + verify and delete whichever side is still there and skip whichever already is not, never + block one side's cleanup on the other side's absence. No `--` on `rev-parse`, verified + empirically: `git rev-parse -- ""` treats the argument after `--` as a path rather + than a revision and never resolves a SHA at all. The fully-qualified form needs no `--` + regardless, since `refs/heads/` never itself starts with `-`, and `--verify` fails + loudly rather than guessing when it does not resolve. Every branch or + worktree-path placeholder below is the real value, substituted as its own quoted argument + (a shell variable expansion such as `"$branch"`, or an argv element), never handed to `eval` + or `sh -c` for a second round of shell parsing, the only way an embedded `$()` or backtick + would actually run. A valid ref can start with `-` or carry a shell metacharacter, which is + why it stays quoted regardless. `--` marks the + end of options wherever a command supports it. + `git merge-base --is-ancestor develop` must never be used for either tip check, a + squash merge (drive-pr's own merge method) never makes the feature tip a literal ancestor of + `develop`, so the check reports every already-finished branch as unmerged. Only once GitHub + confirms it, and only when a local worktree or branch is still there to remove, remove the + worktree by its exact path (a dirty worktree stops cleanup rather than discarding uncommitted + work), `git worktree remove ""`, `git worktree list` names it, then delete the + local branch. `git branch + -d` has the identical squash blindness as `git merge-base --is-ancestor` and refuses too, so + use `git branch -D -- ""` here, safe only because the GitHub-state check just + proved that exact branch finished, the narrow post-squash exception git-commit-conventions + describes, never applied to an unverified branch. Then, only when the remote branch still + exists, delete it the same way, `git push origin --delete -- ""`. + Never `--force-with-lease` here, git-commit-conventions + forbids it unconditionally, the GitHub-state check just completed is the verification gate, + not a compare-and-swap at delete time. Never apply this sweep to `develop` or `main` + themselves, only to feature branches a drive-pr loop created. + +## Mechanics Live Elsewhere + +- The Merge Gate itself: pr-review-conduct. +- Never delete develop, no-op republish, the operational repos' dispatch-only model: + operational-vs-release-workflow. +- What the dispatch actually builds and publishes: workflow-ci-contract. +- Skills install and report semantics: skill-lifecycle. +- Cleanup mechanics: repo-worktree. + +## Stop and Report, Never Guess + +- A merge conflict, a newly failing check, or a gate item that regressed since drive-pr finished + are each a stop, report the exact state, never force or retry blindly. +- `gh pr merge` or `gh workflow run` failing is reported with its actual output, never + suppressed, never assumed harmless on the agent's side alone. diff --git a/.github/skills/operational-vs-release-workflow/SKILL.md b/.github/skills/operational-vs-release-workflow/SKILL.md index 0834b42..9a9e9f2 100644 --- a/.github/skills/operational-vs-release-workflow/SKILL.md +++ b/.github/skills/operational-vs-release-workflow/SKILL.md @@ -58,6 +58,7 @@ rather than guessing from the repo's contents. promotion PR's head *is* `develop`, so `--delete-branch` deletes it. An EOL-only conflict on a workflow YAML file resolves on a throwaway branch off `main`, not on `develop`. Full recovery and conflict-resolution commands: `references/branch-protection-and-promotion.md`. +- **A merge or release ends with worktree cleanup and the base clone on current `develop`.** Run the `repo-worktree` post-merge procedure after a feature squash merge. Run it again after a promotion or release completes, unless the user explicitly asks to retain a checkout or branch. Remove finished task, conflict-resolution, installer, and release helpers. Never delete `develop`, and never leave the base clone on `main` merely because `main` was promoted or released. - **Issue-closing keywords (`Closes #N`, `Fixes #N`) go in the `develop -> main` promotion PR, not the feature -> `develop` PR.** GitHub auto-closes an issue only when the closing keyword merges into the **default branch** (`main`), so a feature -> `develop` PR merge never fires it. diff --git a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index f30abd0..6d934c0 100644 --- a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -8,28 +8,24 @@ covers that case). ## Configuring branch protection: don't hand-build the rules -Reconstructing rulesets by hand is error-prone and has gone wrong on past ports. First delete -**all** legacy classic branch-protection rules and any stray rulesets (rulesets are the *only* -mechanism used), then create **exactly two rulesets named `develop` and `main`** by importing the -committed `repo-config/*.json` ruleset payloads via `gh api -X POST "repos///rulesets"` -(`gh ruleset` is read-only). The names are load-bearing, other governance content and the -workflows reference them. Operational repos import `repo-config/operational/develop.json` as their -`develop` ruleset (the `main` ruleset is shared), and the hub's `repo-config/configure.sh`, run -from a hub checkout against the repository named on its command line, selects the right `develop` -payload from the registry `workflowModel` automatically. **Brownfield repos** (pre-existing -history) need an extra step: `Require signed commits` rejects legacy unsigned commits and the -admin bypass does not cover `git push --force`, so re-signing requires temporarily disabling the -ruleset. See `repo-config/README.md` "Rulesets" for the configured state. +Delete **all** classic branch-protection rules and stray rulesets because rulesets are the only +protection mechanism. From a hub checkout at `main`, create **exactly two rulesets named `develop` +and `main`** from the hub's `repo-config/*.json` payloads. Run +`repo-config/configure.sh apply / release|operational` from that checkout. The names +are load-bearing because governance content and workflows reference them. The registry +`workflowModel` selects the `develop` payload for a registered repository. Pass the model +explicitly for a repository outside the registry. See the hub's `repo-config/README.md` +"Rulesets" for the configured state. ## Executing a `develop -> main` promotion safely Two traps, both learned the hard way: - **Never delete `develop`.** A promotion PR's head *is* `develop`, so `gh pr merge --delete-branch` - (and a repo's "Automatically delete head branches" toggle, kept off in `repo-config/settings.json` - for exactly this reason) deletes `develop` itself. Merge a promotion with a plain - `gh pr merge --merge`, no `--delete-branch`. If `develop` is ever lost this way, restore it to - the merged PR's head SHA, which is still reachable as the merge commit's second parent: + (and a repo's "Automatically delete head branches" toggle, kept off in the hub's + `repo-config/settings.json` for exactly this reason) deletes `develop` itself. Merge a promotion + with a plain `gh pr merge --merge`, no `--delete-branch`. If `develop` is ever lost this way, + restore it to the merged PR's head SHA, which is still reachable as the merge commit's second parent: `gh api -X POST "repos///git/refs" -f ref=refs/heads/develop -f sha="$(gh pr view --json headRefOid --jq .headRefOid)"`. - **Spurious EOL-only conflicts resolve by taking `develop`.** When `develop`'s `.editorconfig` line-ending default has changed (for example the fleet-wide CRLF-to-LF flip) while `main` hasn't @@ -39,7 +35,7 @@ Two traps, both learned the hard way: `git checkout -b promote/develop-to-main origin/main && git merge origin/develop`, take `develop`'s side for the EOL-conflicted files (`git checkout --theirs `) **after confirming each is content-identical modulo EOL, or that `develop` is a strict superset** - (`diff <(git show :2:f | tr -d '\r') <(git show :3:f | tr -d '\r')`), then open that branch into + (`diff <(git show ":2:" | tr -d '\r') <(git show ":3:" | tr -d '\r')`), then open that branch into `main`. Verify no genuine `main`-only content is dropped (build/test where the repo supports it). ## Why both rulesets omit "Require branches to be up to date before merging" @@ -110,5 +106,5 @@ supplies its own input-deterministic generator and wires the codegen reference w `actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0. Use `client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}`. When adding new App-token call sites, use the -same form, and do not reintroduce `app-id` / `CODEGEN_APP_ID`. See `repo-config/README.md` -"Secrets" for which secrets each mechanism needs. +same form, and do not reintroduce `app-id` / `CODEGEN_APP_ID`. See the hub's +`repo-config/README.md` "Secrets" for which secrets each mechanism needs. diff --git a/.github/skills/pr-review-conduct/SKILL.md b/.github/skills/pr-review-conduct/SKILL.md index f242468..810955e 100644 --- a/.github/skills/pr-review-conduct/SKILL.md +++ b/.github/skills/pr-review-conduct/SKILL.md @@ -41,11 +41,20 @@ visible comments, routinely still carries a finding nobody has answered. Treatin 2. A review is confirmed on the **current head SHA**, matched by commit SHA rather than assumed from a green merge-state. A push makes checks go green *before* the re-review lands, and the matched review is **read**, not just counted. A review can carry the head SHA and still decline - the PR outright, or say it read only part of the changed files. + the PR outright, or say it read only part of the changed files. `pr_review.py`'s + `review_on_head` names Copilot's own coverage specifically, the currently required reviewer, + not "no review of any kind covers this head": a trialed advisory reviewer (CodeRabbit, + Qodo) carrying the exact head under `other_reviewed`, with an empty review body and no new + threads, is its own ordinary "reviewed, nothing to flag" shape, not a missing review (#1066). 3. **Every** finding on that head SHA is closed: threads resolved, issue-level comments (which have no resolve action) triaged and replied to, **and** the low-confidence findings collapsed in the review body investigated and answered. Those appear in no thread, so polling threads - alone reports a clean pass while they stand. + alone reports a clean pass while they stand. The same holds for CodeRabbit's own + "outside diff range" comments (`cr_outside_diff` in `pr_review.py`'s digest) and for Qodo's + comment-only findings (`qodo_open`): neither opens a `reviewThreads` entry either, so + give each one the same triage the low-confidence findings above already get (#1058). Qodo's own + `Resolved`/`Dismissed` self-tracked badge is a fast pre-triage signal, not a substitute for + reading the finding, spot-verify against `gh pr diff` rather than trusting it outright. 4. Nothing in the review was a shape the tooling could not read (an unrecognized heading, a moved section, an unfamiliar coverage wording). An unrecognized shape blocks the gate on its own. File an issue naming it and quoting the body, rather than guessing what the new wording @@ -57,13 +66,20 @@ full stop, whatever the merge-state field says. ## Expected review loop +Open every fleet-owned pull request ready for review. Draft state delays the loop and causes +reviewers to skip, so it has no place in the internal feature-to-develop or develop-to-main +workflow. The separately documented `upstream-contribution-workflow` may use a draft while a +third-party contribution is still being prepared for upstream review. + Opening a pull request starts this loop by default. Creating the PR is not a terminal handoff. -Only an explicit maintainer instruction may stop, defer, or alter the loop. A draft state, silence, -or a request that says only "open a PR" is not such an instruction. +Only an explicit maintainer instruction may stop, defer, or alter the loop. Silence or a request +that says only "open a PR" is not such an instruction. Run every `scripts/pr_review.py` command below from a hub checkout. The script is hosted there and is never carried into a downstream repository. +Run `local-strict-review` against the branch's current diff before step 1's push, and again before any fix push under outcome 1 below. + 1. Push changes to the PR branch and open the pull request when it does not exist. 2. Run `scripts/pr_review.py status` once in the foreground and read its output. 3. Re-request a review for the **current head SHA**. Auto-trigger is unreliable, so request it @@ -78,14 +94,19 @@ is never carried into a downstream repository. 7. Reply to each thread and resolve what was addressed. 8. Re-run the loop after every fix push until the checks are green and no finding remains open. +The review effort setting is user-controlled. The workflow never selects or changes it. `status` reports `Lite`, `Balanced`, or `Max` when the completed review exposes that metadata, and distinguishes an inherited `Default ()` from an explicit choice. Missing effort metadata reports `unknown` and does not change coverage or completion. A pending effort-labeled request can complete without a `copilot_work_started` timeline event, so absence of that event never proves the request is abandoned. The bounded timeout reports `PENDING` when no review or terminal answer arrives. After a timeout with `requested=yes`, rerun `wait` for another bounded interval by default because the request may still be active. If the maintainer directs a retry, remove Copilot in the pull request UI, add it again, and rerun `wait`. This recovery replaces only the review request and never changes the effort setting. + Drive to green, a review confirmed on the latest head SHA and every actionable finding closed, then apply the Merge Gate above. **Never exit the loop early.** A round count is not a stopping condition, and neither is patience running out. Reporting only that the PR was opened is an early exit unless the maintainer explicitly instructed the agent not to monitor or drive its review. +After an authorized merge, run the `repo-worktree` post-merge cleanup procedure unless the user explicitly asks to retain the checkout or branch. The pull request loop is incomplete while its finished worktree or local task branch remains. It is also incomplete until the base clone returns to fetched and fast-forwarded `develop`. + ## Every finding ends in one of five outcomes -1. **Real, so fix it.** Reply with the fixing commit SHA. For a finding on platform-specific code +1. **Real, so fix it.** Run `local-strict-review` against the branch's current diff before pushing + the fix, then reply with the fixing commit SHA. For a finding on platform-specific code (PowerShell, a macOS- or WSL-only path), "fixed" means executed on that platform, per `agent-conduct` "Before Claiming Done": a fix reasoned out by analogy to a tested equivalent elsewhere is not yet fixed, and the reply says so rather than claiming the SHA closes it. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 02697a7..f7a7451 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -48,7 +48,7 @@ declaration, versioning, VS Code config), see `references/profiles.md`. | [ruff][ruff-link] | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | | [pyright][pyright-link] | type checker (the default, a strict baseline) | `pyproject.toml` `[tool.pyright]` | | [mypy][mypy-link] | additional/alternate type checker (optional, the CI checker in a mypy-in-CI repo, required for Home Assistant) | `pyproject.toml` `[tool.mypy]` (or per home-assistant/core) | -| [pytest][docs-link] | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | +| [pytest][docs-link] | test runner (build profile only, lint-only uses `unittest`) | `pyproject.toml` `[tool.pytest.ini_options]` | **Type checking targets strongly typed, deterministic code.** pyright in strict mode is the default baseline on first-party code (a repo may instead run mypy in CI and keep pyright @@ -72,7 +72,9 @@ inherently consistent. ## Local development loop -From inside the Python project directory: +From inside a **build**-profile Python project directory. A **lint-only** Scripts profile has no +`uv.lock` to sync and no pytest to run, substitute `uvx` per tool and `unittest` per the Two +Profiles section above: ```sh uv sync # creates .venv, installs deps + dev group @@ -85,13 +87,18 @@ uv run pytest # run tests uv build # produce wheel + sdist in ./dist (published packages only) ``` -The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the repo's type checker: -`uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both where the repo runs -both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are -documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell -chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. Git hooks are opt-in, so -wire `pre-commit` for `ruff` and the type checker yourself if you want local enforcement. +The **build**-profile Python clean-compile is `uv run ruff format` + `uv run ruff check` + the +repo's type checker: `uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both +where the repo runs both (see Type checking above). Run it, plus `uv run pytest`, before +committing. A **lint-only** profile's clean-compile substitutes its `uvx` and `unittest` +equivalents, per Two Profiles above, and has no such command to run before committing beyond +those. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, +no `&&` shell chaining, so it runs the same on any task shell) is in the hub +`vscode-tasks-python.json` snippet. CI runs the same clean-compile commands as the authoritative +backstop. A working local hook is strongly suggested, not opt-in: wire the Python `pre-commit` +framework from the canonical `catalog/snippets/pre-commit/.pre-commit-config.yaml`. See +GOVERNANCE.md "Running the Linters Locally" for what the hook must cover and what its absence +means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before @@ -140,9 +147,12 @@ For comments, docstrings, full type-hint rules, naming, imports, and all pattern ## Tests -`uv run pytest`. One test file per module (`test_.py`), fixtures over setup/teardown, -fakes over mocks. Test the docstring's contract, not implementation details. See -`references/testing.md` for the full conventions. +`uv run pytest` for a build profile, `unittest` for a lint-only Scripts profile (see Two Profiles +above). One test file per module (`test_.py`). A build profile prefers fixtures over +`unittest`'s `setUp`/`tearDown` lifecycle hooks. A lint-only profile uses those hooks directly, +since `unittest` has no fixture-injection mechanism of its own. Fakes over mocks either way. Test +the docstring's contract, not implementation details. See `references/testing.md` for the full +build-profile conventions, and `references/profiles.md` for the lint-only `unittest` conventions. ## Versioning @@ -157,10 +167,11 @@ Before pushing or opening a PR: - VS Code's Problems pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). -- The CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's type checker - (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as the local - loop above, run from the Python project directory (invoked as separate steps, not `&&`-chained, - so the runner shell is irrelevant). +- The **build**-profile CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's + type checker (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as + the local loop above, run from the Python project directory (invoked as separate steps, not + `&&`-chained, so the runner shell is irrelevant). A **lint-only** profile's CI gate is its `uvx` + equivalents plus its `unittest` suite, per `references/profiles.md`. - Markdown in this directory follows CODESTYLE.md's repo-wide Markdown and Spelling rules, packaged as the `comment-and-doc-style` Skill. diff --git a/.github/skills/python-codestyle/references/code-style.md b/.github/skills/python-codestyle/references/code-style.md index b7e62b0..9ad17f2 100644 --- a/.github/skills/python-codestyle/references/code-style.md +++ b/.github/skills/python-codestyle/references/code-style.md @@ -44,6 +44,19 @@ - **Use modern syntax**: `list[int]` not `List[int]`, `dict[str, X]` not `Dict[str, X]`, `X | None` not `Optional[X]`, `from __future__ import annotations` only when needed for forward references. +- **Don't hedge that syntax for an older interpreter.** `pyproject.toml` pins `target-version` / + `python_version` to 3.13 for every Python profile in this repo, and `spec/host-tools.json` + carries that as the host floor `scripts/host_gate.py` enforces, so 3.10+-only syntax (`X | None`, + `match`, etc.) needs no quoting, no `typing.Union` fallback, and no `from __future__ import + annotations` guard on that account alone. Add that import only when a real forward reference + needs it, per the bullet above. Three named exceptions carry a lower floor on purpose and say so + themselves: `scripts/skills_install.sh` and the `install-skills.*` bootstrap scripts, which must + run on whatever interpreter a host already has before this floor's toolchain exists to install + one, and `spec/resolve_description.py`, which `repo-config/configure.sh`'s own bootstrap probe + accepts down to 3.7 for the same reason, and which carries `from __future__ import annotations` + for exactly that purpose rather than out of habit. No other `spec/` code has a reason to hedge, + so that import or a quoted annotation appearing anywhere else in `spec/` is a sign this one + exception got copied rather than a pattern to follow. - **Don't add `# type: ignore` to silence pyright errors without a comment** explaining the constraint. If a recurring false positive needs suppression, configure it project-wide in `[tool.pyright]`. A new port doesn't change this, fix freshly surfaced type errors rather than diff --git a/.github/skills/python-codestyle/references/testing.md b/.github/skills/python-codestyle/references/testing.md index c19ff9d..0dae3fd 100644 --- a/.github/skills/python-codestyle/references/testing.md +++ b/.github/skills/python-codestyle/references/testing.md @@ -1,5 +1,9 @@ # Python Testing Conventions +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` and does not +use pytest, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) +are in `references/profiles.md`. + Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index b2d3ec6..d4f9e0e 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -161,11 +161,19 @@ A machine not yet migrated to this layout still isolates exactly the same way, s is the isolation rather than the path: create the worktree beside whatever layout the machine has, and note that the base clone may live elsewhere than `~/repos/`. -Claude Code's own `EnterWorktree` tool acts only on an explicit instruction from the user or the -project instructions, which is why the carried rules state this mandate in so many words. Given -a `name`, it creates the worktree under `.claude/worktrees/` inside the repo and bases it on the -GitHub default branch, which is the wrong path and the wrong base here. Create the worktree with -`git worktree add` as above, then attach with `EnterWorktree` `path:`, not `name:`. +## Agent-Specific Worktree Tools + +Provider-specific mechanics stay separate from the general creation procedure above: + +- **Claude Code:** its `EnterWorktree` tool acts only on an explicit instruction from the user or + project instructions. Given a `name`, it creates the worktree under `.claude/worktrees/` and + bases it on the GitHub default branch. Both differ from the fleet path and base. Create the + worktree with `git worktree add`, then attach with `EnterWorktree` `path:`, not `name:`. +- **Codex:** no provider-specific creation override applies. Use the general `git worktree add` + procedure above. Its host-specific writable-root setting lives in `docs/host-setup.md` "Agent + Worktree Access". +- **opencode:** no provider-specific creation override applies. Use the general + `git worktree add` procedure above. ## Preparing Git Hooks @@ -177,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. @@ -191,11 +199,12 @@ fails, report that boundary and fix the setup. Never bypass the hook to make the - `git worktree list`, run in any checkout of a repo, names that repo's base clone and every worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is in flight across the whole fleet. -- After the task's pull request merges, remove the worktree and its branch from the base clone: - `git worktree remove ~/repos/worktrees/-`, then `git branch -d `. -- After the task's pull request merges, remove a temporary standalone clone at its exact - `/-` path. The remote branch follows the repository's normal - pull request cleanup policy. +- **Cleanup after merge is the default terminal step.** Run it after a squash merge into `develop`. Run it again after a merge-commit promotion into `main`, unless the user explicitly says to retain a checkout or branch. A merge or release handoff is incomplete while finished task, conflict-resolution, installer, or release worktrees remain registered. +- **Verify before removing.** Read the pull request's merged state and head SHA from live GitHub state. Confirm the worktree is clean and resolves to that head. A dirty worktree stops cleanup because force-removing it would discard work. A detached helper worktree needs no pull request, but its commit must be contained in the branch whose completed operation created it. +- **Remove the exact finished worktree, then its local task branch.** Use `git worktree remove `. Try `git branch -d ` after a merge commit. A squash merge does not make the feature tip an ancestor of `develop`, so `-d` cannot recognize it as merged. After the live merged-PR and clean-worktree checks prove that exact branch finished, use `git branch -D ` under the narrow post-squash exception in `git-commit-conventions`. Never apply that exception to an unverified branch or to `develop`. +- **Remove temporary standalone clones and detached helper worktrees too.** Remove the exact `/-` path after confirming it is clean. The remote feature branch follows the repository's normal pull request cleanup policy. Never delete `develop` after a promotion because it is the permanent integration branch. +- **Return the base clone to current `develop`.** Fetch and prune `origin`, confirm the base clone is clean, switch it to `develop` when needed, and fast-forward it with `git merge --ff-only origin/develop`. A completed promotion or release does not leave the base clone on `main`. Stop and report a dirty base clone or a non-fast-forward instead of switching or reconciling it. +- **Prove the cleanup.** Finish with `git status --short --branch` in the base clone and `git worktree list`. The expected result is a clean base clone at `origin/develop` and no worktree belonging only to the completed task. - A worktree that refuses removal is dirty, and force is not the fix: look at what is uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, per the `git-commit-conventions` skill. diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index 308dcbc..9ac991f 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -42,29 +42,26 @@ stale clone answers confidently instead of failing, and verify the host with RESYNC.md section 2, against the target's `main` branch, never `develop`. A finding is a snapshot, so quote the run stamp in anything derived from it and re-run before acting on a finding read earlier in the session. +File any hub defect this work exposes against `ptr727/ProjectTemplate`. +Examples include bugs, conflicting sources, unclear or incomplete instructions, missing capabilities, and Copilot findings about any of them. +Search open and closed issues first, then update the matching issue or file a new one. +Preserve the evidence RESYNC.md section 2 requires, and do not leave the finding only in chat, a review thread, the downstream repo, or agent memory. ## Apply, in this order -1. **The instruction set first.** `AGENTS.md` and `GOVERNANCE.md` verbatim sections, then - `CODESTYLE.md` and `WORKFLOW.md`, including the `AGENTS.md` skill-dependency pointer paragraph - (naming `scripts/skills_install.py` and where the fleet's Skills live) as one more verbatim - unit carried in this same step, not a separate pass. **Before any verbatim re-vendor in this - step, run the `carried-instruction-file-guard` skill's distinctive-phrase probe against the - target file, every time, without exception.** This is not advisory language to weigh against - how routine the diff looks, a diff that looks routine is exactly the shape the - AGENTS.md-overwrite incident took. Do not proceed to the re-vendor until the probe has run and - any local addition it finds has a destination, per that skill's own procedure. +1. **The instruction set first.** `CLAUDE.md`, then `AGENTS.md` and `GOVERNANCE.md` verbatim sections, then `CODESTYLE.md` and `WORKFLOW.md`, including the `AGENTS.md` skill-dependency pointer paragraph (naming `scripts/skills_install.py` and where the fleet's Skills live) as one more verbatim unit carried in this same step, not a separate pass. `CLAUDE.md` is the single `@AGENTS.md`-import file that gets `AGENTS.md` into a Claude Code session's context at all, a separate baseline entry from `AGENTS.md` itself, so carrying one without the other still leaves that provider unconfigured. **Before touching `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, or `WORKFLOW.md` in this step, run the `carried-instruction-file-guard` skill's distinctive-phrase probe against the target file, every time, without exception, regardless of whether the update is a verbatim re-vendor or an intent-fidelity edit.** This is not advisory language to weigh against how routine the diff looks, a diff that looks routine is exactly the shape the AGENTS.md-overwrite incident took. Do not proceed to the re-vendor until the probe has run and any local addition it finds has a destination, per that skill's own procedure. `CLAUDE.md` is outside that guard's scope: it carries no mixed or repo-specific content by design, so its re-vendor is an ordinary verbatim-fidelity copy, no probe needed. 2. **Deletions second, before any re-vendor.** Only a `retire` disposition in `spec/divergences.json` authorizes removing a file, and the removal is swept tree-wide, per RESYNC.md section 4, before the deletion counts as done. -3. **Verbatim re-vendors** for everything the probe in step 1 cleared. A finding classified - modified rather than stale gets its diff read before being overwritten, since it may be an - improvement the hub should adopt instead of a mistake to erase. +3. **Verbatim re-vendors** for `CLAUDE.md` and everything else the probe in step 1 cleared. A + finding classified modified rather than stale gets its diff read before being overwritten, + since it may be an improvement the hub should adopt instead of a mistake to erase. 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` against the repo by name, - then `apply` for what it reports, never from a carried copy. + `repo-config/configure.sh check "/" release` (substitute `operational` for an + operational repo) from the hub at `main`, then `apply` for what it reports, never from a + carried copy. Run `spec/audit.py [RepoName]` from the same checkout for secrets. 6. **Intent files last, and by hand,** since nothing mechanical judges these. Reconcile the registry entry (`status`, `types`, `releaseTrigger`, `workflowModel`, @@ -77,4 +74,5 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report, done means measured, not applied. +commit the report once authorized, per `git-commit-conventions`, done means measured, not +applied. diff --git a/.github/skills/shell-codestyle/SKILL.md b/.github/skills/shell-codestyle/SKILL.md index 50b1f60..eaa4a58 100644 --- a/.github/skills/shell-codestyle/SKILL.md +++ b/.github/skills/shell-codestyle/SKILL.md @@ -3,8 +3,9 @@ name: shell-codestyle description: >- Governs Bash/shell script style for ptr727/ProjectTemplate fleet repos: when a bootstrap or host-tool script may be shell instead of Python, the mandatory set -Eeuo pipefail header, the - pipefail-versus-early-reader pitfall, self-locating scripts, shellcheck cleanliness, and the - why-not-what comment rule. Use this whenever writing, reviewing, or editing a .sh file, whenever + pipefail-versus-early-reader pitfall, self-locating scripts, the shellcheck-plus-shfmt + clean-compile, and the why-not-what comment rule. Use this whenever writing, reviewing, or + editing a shell script (a `.sh` file, or an extensionless bash/sh shebang script), whenever deciding whether a new script should be Bash or Python, or whenever a pipeline built from `curl`/`grep`/`jq`-style commands looks like it silently swallowed a failure. Triggers even when the task looks like a one-line tweak to an existing script, because a missing `-e`/`pipefail`, @@ -31,6 +32,12 @@ depend on one. Everything else is Python, with a test under the scripts tree's ` ## Rules +- **`shellcheck` is the linter and `shfmt` the formatter.** The clean-compile is `shellcheck` + clean at default severity plus `shfmt -d`, both reporting nothing before a commit. CI enforces + both, per `GOVERNANCE.md` "Running the Linters Locally", and `scripts/docker_lint.py` runs the + same pair headless. Neither is scoped to the `*.sh` glob alone: a tracked, extension-less + script whose shebang names bash or sh (the shape a script meant to run as a bare command takes) + joins the target list too. - **`set -Eeuo pipefail`, before the first command the script runs.** A header comment sits above it, as `repo-config/configure.sh` and the `host-setup/` scripts do, since what matters is that nothing executes unguarded rather than which line number it lands on. Without `-e` a failed diff --git a/.github/skills/skill-lifecycle/SKILL.md b/.github/skills/skill-lifecycle/SKILL.md index 720c59a..aa85391 100644 --- a/.github/skills/skill-lifecycle/SKILL.md +++ b/.github/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then, once authorized, commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. diff --git a/.github/skills/standup-a-repo/SKILL.md b/.github/skills/standup-a-repo/SKILL.md index 56c66a1..363d6bb 100644 --- a/.github/skills/standup-a-repo/SKILL.md +++ b/.github/skills/standup-a-repo/SKILL.md @@ -51,10 +51,13 @@ maintainer can supply what section 0A lists. section 2, then write or repair its `registry/repos.json` entry and confirm it with `spec/validate.py`. -4. **The instruction set, before authoring anything.** STANDUP.md section 1A: carry `AGENTS.md`, - `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md` and `AUDIT.md`, adapted rather than cloned for - the ones that describe a repo, plus `.markdownlint-cli2.jsonc` and `cspell.json`. Read - `CODESTYLE.md` and the `GOVERNANCE.md` documentation-style rules before writing any repo +4. **The instruction set, before authoring anything.** STANDUP.md section 1A: carry `CLAUDE.md`, + `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md` and `AUDIT.md`, adapted rather + than cloned for the ones that describe a repo, plus `.markdownlint-cli2.jsonc` and + `cspell.json`. `CLAUDE.md` is the fixed, verbatim `@AGENTS.md`-import file that gets + `AGENTS.md` into a Claude Code session's context at all, a separate baseline entry from + `AGENTS.md` itself, so carrying one without the other still leaves that provider unconfigured. + Read `CODESTYLE.md` and the `GOVERNANCE.md` documentation-style rules before writing any repo content of your own, the same window-closes shape as signing in step 1. 5. **Capture the source, if one exists.** STANDUP.md section 1B, only when the repo's content @@ -74,9 +77,12 @@ maintainer can supply what section 0A lists. inventing a shape. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub - repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` and check with the same - command's `check` subcommand, never from a hand-built or carried copy. + repository agree before running anything else here, then run + `repo-config/configure.sh check owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main`. A non-zero exit there means drift was found, not a + command failure. Review what it reports. Then run the same command's `apply` subcommand, which + idempotently reconciles the repo to the full committed configuration regardless of what `check` + reported, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in diff --git a/.github/skills/upstream-contribution-workflow/SKILL.md b/.github/skills/upstream-contribution-workflow/SKILL.md index 3f88062..eec4fcf 100644 --- a/.github/skills/upstream-contribution-workflow/SKILL.md +++ b/.github/skills/upstream-contribution-workflow/SKILL.md @@ -39,7 +39,11 @@ fleet's internal model so the two are never conflated. carries only the intended, minimal commit history, one commit (or a small, deliberate set) that states what the change is, not how it was arrived at. 4. **Open the PR against the upstream repo from that second, clean branch.** This is the only - branch upstream ever sees. + branch upstream ever sees. An upstream draft may be opened only after that clean presentation + branch exists and is published. When more preparation is needed, continue on the dirty branch, + re-squash it into the clean branch, and update the same draft by the step 5 procedure. Never + iterate directly on the published presentation branch. Mark the draft ready when preparation + finishes. Open it ready immediately when no preparation remains. 5. **If upstream reviewers ask for changes, apply them to the dirty branch first**, iterate there the same way as step 2, then re-squash the updated dirty branch into the clean branch that actually reaches upstream. Updating the same upstream PR rather than opening a new one each diff --git a/.github/skills/workflow-ci-contract/SKILL.md b/.github/skills/workflow-ci-contract/SKILL.md index b36ec4a..e02fdfb 100644 --- a/.github/skills/workflow-ci-contract/SKILL.md +++ b/.github/skills/workflow-ci-contract/SKILL.md @@ -21,7 +21,7 @@ description: >- ## Style Rules That Break in One-Line Diffs - **Pin every action to a commit SHA** with a trailing `# vX.Y.Z` comment, first-party included. The one documented no-pin exception is `dotnet/nbgv@master`. Invent no others. -- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and `repo-config/{develop,main}.json` in lockstep, or required-check enforcement silently breaks. +- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and the hub's `repo-config/` payloads in lockstep, or required-check enforcement silently breaks. - **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'` with `cancel-in-progress: true`. The publisher is the documented exception: a global ref-independent group with `cancel-in-progress: false`, so publishes serialize and never cancel mid-push. - **Shells**: every multi-line bash `run:` starts `set -Eeuo pipefail`. Multi-line `if:` uses `>-`, never `|`. - **Boolean inputs** are declared in both trigger blocks and compared against both forms, `${{ inputs.foo == true || inputs.foo == 'true' }}`, since `workflow_dispatch` delivers strings. diff --git a/AGENTS.md b/AGENTS.md index 1384d0d..9bf648e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,8 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). R | A live config repo rather than a code repo | `Operational Repositories`, packaged as the `operational-vs-release-workflow` Skill | | Onboarding a repo or running a conformance sweep | `Repository Onboarding and Conformance` (hub only, not carried). Standing up a new repo from a hub checkout is packaged as the `standup-a-repo` Skill, resyncing one already stood up the same way is `resync-a-repo`, and measuring a named repo against the fleet ground truth per `AUDIT.md` is `audit-a-repo`, all hub-context only | | Running a fleet gate, the review digest, or the config script | `Hub-Hosted Tooling` | +| Running a lint or format check locally, or a lint tool missing from `command -v` | `Running the Linters Locally (Known-Working Invocations)` | +| Running a test locally, or a test runner missing or failing to spawn | `Verification Discipline` | | Writing a commit message or pull request title | `Pull Request Title and Commit Message Conventions`, packaged as the `comment-and-doc-style` Skill | | Any prose, comment, doc, or line-ending change | `Documentation Style Conventions`, packaged as the `comment-and-doc-style` Skill | | Proving work actually happened | `Verification Discipline`, surfaced at its decision moment by the `agent-conduct` Skill, and the section keeps the full rules | @@ -107,3 +109,7 @@ A row above with no Skill annotation is doc-only by decision, not by omission. A Some of the rules above are also packaged as Claude Code / opencode / Codex Skills, hand-authored at `.agents/skills/` in the hub (not a repo-relative link here, since that path is hub-local and not carried into every fleet repo), so they surface automatically instead of needing to be re-read every session. `scripts/` is hub-hosted and reached rather than carried, per "Hub-Hosted Tooling", so run the installer from a hub checkout: `python3 scripts/skills_install.py` (or the `.sh`/`.ps1` wrapper) once per machine, from `github.com/ptr727/ProjectTemplate`, installs them for every repo touched from that machine. `python3 scripts/skills_install.py --report`, also from a hub checkout, says whether this machine is current. A rule that keeps needing to be restated is a sign the install is missing or stale, not that the rule does not exist. Keeping a repo's own carried `.github/copilot-instructions.md` in sync with the hub, without losing that repo's own "Disproved Claims" ledger entries in the process, is `copilot-instructions-keeper`, a skill about maintaining that file rather than a rule extracted from it, since the file itself is read directly by the Copilot bot and stays fully intact everywhere it is carried. Checking, from inside this repo's own session with no operator watching, whether this repo and this machine are actually current against the hub is `fleet-conformance-check`, new content rather than a rule extracted from a section, the counterpart to `resync-a-repo` that needs no standing hub checkout or named target beyond the repo the session is already in, even though its own check fetches a hub checkout to reach `scripts/skills_install.py`. Opening a pull request against a repository outside this fleet, one the maintainer does not control, follows a different workflow entirely, new content rather than a rule extracted from a section, packaged as `upstream-contribution-workflow` and independent of the target repo's own type or workflow model. Isolating a task into its own worktree before its first file edit, with the base-branch choice, the layout convention, and the cleanup mechanics, is `repo-worktree`, the task-start surface of the `Repository Boundaries and Write Safety` law, which keeps the rule. Creating, changing, or retiring one of these skills is itself packaged as `skill-lifecycle`, hub-context only, since `.agents/skills/` exists only in the hub and the generated plugin tree is never hand-edited. Adding or changing a managed host tool is packaged as `add-host-tool`. It keeps the cross-platform contract, installer, documentation, test, and native-verification surfaces together. + +Driving a pull request through its review loop, from a feature branch into `develop` and, when asked, on to a mergeable `develop -> main` promotion PR, disposing of every reviewer finding along the way per `pr-review-conduct`, is packaged as `drive-pr`, new content rather than a rule extracted from a section. Merging a ready promotion PR and dispatching the release it unblocks, refreshing this machine's installed Skills first when the repo is this hub, is `merge-and-release`, its own new-content package, invoked separately from `drive-pr` so the promotion merge and the release dispatch each keep their own explicit go-ahead. + +Running one read-only, adversarial review pass against a branch's current diff against its target branch, full file context included, on the strongest model tier the session can reach, before a unit of PR-bound work is pushed toward a pull request or claimed done, is packaged as `local-strict-review`, new content rather than a rule extracted from a section. `drive-pr`, `pr-review-conduct`, and `agent-conduct` each reference it at the moment they already govern, rather than restating what it does. diff --git a/AUDIT.md b/AUDIT.md index 28cd096..5c7acf2 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,88 +1,65 @@ # AUDIT.md -How this repository audits itself against its committed baseline and reports drift. This is the repo-scoped adaptation of the fleet-wide AUDIT.md kept at the fleet hub (carried per the [repo-config downstream carry][repo-config-readme]), and the hub's fleet-wide audit remains authoritative. The ground truth here is the committed [`repo-config/`][repo-config] payloads and [`spec/secrets.json`][secrets], and the prose authorities are [`GOVERNANCE.md`][governance], [`CODESTYLE.md`][codestyle], and [`WORKFLOW.md`][workflow]. +How this repository audits itself against its committed baseline and reports drift. This is the +repo-scoped adaptation of the fleet-wide AUDIT.md kept at the fleet hub, and the hub's fleet-wide +audit remains authoritative. The ground truth is the hub's own committed `repo-config/` payloads +and `spec/secrets.json` (this repo carries no local copy of either), and the prose authorities are +[`AGENTS.md`][agents], [`GOVERNANCE.md`][governance], [`CODESTYLE.md`][codestyle], and +[`WORKFLOW.md`][workflow]. -The audit is read-only: it diffs live state against the committed baseline and reports findings; it never applies changes. The verdict vocabulary is [`WORKFLOW.md`][workflow]'s: **operational / not operational**, **N/A**, **defect**, and the applicable/absent rule. +The audit is read-only: it diffs live state against the committed baseline and reports findings, +and it never applies changes. The verdict vocabulary is [`WORKFLOW.md`][workflow]'s: **operational +/ not operational**, **N/A**, **defect**, and the applicable/absent rule. ## Scope -This is an operational (live-config) repo: `main` and `develop` rulesets, general repository settings, and secret names. Code-project dimensions (analyzers, publish mechanisms, coverage) are N/A, so see [GOVERNANCE.md "Branching Model"][governance-branching-model] for the model this baseline encodes. +This is an operational (live-config) repo: `main` and `develop` rulesets, general repository +settings, and secret names. Code-project dimensions (analyzers, publish mechanisms, coverage) are +N/A, see [GOVERNANCE.md "Branching Model"][governance-branching-model] for the model this baseline +encodes. -## General Settings +## General Settings and Rulesets -Diff the live repository settings against [`repo-config/settings.json`][repo-config-settings]. The two state-dependent settings are not in the file: `has_discussions` follows visibility (public on / private off) and `default_branch` is `main`. +This repo carries no local `repo-config/` directory: the hub hosts the settings and ruleset +payloads it is checked against, so both are validated from a hub checkout rather than diffed +locally. Run, from a checkout of `ptr727/ProjectTemplate` at `main`: -```bash -repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" -live=$(gh api "repos/$repo" --jq '{has_wiki,has_projects,allow_merge_commit,allow_squash_merge,allow_rebase_merge,allow_auto_merge,allow_update_branch,delete_branch_on_merge}') -diff <(jq -S . repo-config/settings.json) <(jq -S . <<<"$live") \ - && echo "settings: in sync" || echo "settings: DRIFT" +```sh +repo-config/configure.sh check ptr727/ESPHome-Config operational ``` -## Rulesets - -Diff each live ruleset against the committed expected payload with a normalized comparison (sort the order-insensitive `rules[]` on each rule's whole content before diffing, so a reordered but equivalent ruleset does not read as drift). The compared subset is `name`, `target`, `enforcement`, `conditions` and `rules`, and `bypass_actors` sits deliberately outside it, which is the same subset and the same sort key the fleet-wide audit uses. Who may bypass a ruleset is a per-repository human decision taken in the UI, no payload declares one, and the fleet's ruleset tooling treats it that way in both modes, writing the live list back unchanged when it applies a payload and reporting it without asserting when it checks one. Comparing it here would contradict that and report a ruleset finding against this repository for having any bypass actor at all, which is the field's normal state rather than a deviation. This operational carry keeps its `develop` payload at [`repo-config/operational/develop.json`][repo-config-develop]. - -```bash -repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" -# bypass_actors stays outside the projection, since no payload declares one and jq cannot sort the null that leaves. -# Rules sort on each rule's whole content, matching the key the fleet-wide audit sorts by. -# Sorting on .type alone leaves two rules of one type in input order, so a reordered pair would read as drift. -# canon sorts keys at every depth before serializing, because the committed payload is written key-sorted and the API returns its own order, so a bare tojson gives the same rule two different sort keys. -# It recurses rather than calling walk/1, which the declared jq floor does make available, because the recursion costs nothing and compiles below the floor as well. -# A host on jq 1.5 would not degrade on walk, it would fail to compile the filter and report drift on every ruleset it never compared, which is what the fleet's ruleset tooling defines its own recursion to avoid. -canon='def canon: . as $in | if type == "object" then reduce (keys_unsorted|sort)[] as $k ({}; . + { ($k): ($in[$k]|canon) }) elif type == "array" then map(canon) else . end;' -norm="$canon"'{name,target,enforcement,conditions,rules} | .rules|=sort_by(canon|tojson)' -# Paginate so later-page rulesets count: --paginate with --jq '.[]' emits one JSON object per ruleset -# across all pages; jq -s re-assembles them into the single array the selections below expect. -rulesets=$(gh api --paginate "repos/$repo/rulesets" --jq '.[]' | jq -s '.') -for b in develop main; do - file="repo-config/$b.json" - [ "$b" = "develop" ] && file="repo-config/operational/develop.json" - # Exactly one ruleset per name: zero or duplicates is itself a finding - report it, never diff a guess. - count=$(jq --arg n "$b" '[.[] | select(.name==$n)] | length' <<<"$rulesets") - [ "$count" -eq 1 ] || { echo "$b: expected exactly 1 ruleset, found $count (defect/drift)"; continue; } - id=$(jq --arg n "$b" '.[] | select(.name==$n) | .id' <<<"$rulesets") - diff <(jq -S "$norm" "$file") \ - <(gh api "repos/$repo/rulesets/$id" --jq '{name,target,enforcement,conditions,rules}' | jq -S "$norm") \ - && echo "$b: in sync" || echo "$b: DRIFT" -done -``` - -The result must be exactly two rulesets named `develop` and `main` - a missing ruleset or a divergent payload is a **defect**; a duplicate or stray ruleset is a **drift finding**. +This asserts the live repository settings against the hub's `settings.json`, and each of the +`main` and `develop` rulesets against the hub's `main.json` and `operational/develop.json`, +exiting non-zero on drift. The result must be exactly two rulesets named `develop` and `main` +(a missing ruleset or a divergent payload is a **defect**, and a duplicate or stray ruleset is a +**drift finding**). ## Secrets -Confirm each name [`spec/secrets.json`][secrets] requires exists in the stores its mechanism claims - the name lists derive from the spec itself, so this check and the spec cannot drift apart - and no forbidden name is present in any store (names only; values are not readable). +This repo carries no local `spec/secrets.json` either, for the same reason: the required and +forbidden name lists resolve centrally from the registry entry (`publish[]` and `types[]`) rather +than from anything repo-specific, so a per-repo copy could only restate the hub's own computation +or drift from it. This repo declares `requiredSecrets: []` and `publish: [{ "target": +"github-release", "mechanism": "none" }]` in the hub's registry, which resolves to no required +secret beyond the fleet baseline. Confirm it from a hub checkout at `main`: -```bash -repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" -# Name lists derive from spec/secrets.json (baseline plus any mechanisms, per store claim; forbidden names -# checked in every store), so this check and the spec cannot drift apart. --paginate so names beyond the -# first page still count. -for store in actions dependabot; do - names=$(gh api --paginate "repos/$repo/$store/secrets" --jq '.secrets[].name') - for s in $(jq -r --arg store "$store" '[.baseline, ((.mechanisms // {}) | .[])] | map(select(.stores | index($store)) | .requires[]) | .[]' spec/secrets.json); do - grep -qx "$s" <<<"$names" && echo "$store/$s: present" || echo "$store/$s: MISSING (defect)" - done - for s in $(jq -r '[.baseline, ((.mechanisms // {}) | .[])] | map(.forbids[]) | .[]' spec/secrets.json); do - grep -qx "$s" <<<"$names" && echo "$store/$s: forbidden name present (defect)" || true - done -done +```sh +python3 spec/audit.py ESPHome-Config ``` ## Verdict and Follow-Up -A missing required item or a divergent payload is a **defect** (not operational), and an equivalent outcome in a non-standard form is a **drift finding**. N/A items are excluded, never counted as failures. Surface findings as repository issues. Fixes land as direct signed commits to `develop` per [GOVERNANCE.md "Branching Model"][governance-branching-model]. To re-apply the whole baseline, run the hub's apply script from a hub checkout naming this repository, which is where that script lives rather than here (see [repo-config/README.md][repo-config-readme]). +A missing required item or a divergent payload is a **defect** (not operational), and an +equivalent outcome in a non-standard form is a **drift finding**. N/A items are excluded, never +counted as failures. Surface findings as repository issues, and fixes land as direct signed +commits to `develop` per [GOVERNANCE.md "Branching Model"][governance-branching-model]. To +re-apply the whole baseline, run the hub-hosted `configure.sh apply` from a hub checkout, naming +this repository and its model. +[agents]: ./AGENTS.md [codestyle]: ./CODESTYLE.md [governance]: ./GOVERNANCE.md [governance-branching-model]: ./GOVERNANCE.md#branching-model -[repo-config]: ./repo-config/ -[repo-config-develop]: ./repo-config/operational/develop.json -[repo-config-readme]: ./repo-config/README.md -[repo-config-settings]: ./repo-config/settings.json -[secrets]: ./spec/secrets.json [workflow]: ./WORKFLOW.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a2c84a1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# Claude Code Entry Point + +@AGENTS.md + +Claude Code reads `CLAUDE.md`, not `AGENTS.md`, so the import line above is what gets this repository's rules into a Claude Code session at all. See `AGENTS.md` for what is authoritative and why. This file carries no rule of its own, and adds none beyond the import line. diff --git a/CODESTYLE.md b/CODESTYLE.md index 5c89d38..4acf03d 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in [GOVERNANCE.md "Running the Linters Locally"][governance-running-the-linters-locally]. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A local commit/pre-commit gate is the repo's choice.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so none is mandated, but that is **not** a recommendation against commit gates. CI is the authoritative backstop regardless, and a local gate is an additive convenience a repo may wire and keep: Husky.Net (and `dotnet husky run` as a style step) for .NET, `pre-commit` for Python. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so the *mechanism* stays the repo's choice: Husky.Net for .NET, `pre-commit` for Python, canonical configs for both in `catalog/snippets/`. What that gate must cover, and what its absence means for the audit, is [GOVERNANCE.md "Running the Linters Locally"][governance-running-the-linters-locally], not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions @@ -61,7 +61,7 @@ One Python subtree, [`easystart/python`][easystart-python], the standalone EasyS ## Shell -Bash, and only where a program cannot be Python: a bootstrap that installs the interpreter cannot be written in it, and a host tool that must run before a development toolchain exists cannot depend on one. Everything else is Python, with a test under the scripts tree's `tests/` directory. The mandatory `set -Eeuo pipefail` header, the pipefail-versus-early-reader pitfall, self-locating scripts, `shellcheck` cleanliness, and the why-not-what comment rule are packaged as the `shell-codestyle` Skill at `.agents/skills/shell-codestyle/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full rules. +Bash, and only where a program cannot be Python: a bootstrap that installs the interpreter cannot be written in it, and a host tool that must run before a development toolchain exists cannot depend on one. Everything else is Python, with a test under the scripts tree's `tests/` directory. The mandatory `set -Eeuo pipefail` header, the pipefail-versus-early-reader pitfall, self-locating scripts, the `shellcheck`-plus-`shfmt` clean-compile, and the why-not-what comment rule are packaged as the `shell-codestyle` Skill at `.agents/skills/shell-codestyle/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full rules. Run the clean-compile check itself per [GOVERNANCE.md "Running the Linters Locally"][governance-running-the-linters-locally], not by probing `command -v shellcheck`. ## C++ diff --git a/GOVERNANCE.md b/GOVERNANCE.md index b7056a7..4538b5a 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -100,6 +100,25 @@ pull request. Line-ending governance for an operational repo is in [Line Endings](#line-endings), where its `[*]` default follows the consuming app's native platform per the registry `lineEndings` field, not the fleet LF default. +### Repo-Scoped Secrets + +A repo whose own stacks or scripts read local runtime credentials from disk, most commonly an operational repo's Docker Compose stack, keeps them in a dotted `.secrets/` directory at the repo root. This is the repo-scoped counterpart to the host-scoped `~/.secrets/` convention a repo's own `OPERATIONS.md` may document, and it is a different thing from `spec/secrets.json`, the CI/GitHub Actions secret-name registry `spec/audit.py` cross-checks. `spec/secrets.json` governs what a workflow reads from GitHub Actions. This convention governs what a repo's own process reads from its own checkout. + +- **The directory is named `.secrets/`, dotted, never a bare `secrets/`.** +- **A single opaque credential file carries no extension** (`homeassistant_db_password`, not `homeassistant_db_password.txt`), the same reason `README` and `LICENSE` carry none. It is a security property, read but never sourced, not a formatting preference. +- **A structured credential keeps its format's extension** (`.json` for structured config). +- **The shared env file is named for what it configures**, not a bare `.env` (`docker.env` for a repo whose stacks are Docker Compose), so a second env-shaped file added later stays unambiguous. +- **Every real secret file has a tracked `.example` beside it**, and only the `.example` files plus a `README.md` catalog are un-ignored: + + ```gitignore + **/.secrets/* + !**/.secrets/*.example + !**/.secrets/README.md + ``` + + A fresh checkout then documents its own required shape without ever exposing a real value. This negation keeps a real secret file out of a **new** commit. It does not remove one already tracked: `.gitignore` has no effect on a path git already follows. A real secret file found tracked is removed from the index (`git rm --cached `) and its credential is rotated, not just added to `.gitignore` going forward. +- **`.secrets/README.md` is a catalog**, one row per file naming what it holds and what consumes it, plus a short note on how the directory relates to `~/.secrets/` where the repo also touches that. + ## Hub-Hosted Tooling The fleet's tooling lives in the hub once and a repository runs it from there rather than holding a copy. A carried script is current only until the next fix to it, and a repository that misses the sweep does not fail loudly, it audits itself with an older gate while reporting the same command in its output. Removing the copy removes the sweep, the stale-copy detection, and the disposition each stale copy earns, all at once. The hub is the repository [`AGENTS.md`](./AGENTS.md) "Fleet Bootstrap" names, and that section is the entry point whenever nothing else present says where it is. @@ -149,6 +168,7 @@ ASD-STE100's structural half is the adopted house style: short sentences, one in The checks that separate work actually done from work that merely reports success. Their unifying property: **every failure below is green.** A skipped job and a passing job are indistinguishable in the aggregated required check, a pattern that matches less still exits zero, and a gate that stops gating still reports success. No linter, status check, or review layer catches any of them. - **Locate every check a change owes before running any of them, and CI's coverage is not that list.** The checks are read from what the repository declares, meaning its [`OPERATIONS.md`](./OPERATIONS.md) "Local Verification" section alongside the workflows, rather than inferred from whatever the pipeline happens to run. Part of a repository's contract is routinely unreachable from a runner, a redirect no build serves, a deploy no pull request performs, hardware no runner holds, so the check covering that part lives in a document rather than in a workflow and is run by hand before the pull request opens. Green is then the precise signal that it was skipped, because the pipeline reports success over the half it reaches while saying nothing about the half it cannot. Reading a document's own description of itself is not how such a check is found, since a topical document is named for its most visible function, usually a post-merge one, and an accurate description of that function routes a pre-merge task away from the file holding the gate. The destination is declared fleet-wide for that reason, so finding the check never depends on how well one repository worded a pointer to it. +- **A test runner failing to spawn is not evidence that no test coverage applies here.** `uv run pytest` failing to spawn in a lint-only Python Scripts profile is that profile working as intended, not a missing dependency, per the `python-codestyle` Skill's Two Profiles. Read the actual invocation from the same `OPERATIONS.md` "Local Verification" section the bullet above names, rather than guessing a generic test-runner command, and report that document's own command result, not the guessed command's failure. - **A test must assert the mechanism it names, and a gate has to be watched failing.** Label each case by the behavior it proves, then write the case that reintroduces the fault and confirm the gate objects to it. A case that passes for an incidental reason, the right answer reached by the wrong path, is worse than no case, because it is later cited as evidence. A proof that restates the gated data instead of reading it proves only that the function works, so drive the real table or the real config. And a gate that finds nothing is indistinguishable from a gate with nothing to find, so assert a floor on what a healthy run covers. - **Gates, filters, and gate-like watchers fail loud, never narrow quietly.** A pattern that silently matches less, an allowlist that silently stops matching, or a gate that silently stops gating all report success while doing nothing. When a construct exists to notice something, make the not-noticing case produce an error or an annotation. An identity allowlist used as a gate, for one, must raise an error when its list stops matching, not silently pass everything through. - **Config with a uniqueness rule is validated on read, and its consumers assert what it promised.** A repeated key in a lookup table is not a precedence question to settle quietly, it is two answers to one question, and keeping whichever came last picks one of them where the reader sees no choice being made. Fail on the duplicate at the point the config is read, so the code downstream can rely on the invariant instead of re-deriving it. @@ -160,7 +180,9 @@ The checks that separate work actually done from work that merely reports succes - **Never edit an active `.code-workspace` file.** A workspace file rewritten on disk can make VS Code reload the window, and a reload destroys the running agent session's context, so the work in flight is lost with nothing to catch it, and the trigger is not fully characterized (an agent's edit has caused the reload where a human's identical edit did not). Surface the needed change for the maintainer to apply by hand. - **A green check is not evidence the work happened.** A skipped job and a passing job are indistinguishable in the aggregated required check. When a job exists to exercise something, confirm from its log that it ran and produced the output it promises. - **A local clone is not the branch it names, it is whatever that clone last fetched.** Reading a checkout on disk answers what that clone last saw, so a finding taken from one carries a date nobody stated, and two failures of exactly that shape are on record from one session: a repository reported as still drifted on a file whose fix had already merged, and a repository reported as missing a file it carries because the checkout sat on an older branch. Read the live ref through the API where the claim will be acted on, or fetch immediately before reading, and name the ref and the commit in any finding a local read produced. A clone stays the right tool for anything needing history or a build, which an API read cannot give. +- **A checkout already sitting on disk is not yours to trust for being there.** A clone or worktree this session did not create, found while looking around a machine, may belong to another concurrent session's task, sit on a stale fetch or a branch nobody expects, or hold uncommitted edits nobody has reviewed, and none of that is visible from the directory listing that found it. Running `git status`, `git remote -v`, or `git branch --show-current` against it, or reading a file inside it, answers for whatever that checkout happens to hold at that moment, not for the repository, and the found checkout is not the "local clone" the bullet above means, since this session never fetched it and has no basis for trusting what it last saw. Clone the repository fresh into a location this session controls, or read the live state through the GitHub API, rather than adopting a pre-existing checkout as ground truth. - **A "does not exist" claim names the branch it was checked against.** A worktree or checkout answers for whichever ref it was built from, and that ref is not necessarily the one the content lives on: a `release`-model repo carries in-flight content on `develop`, per "Branching Model" above, well before it reaches `main`, so a worktree defaulted to the fleet's default branch can hold nothing while the repository holds everything. Before reporting a file, a directory, or a piece of content as absent anywhere in a repo, check it against the branch the repo's own model designates as current for that kind of content, not only whichever branch a worktree or checkout happened to default to, and name the branch the negative claim was checked against in the finding itself. +- **A raw-file fetch 404s the same way for a private repository as for a genuinely missing file.** `curl`ing `raw.githubusercontent.com////` returns an indistinguishable 404 whether the repository is private, the ref does not exist, or the path is wrong, so an agent that treats that response as "the content does not exist" has made the same unstated-branch mistake the bullet above names, only over visibility instead of branch. Where a repository's visibility is not confirmed public, read its content through `gh api "repos///contents/?ref="` instead. Capture the result before decoding it rather than piping straight into `base64 -d`: `content=$(gh api "repos///contents/?ref=" --jq '.content') && printf '%s' "$content" | base64 -d`. A direct pipe reports only the decoder's own exit status, so a failed `gh api` call (a bad ref, an auth gap, a network error) can hand `base64 -d` nothing to decode, which it does successfully, misreading a failed fetch as a successful empty read. The `&&` form fails on the fetch itself instead of decoding past it. Never merge the error stream into either form with `2>&1`: a failed call's error text flows into `base64 -d` in place of content and corrupts the decode with no clue why. Verify the ref resolves (a commit SHA is unambiguous where a branch name may have moved, been deleted, or never existed on the remote) before reading either failure as an answer about the content itself. - **A launched process is not a result, and a cause nobody observed is not a diagnosis.** "The watcher is armed" names a process rather than a finding, so what gets reported is the output that process produced, and where it produced none, that absence is the report. The failure it prevents is an agent standing still on a condition that was met half an hour earlier, having announced the wait and never read it. Naming an external cause for such a stall afterwards, a throttle or a quota that appears nowhere in the record, turns a local defect into a story about someone else and closes the investigation on the wrong party, so read the record for the cause before naming one, and where the record does not carry it, report the cause as unknown. - **A workflow change is only fully exercised by CI.** Extracting a `run:` block and executing it locally validates the script and nothing else, because `secrets: inherit`, `permissions:`, `needs:` wiring, and reusable-workflow inputs resolve only in a real run. - **Platform-specific code is "verified" only on the platform it runs on.** PowerShell on Windows, a macOS-only `mktemp`/`ssh-agent` behavior, a WSL-specific path quirk: an agent reasoning about such code from a different host, however carefully, has not executed it, and reasoning by structural analogy to an already-tested equivalent on another platform ("the POSIX version works, so the PowerShell version should too") is a plausible first pass, not verification. State it as exactly that, an unverified structural match, and never in the same words used for a tested fact. When no agent in the loop has access to the target platform, say so, and either defer the platform-specific portion to a human or an agent that has that access, or ship it clearly labeled unverified. @@ -170,7 +192,7 @@ This section keeps the full rules and is surfaced at its decision moment by the ## PR Review Etiquette -The provider-agnostic review-loop contract every fleet repo follows starts when a pull request opens. Creating the pull request is not a terminal handoff. Run the review status once in the foreground. Then start the bounded review wait in a background process. Request a review on every push. Confirm it covers the current head SHA and the full diff rather than only part of it. Triage every finding, including low-confidence findings collapsed into the review body rather than threads. Reply to and resolve every addressed finding. Repeat after every fix until the checks are green and the current-head review leaves no finding open. Only an explicit maintainer instruction may stop, defer, or alter this default. A draft state, silence, or a request that says only "open a PR" is not such an instruction. Never merge on a green or CLEAN merge state alone. That state does not prove the review covered the current head SHA and full diff. It also does not expose unanswered low-confidence findings that opened no thread. +The provider-agnostic review-loop contract every fleet repo follows starts when a pull request opens. Open every fleet-owned pull request ready for review. Draft state is reserved for the separately documented upstream contribution workflow while a third-party contribution is still being prepared. Creating the pull request is not a terminal handoff. Run the review status once in the foreground. Then start the bounded review wait in a background process. Request a review on every push. Confirm it covers the current head SHA and the full diff rather than only part of it. Triage every finding, including low-confidence findings collapsed into the review body rather than threads. Reply to and resolve every addressed finding. Repeat after every fix until the checks are green and the current-head review leaves no finding open. Only an explicit maintainer instruction may stop, defer, or alter this default. Silence or a request that says only "open a PR" is not such an instruction. Never merge on a green or CLEAN merge state alone. That state does not prove the review covered the current head SHA and full diff. It also does not expose unanswered low-confidence findings that opened no thread. This is packaged as the `pr-review-conduct` Skill at `.agents/skills/pr-review-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the merge gate, the expected loop, and how a finding is closed. @@ -193,8 +215,8 @@ This section and [`WORKFLOW.md`](./WORKFLOW.md) keep the full rules, this sectio - **Action pinning**: pin **every** action, first-party (`actions/*`) and third-party alike, to a commit SHA with a trailing `# vX.Y.Z` comment, so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. Use `# vX` (major-only) only when the upstream's floating major tag doesn't correspond to a specific patch/minor release SHA, since pinning to the floating-tag SHA still gives the SHA guarantee, the version comment just records the major line. Documented exception (no SHA pin at all): `dotnet/nbgv` is consumed via `@master` because the upstream tag stream lags `master` substantially and Dependabot's tag-tracking would propose a downgrade. **This applies to repo-owned build-layer leaves too**, since a leaf owning its build specifics is not a reason to use floating tags, and Dependabot still bumps SHA pins (updating the SHA + version comment). - **Filename**: reusable workflows (those with `on: workflow_call`) end in `-task.yml`. Entry-point workflows (`on: push` / `pull_request` / `schedule` / `workflow_dispatch`) do NOT use the `-task` suffix. They end with what they do: `-pull-request.yml`, `-release.yml`, etc. The suffix carries semantic meaning: a `-task.yml` file is meant to be `uses:`-d, never triggered directly. - **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`), and entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee. -- **Job and step `name:` suffixes**: every job's `name:` ends in **"job"** and every step's `name:` ends in **"step"**, including the PR-gate aggregator, whose `name:` is a required-status-check `context:` in a branch ruleset (`Check pull request workflow status job` in `test-pull-request.yml`). A ruleset-bound job's `name:` and its ruleset `context:` are the **same string**: rename them **together**, updating the live ruleset and `repo-config/{develop,main}.json` in lockstep with the job `name:`, never one without the other, or required-status-check enforcement silently breaks. There is no un-suffixed exception. -- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exceptions** (both record the rationale inline in their header comment): (1) [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because the merge-bot's job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order, because cancellation would leave auto-merge in an inconsistent state. (2) The publisher workflow uses both a **global, ref-independent group** (`group: ${{ github.workflow }}`, dropping the usual `-${{ github.ref }}`) and `cancel-in-progress: false`. It publishes shared ref-independent artifacts (both branches' Docker tags/caches and GitHub releases) on schedule/dispatch regardless of the triggering ref, so a ref-scoped group would let a scheduled run (ref `main`) and a manual dispatch (ref `develop`) run concurrently and double-push, and cancelling a publish mid-flight can leave a partially pushed tag set or a half-created release. The global group + queueing serializes every publish run to completion. +- **Job and step `name:` suffixes**: every job's `name:` ends in **"job"** and every step's `name:` ends in **"step"**, including the PR-gate aggregator, whose `name:` is a required-status-check `context:` in a branch ruleset (`Check pull request workflow status job` in `test-pull-request.yml`). A ruleset-bound job's `name:` and its ruleset `context:` are the **same string**: rename them **together**, updating the live ruleset and the hub's `repo-config/` payloads in lockstep with the job `name:`, never one without the other, or required-status-check enforcement silently breaks. There is no un-suffixed exception. +- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exceptions** (both record the rationale inline in their header comment): (1) [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because the merge-bot's job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order, because cancellation would leave auto-merge in an inconsistent state. (2) `.github/workflows/publish-release.yml` uses both a **global, ref-independent group** (`group: ${{ github.workflow }}`, dropping the usual `-${{ github.ref }}`) and `cancel-in-progress: false`. It publishes shared ref-independent artifacts (both branches' Docker tags/caches and GitHub releases) on schedule/dispatch regardless of the triggering ref, so a ref-scoped group would let a scheduled run (ref `main`) and a manual dispatch (ref `develop`) run concurrently and double-push, and cancelling a publish mid-flight can leave a partially pushed tag set or a half-created release. The global group + queueing serializes every publish run to completion. - **Shells**: every bash surface, a multi-line `run:` block and every committed `.sh` script alike, starts with `set -Eeuo pipefail`: fail fast, fail on undefined vars, fail on a failed pipe segment, and let an `ERR` trap inherit into functions, subshells, and command substitutions (`-E`). The `-E` is defense in depth: the fleet ships no `ERR` trap today, so a script that later adds one inherits the behavior instead of silently losing it. - **Conditionals**: multi-line `if:` uses folded scalar `if: >-` so YAML preserves whitespace correctly. Literal block (`if: |`) is wrong because it embeds newlines inside the boolean expression. - **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks, since one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans, and `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms: `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`. @@ -207,67 +229,39 @@ This section and [`WORKFLOW.md`](./WORKFLOW.md) keep the full rules, this sectio ### Running the Linters Locally (Known-Working Invocations) -CI runs the full lint set, but run the linters locally before pushing to catch issues early, so an agent must know how to invoke them. Their non-Docker install paths (curl-pipe installers, global npm) are frequently blocked in sandboxes or fail on WSL, so **prefer the Docker invocations below, the known-working path that needs no local toolchain.** These tools auto-discover their targets from the working directory. +CI runs the full lint set, but run the linters locally before pushing to catch issues early, so an agent must know how to invoke them. Their non-Docker install paths (curl-pipe installers, global npm) are frequently blocked in sandboxes or fail on WSL, so **prefer the Docker invocations below, the known-working path that needs no local toolchain.** These tools auto-discover their targets from the working directory. None of markdownlint, cspell, actionlint, editorconfig-checker, shellcheck, shfmt, or PSScriptAnalyzer is installed natively on a fleet host, by decision, so a `command -v ` miss means exactly that and not that the check is unavailable here: read this section for the working invocation before reporting a check as skipped or unavailable. **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): -- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** the same way for a repo that carries `.sh` files, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (neither one has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. -- **The `.husky/pre-commit` hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to what the commit changes rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out, since it resolves a same-owner pin against the GitHub API and a hook that needs a network fails offline. A repo enables the hook per clone with `git config core.hooksPath .husky`, and CI remains the authoritative run either way. -- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks. +- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** and **shfmt** the same way for a repo that carries a shell script, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (none of the three has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. +- **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Two catalog snippets carry the canonical shape, `catalog/snippets/husky/` (Husky.Net, for .NET or any project including Python) and `catalog/snippets/pre-commit/` (the Python `pre-commit` framework, for a repo with no `.husky/` tree), each carrying a copy of `catalog/snippets/hub-fetch-run.py` alongside it. +- **The hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff and the repo's type checker for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to the working tree diff against `HEAD` rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. That scope is the working tree rather than the staged index, so a partially staged file is judged on all of its edits, not only the staged ones, and CI re-checks the whole tree regardless, which is what makes that scope affordable in a hook. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out of the hook regardless, since it resolves a same-owner pin against the GitHub API. +- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the doc gates make, one per fetched script. A Python repo's `uvx`-run ruff and type checker can also reach the network, to resolve `@latest` on a cache miss or refresh, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`. The Husky.Net snippet needs one more step per clone, `dotnet tool restore` then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. CI remains the authoritative run either way. +- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks, plus `Lint: Prose` and `Lint: EOL`, the same two hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. -The Docker invocations below are the same ones the VS Code tasks use, for ad-hoc or headless (agent) runs. +The Docker invocations below run the same tools and configs as the VS Code tasks. Their headless form separates the image pull and minimizes repository exposure for an agent executor. -**Restricted executors keep tool state in a task-specific writable temporary directory.** Set each tool's own cache variable, such as `UV_CACHE_DIR` and `RUFF_CACHE_DIR`, instead of changing `HOME` or an agent configuration directory. A sandbox denial is not a lint result. Preserve the denial, then rerun the required command through the executor's scoped approval mechanism. Network approval covers a package or image fetch. Host approval covers access to the Docker socket. Use a narrow reusable command prefix when the executor supports one, and report the approved rerun as the evidence. +**Restricted executors keep tool state in a task-specific writable temporary directory.** Set each tool's own cache variable, such as `UV_CACHE_DIR` and `RUFF_CACHE_DIR`, instead of changing `HOME` or an agent configuration directory. A sandbox denial is not a lint result. Preserve the denial, then rerun the required command through the executor's scoped approval mechanism. Network approval covers any required fetch, including an image or package download. Host approval covers access to the Docker socket. Repository-exposure approval covers letting third-party image code read the checkout, even through a read-only mount. Persist approval only when the executor constrains the read-only mount, disabled networking, and resolved digest together. Never allow an unconstrained `docker run` prefix. PSScriptAnalyzer's separate module-install phase gets network approval without any repository mount. Report the approved rerun as the evidence. -- **editorconfig-checker** (line endings + charset across the tree): +Agent-specific authorization stays separate from the executor-neutral contract above: - ```sh - docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest - ``` +- **Codex:** execution rules match exact argument prefixes, so they cannot safely cover changing worktree paths and digests. Smart Approvals can therefore request repository-exposure approval per task. The no-prompt alternative combines `sandbox_mode = "danger-full-access"` with `approval_policy = "never"`. Use that pair only when an external sandbox contains the Codex process. It removes protection from every command rather than only lint. -- **actionlint** (GitHub Actions workflow YAML, run after any `.github/workflows/` edit, since workflow-only changes are not smoke-built): +Run the hub-hosted wrapper from the repository it checks: - ```sh - docker run --rm --pull=always -v "$PWD":/repo --workdir /repo rhysd/actionlint:latest -color - ``` +```sh +python3 /path/to/ProjectTemplate/scripts/docker_lint.py --root "$PWD" +``` - The `rhysd/actionlint` image bundles `shellcheck`, so it also validates `run:` shell blocks. The direct-binary/curl-installer path is often sandbox-blocked, so use Docker. +The wrapper discovers tracked and unignored targets before it pulls applicable images. It reports a zero-target skip without pulling or mounting the repository. It pulls each applicable image in a distinct pull phase, then resolves the pulled repository digest. A digest prevents the tag from changing between the pull and execution. It does not make third-party code trusted. -- **markdownlint-cli2** (Markdown, mirroring the davidanson VS Code extension via the shared [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc), so the CLI and IDE agree): - - ```sh - docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir davidanson/markdownlint-cli2:latest "**/*.md" - ``` - -- **cspell** (spelling in user-facing docs, with the word list and exclusions in [`cspell.json`](./cspell.json)): - - ```sh - docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md - ``` - -- **PSScriptAnalyzer** (PowerShell, the peer of the shellcheck step, **only applies to a repo that carries `.ps1` files**, which carries `PSScriptAnalyzerSettings.psd1` alongside them with the excluded rules and their reasons): - - ```sh - docker run --rm --pull=always -e PS_SCRIPTS="$(git ls-files '*.ps1')" -v "$PWD":/mnt --workdir /mnt mcr.microsoft.com/powershell:latest \ - pwsh -NoProfile -Command ' - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module PSScriptAnalyzer -RequiredVersion 1.23.0 -Force -Scope AllUsers - Import-Module PSScriptAnalyzer - $files = $env:PS_SCRIPTS -split "\s+" | Where-Object { $_ } - if (-not $files) { Write-Host "no PowerShell scripts are tracked"; exit 0 } - $found = @() - foreach ($file in $files) { $found += Invoke-ScriptAnalyzer -Path $file -Settings ./PSScriptAnalyzerSettings.psd1 } - Write-Host "Checked $($files.Count) file(s)" - if ($found) { $found | Format-Table RuleName,Severity,ScriptName,Line,Message -AutoSize | Out-String -Width 200 | Write-Host; exit 1 } - Write-Host "no findings" - ' - ``` +After all pulls, the wrapper reports that repository mounts are about to begin. Each execution uses the resolved digest, disabled networking, and a read-only checkout mount. PSScriptAnalyzer installs its pinned module in a separate container without the checkout mount. File-argument linters receive each tracked path as a distinct argument, split across bounded batches before host command-line limits become relevant. - The module version is pinned beside the image, because the image alone does not fix it and a floating install makes a local run a different check from CI. 1.23.0 rather than the newest, since 1.24.0 needs a newer `System.Management.Automation` than the image carries and fails to import after installing cleanly. The file list comes from `git ls-files` for the same reason the shellcheck step uses it, and the count is printed because a run that read no files reports the same clean as one that read them all. +Every primary Docker command has a five-minute timeout by default. Use `--timeout` to select another positive bound. The wrapper emits a start and completion line for each primary command. Timeout cleanup has a separate maximum of 30 seconds and emits its result through the failed lint step. The wrapper reports the checked-file count for every linter, including tools that produce no success output. Timeout, container failure, zero-target execution, and successful quiet completion have distinct result lines. The wrapper names each lint container and removes it after a timeout. - **The list splits on whitespace rather than on a newline, and the regex is double-quoted.** A shell joins the file list with newlines and PowerShell joins it with spaces, so a newline-only split hands the analyzer one path holding every file, which it reports as one file it cannot find followed by a clean run over nothing. The double quotes are what let the whole invocation stay inside the single-quoted `-Command` a shell passes, since PowerShell escapes with a backtick and leaves the backslash alone. Run verbatim it reports `Checked 5 file(s)` from either shell. +Use repeated `--linter` options for a subset. The supported names are `editorconfig-checker`, `actionlint`, `markdownlint`, `cspell`, `shellcheck`, `shfmt`, and `PSScriptAnalyzer`. editorconfig-checker reads the mounted tree. actionlint reads eligible workflows and includes shellcheck for `run:` blocks. markdownlint reads tracked and unignored Markdown files. CSpell reads `README.md` and `HISTORY.md` only. shellcheck, shfmt, and PSScriptAnalyzer run only when matching scripts are tracked or unignored. shellcheck and shfmt share one target list, a tracked `.sh` file plus a tracked, extension-less file whose shebang names bash or sh. - In a configured editor the davidanson extension is enough. Use the Docker CLI when there's no IDE (agent/headless) or to confirm a clean run before pushing. +In a configured editor the `DavidAnson.vscode-markdownlint` extension is enough for Markdown. Use the wrapper for a headless run or before pushing. When pulling a public image fails on a Docker-Desktop/WSL credential-helper error (`docker-credential-desktop.exe: exec format error`), retry with an empty Docker config: `DOCKER_CONFIG=$(mktemp -d) docker run ...` after writing `{}` to `$DOCKER_CONFIG/config.json`. @@ -292,7 +286,7 @@ This repository is the live `/config` directory of a running ESPHome instance, s Every repo's GitHub repository details (the About panel) follow a fixed convention so the fleet stays consistent and self-describing. -- **Description** matches the README's **tagline**, its first non-empty line after the `#` H1 heading, as plain text, stripping Markdown links (`[text](url)` and `[text][ref]` become `text`) since a description is not rendered. It is that one line and not the paragraph it opens: a README may carry further paragraphs below the tagline, and no mirror reads them. The README is the source of truth: set the description from it (`gh api -X PATCH repos// -f description=...`), never the reverse. When the current description is *more specific* than the README (a chip revision or variant the README omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the README so the description follows it. Keep the line at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. For a repo that publishes a Docker image, the **Docker Hub short description** mirrors the same tagline, so one canonical sentence carries to the README, the About panel, and Docker Hub alike. Docker Hub receives it from the About panel, which the docker-readme task reads at publish time, so an About panel left diverged from the README is carried onward rather than corrected there. +- **Description** is one canonical sentence that carries to the README, the About panel, and (for a Docker repo) the Docker Hub short description alike, at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. Once a repo declares `registry/repos.json`'s optional `description` field, that field is the source, itself link-free plain text on one line for the same reason: `repo-config/configure.sh apply` writes it to the About panel directly, and the README's **tagline** (its first non-empty line after the `#` H1 heading) follows it rather than the other way around. A repo that has not adopted the field yet keeps the pre-existing convention, where the README tagline is the source of truth and the About panel is set from it by hand (`gh api -X PATCH repos// -f description=...`). `spec/audit.py`'s `description_findings()` reports drift either way, falling back to the tagline when no field is declared. It is that one line and not the paragraph it opens: a README may carry further paragraphs below the tagline, and no mirror reads them. When the current description is *more specific* than the declared source (a chip revision or variant it omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the declared source so the other mirrors follow it. Docker Hub receives it from the About panel, which the docker-readme task reads at publish time, so an About panel left diverged from the canonical value is carried onward rather than corrected there. - **Topics** are optional, and any that are present match the repo's actual content. Do not invent topics to fill the field. - **Include in the home page**: Releases on, Deployments off, Packages off. These toggles are UI-only, since the REST and GraphQL APIs neither read nor write them, so they are set by hand and cannot be audited through `gh`. @@ -307,8 +301,7 @@ Every repo's GitHub repository details (the About panel) follow a fixed conventi - [`test/`](./test/), one minimal example device config per published template. CI compiles these against the current ESPHome release, so a published template is known to build, and they double as usage samples. - [`easystart/`](./easystart/), the Micro-Air EasyStart soft-starter project: the ESPHome external component, its protocol and reverse-engineering notes, and the standalone BLE monitor utility under `easystart/python`. - [`archive/`](./archive/), retired device configs kept for reference, neither built nor validated. -- [`repo-config/`](./repo-config/), the branch ruleset payloads and the general-settings payload, kept out of `.github/` (which is Actions-owned). The script that applies them is hub-hosted rather than carried here. -- [`spec/`](./spec/), the machine-readable secret-name ground truth this repo's audit checks against. +- This repo carries no local `repo-config/` or `spec/secrets.json`: the hub hosts the branch ruleset and settings payloads and the secret-name ground truth, and [`AUDIT.md`](./AUDIT.md) "General Settings and Rulesets" and "Secrets" check against the hub's checkout rather than a local copy. - [`host-tools.json`](./host-tools.json), what a development host needs beyond the fleet baseline, layered over it tighten-only. Empty here, because every tool this repo's procedures need is already declared at its own floor. - [`.devcontainer/`](./.devcontainer/), the offline debugging container described above. - [`.github/workflows/`](./.github/workflows/), lint plus change-gated compile tests, and the source-only release. diff --git a/OPERATIONS.md b/OPERATIONS.md index 8cdfc3c..78c7c62 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -11,7 +11,7 @@ An operational discovery is written down as part of the change that surfaced it, - **A mechanism that stopped working, and whatever replaced it**, goes in the section that owns the subject. Record the replacement and the reason the old route failed, so the next agent does not retry it. - **A procedure carried out for the first time** gets a section describing how, not that it happened. Write the recipe someone would follow, and name the command or the file rather than narrating the session. - **A fact established by running something** is recorded with what proved it, since a claim nobody can re-derive gets doubted and re-tested. Prefer the observed output over an assertion. -- **A defect in the carried fleet content** (`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, `repo-config/`, `spec/`, and `AUDIT.md`) is reported upstream rather than patched here, because a local edit registers as drift and fails the audit. Filing that report is a **cross-repository write and needs the maintainer's explicit permission for that specific repository in the current session**, per [Repository Boundaries and Write Safety][governance-write-safety]. Ask, do not assume. +- **A defect in the carried fleet content** (`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, and `AUDIT.md`, plus the hub-hosted `repo-config/` and `spec/secrets.json` this repository reaches rather than carries) is reported upstream rather than patched here, because a local edit registers as drift and fails the audit. Filing that report is a **cross-repository write and needs the maintainer's explicit permission for that specific repository in the current session**, per [Repository Boundaries and Write Safety][governance-write-safety]. Ask, do not assume. - **Nothing here is a changelog.** State the current mechanism in the present tense, per the documentation rules in `GOVERNANCE.md`. The before-and-after belongs in the commit message. - **`git blame` does not establish who wrote a line.** Agent commits carry the maintainer's `noreply` identity by policy, so blame attributes agent-authored prose to the maintainer just as it does their own. The `AGENTS.md` carve-out for Unicode the developer deliberately typed cannot be claimed from blame output, and a review flagging non-ASCII is answered by removing it rather than by defending it with an authorship claim the repository cannot support. @@ -478,17 +478,19 @@ The steps below run ESPHome outside the live instance, on a workstation, which i Sharp edges in the tooling around this repository, each one learned by tripping over it. - **Never build a GitHub comment or reply body inside a double-quoted shell string.** Write it to a file and pass `--body-file`, or `-F body=@file` on a REST call. Backticks in a double-quoted string are command substitution, so a body that mentions a path in code formatting **executes that path**. This is not theoretical: a review reply naming the ruleset apply script in code formatting **executed** it, back when this repository carried its own copy, and that script writes by default, so the posted comment came out with its code spans replaced by command output. Escaping each backtick works and is one missed backslash from repeating the incident. The hazard is the shell's, not that script's, so it survives the copy being retired. -- **The ruleset apply script writes unless told otherwise, and it is hub-hosted rather than carried here.** Run it from a hub checkout and name this repository explicitly, since it otherwise targets whichever repository the shell is sitting in. Its default mode PATCHes repository settings, toggles Dependabot features, and PUTs both branch rulesets. Pass its `check` mode for read-only validation, which is what you almost always want. The payloads it compares against are the committed [`repo-config/`][repo-config] files. +- **The ruleset apply script writes unless told otherwise, and it is hub-hosted rather than carried here.** Run it from a hub checkout and name this repository explicitly, since it otherwise targets whichever repository the shell is sitting in. Its default mode PATCHes repository settings, toggles Dependabot features, and PUTs both branch rulesets. Pass its `check` mode for read-only validation, which is what you almost always want. The payloads it compares against are the hub's own committed `repo-config/` files; this repository carries no local copy, per [AUDIT.md "General Settings and Rulesets"][audit-general-settings-and-rulesets]. - **This file uses reference-style links, and it is the one file in this repository that should not.** The fleet rule allows four agent-instruction files to keep inline links, [`AGENTS.md`][agents], [`GOVERNANCE.md`][governance], this one, and `.github/copilot-instructions.md`, on the grounds that they are read one section at a time so a definition at the foot of the file is never reached. The other three take that allowance and this one does not, which is a divergence to close deliberately rather than incidentally. Every other Markdown file in the repository is reference-style by the rule rather than by exception. Definitions live at the bottom of the file, grouped under `` and `` and alphabetized within each group. A reference name encodes what it points at, so `analog-max17048-link`, never `analog-en-products-link` after a URL path segment. Removing a link also removes its definition, since an orphan fails the lint. - **Inline HTML is limited to `
` and ``.** Those two are allowed because a collapsible has no markdown equivalent. Every other element still fails `MD033`, and that includes a `` nested inside a `` - use a markdown code span there instead. - **The spell-check gate covers `**/README.md` plus [`DEVICES.md`][devices] and `HISTORY.md`**, wider than the fleet default, so a nested README fails CI like any other. The CI workflow and the `Lint: Spelling` task carry the identical list. -- **Never mount the live checkout into a third-party lint container.** `secrets.yaml` holds the live secrets and is git-ignored. [`secrets._yaml`][secrets-example] is the tracked placeholder. Build a temporary snapshot from tracked and intended untracked files, then mount that snapshot read-only. The Git exclusion rules keep `secrets.yaml` out without maintaining a second exclusion list. +- **Never mount the live checkout into a third-party lint container.** `secrets.yaml` holds the live secrets and is git-ignored. [`secrets._yaml`][secrets-example] is the tracked placeholder. Build a temporary snapshot from tracked and intended untracked files, then mount that snapshot read-only. The Git exclusion rules keep `secrets.yaml` out without maintaining a second exclusion list. **This includes the hub's `scripts/docker_lint.py` wrapper**: its `--root` argument becomes a plain read-only bind mount with no exclusion of its own (read-only stops the container writing back, not reading `secrets.yaml`), so pass the snapshot's path as `--root` here, never `"$PWD"` on the live checkout. ```shell lint_root="$(mktemp -d /tmp/esphome-lint.XXXXXX)" git ls-files --cached --others --exclude-standard -z | tar --null -T - -cf - | tar -xf - -C "$lint_root" chmod -R a+rX "$lint_root" docker run --rm -v "$lint_root":/workdir:ro --workdir /workdir + # Or, for the hub's docker_lint.py wrapper: + python3 /path/to/ProjectTemplate/scripts/docker_lint.py --root "$lint_root" ``` - **The installed `gh` is old enough that `gh pr edit` always fails**, on every repository, and the message names a GraphQL field rather than a permission. See [The `gh` CLI Is Too Old for `gh pr edit`][gh-cli-too-old] for the cause and the REST workaround. @@ -508,6 +510,7 @@ Sharp edges in the tooling around this repository, each one learned by tripping [agents]: ./AGENTS.md [api-connection-cap]: #logs-and-the-api-connection-cap [apollo-template]: ./templates/apollo-plt-1b.yaml +[audit-general-settings-and-rulesets]: ./AUDIT.md#general-settings-and-rulesets [ble-issue]: ./easystart/ESPHOME-BLE-ISSUE.md [ble-re-playbook]: ./easystart/BLE-RE-PLAYBOOK.md [ceilsense-template]: ./templates/smarthome-ceilsense.yaml @@ -525,7 +528,6 @@ Sharp edges in the tooling around this repository, each one learned by tripping [norvi-template]: ./templates/norvi-enet-ae06-r.yaml [office-bluetooth-proxy]: ./office-bluetooth-proxy.yaml [readme]: ./README.md -[repo-config]: ./repo-config/ [repository-tooling-hazards]: #repository-tooling-hazards [rgb-led-status]: #rgb-led-status [rgb-led-status-template]: ./templates/rgb-led-status.yaml diff --git a/WORKFLOW.md b/WORKFLOW.md index e1c7868..1ebe89e 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -1,6 +1,6 @@ # WORKFLOW.md -The guide for CI/CD **workflows** (GitHub Actions): a deliberate mixture of code style, architecture, a **behavioral contract** (expected inputs and outputs), and a **test methodology**. Code style lives in [`CODESTYLE.md`][codestyle]. This file is its sibling for everything under [`.github/workflows/`][workflows]. +The guide for CI/CD **workflows** (GitHub Actions): a deliberate mixture of code style, architecture, a **behavioral contract** (expected inputs and outputs), and a **test methodology**. Code style lives in [`CODESTYLE.md`][codestyle]. This file is its sibling for everything under `.github/workflows/`. Its defining principle: **it describes required outcomes, not a required implementation.** Two repos may implement the same guarantee with different YAML. A workflow is correct when it **satisfies the contract** in section 4 and is **defect-free against the expected inputs and outputs**, not when it matches a reference implementation byte for byte. The conventions in section 2 keep workflows legible. The contract in section 4 is what they must *do*. @@ -64,7 +64,7 @@ flowchart LR The direct commit is an **allowance, not a substitute for review**. The ruleset drops the pull-request *requirement*, which permits a direct push without withdrawing the pull request, so a change worth reviewing still takes one and both paths reach `develop` legally. Which changes those are is stated as a shape rather than a line count in [GOVERNANCE.md "Operational Repositories"][governance-operational-repositories], which owns the test and is the one place it is written, since nothing in a ruleset can apply it. What differs is when validation lands. On the direct-commit path the commit is already on the branch, so CI can only be advisory after the fact, and that is the accepted cost of the model. On the pull-request path the change has not landed, so validation is pre-merge and actionable, which is the moment it is worth the most, and the lint workflow's `pull_request` trigger therefore names `develop` alongside `main` (Section 6). That is what makes **D1.2** hold here, since its input is *any* PR and the operational model is no exception. The check is reported on a `develop` PR rather than required, because a required status check on `develop` binds the direct push too and would dissolve the allowance the model is built on. -Their CI is lint/validation only (editorconfig/EOL plus domain linters such as Home Assistant or ESPHome config validation or a firmware build, but **no unit tests**), so the D-guarantees below that assume a build/test pipeline are **N/A** exactly as for `source-only` (Section 6). What binds: the promotion gate, where the `develop -> main` PR must pass the required `Check pull request workflow status job`, and the source-only release on manual dispatch (`releaseTrigger: dispatch-only`; tag + source zip). Branch-model rulesets are specified in [GOVERNANCE.md "Branching Model"][governance-branching-model] and [repo-config/README.md][repo-config-readme], not here. +Their CI is lint/validation only (editorconfig/EOL plus domain linters such as Home Assistant or ESPHome config validation or a firmware build, but **no unit tests**), so the D-guarantees below that assume a build/test pipeline are **N/A** exactly as for `source-only` (Section 6). What binds: the promotion gate, where the `develop -> main` PR must pass the required `Check pull request workflow status job`, and the source-only release on manual dispatch (`releaseTrigger: dispatch-only`; tag + source zip). Branch-model rulesets are specified in [GOVERNANCE.md "Branching Model"][governance-branching-model], not here. ### Two Layers: Orchestration vs Build @@ -149,7 +149,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input - **D1.3 Smoke never publishes and never uploads.** Input: `smoke: true`. Output: full compile/lint/test, but no registry/image push, no release, and **no** artifact uploads (every `upload-artifact`, including any aggregation job, is gated `!smoke`). *Prevents: a PR publishing, and orphaned artifacts churning the storage quota.* - **D1.4 Workflow-file changes are not smoke-built.** Input: a PR changing only `.github/workflows/**`. Output: the paths-filter excludes workflow files, so smoke-build skips. *Implication: a workflow-only change is not smoke-built, but actionlint still validates it in CI.* - **D1.5 One required aggregator gates merge.** Input: any PR. Output: a single aggregator job must **succeed**, `needs:` the changes job and the validation job, treat a **skipped** smoke build as pass, and **block** on `failure`/`cancelled`. Its name is ruleset-bound: the job `name:` and the ruleset `context:` are the same string and MUST be renamed together, never independently. *Prevents: a paths-filter error letting a target-changing PR merge unbuilt.* -- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --collect:"XPlat Code Coverage"` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate). `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests (see `spec/secrets.json` `typeMechanisms`). The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR (a distinct knob from `fail_ci_if_error`, which only guards the upload step), and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`. A repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact, so `.gitignore` excludes it (e.g. `coverage/` and `*.cobertura.xml`, with `.gitignore` the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported; a stale, unused token; a coverage regression blocking an unrelated PR; a coverage artifact committed by a blanket add.* +- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --collect:"XPlat Code Coverage"` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate). `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests. Where this guarantee does not apply (a `lint-only` profile for that type, per `registry/repos.json`), the hub's `spec/secrets.json` `typeMechanisms` mapping is not claimed for that repo, and the absence is not drift. The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR (a distinct knob from `fail_ci_if_error`, which only guards the upload step), and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`. A repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact, so `.gitignore` excludes it (e.g. `coverage/` and `*.cobertura.xml`, with `.gitignore` the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported; a stale, unused token; a coverage regression blocking an unrelated PR; a coverage artifact committed by a blanket add.* ### D2 - Input/State Validation at Entry @@ -172,7 +172,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input - **D4.2 Tag the built commit.** Output: the release `target_commitish` is the built commit's SHA (NBGV's `GitCommitId`), never a branch name or a separately re-resolved ref. *Prevents: the tag landing on the default branch instead of the built tree.* - **D4.3 Release contents.** Output: every release contains a tag on the built commit plus the auto source zip, README, and LICENSE. File targets attach `release-asset-*`. The `prerelease` value equals `branch != default`. A no-file-target caller sets `expect_release_assets: false` to reach the tag-only shape. This applies to Docker-only, PyPI-only, and source-only repos. The setting relaxes `fail_on_unmatched_files` and skips the asset download. The release-create step fails when no assets exist and the setting retains its default `true`. A source-only caller also sets every `enable_*` input false. - **D4.4 No-op republish.** Input: a re-run whose version is unchanged. Output: nothing is re-pushed, because the release-create step is skipped when the tag exists (refreshed only on `workflow_dispatch`), and the paired asset-delete is skipped with it. Registry pushes are no-ops. The NuGet/PyPI publish steps are **not** statically gated on existence. They run and the **server** dedupes (`dotnet nuget push --skip-duplicate` turns a 409 into success, and PyPI does the same under `skip-existing: true`). **Docker always re-pushes** the image (base-image refresh), independently of the release-create skip, within the same run. *Prevents: duplicate releases and wasted pushes.* -- **D4.5 A build failure blocks every publish target.** Input: a real publish where one enabled build fails. Output: nothing publishes. `github-release` needs every build, so a failed build skips it (no tag, no release), and the terminal registry pusher (Docker) needs every other build and guards its `if` with `!failure() && !cancelled()`, so a failed build skips docker too (no image push) while a disabled or unchanged target (skipped, not failed) still lets docker build on smoke. *Prevents: a partial publish, e.g. a Docker image pushed while .NET publish failed and no release was cut.* A repo pushing two registry targets at once would need a build/publish split behind an all-builds gate, which none does today. +- **D4.5 A build failure blocks every publish target.** Input: a real publish where one enabled build fails. Output: nothing publishes. `github-release` needs every build, so a failed build skips it (no tag, no release), and the terminal registry pusher (Docker) needs every other build and guards its `if` with `!failure() && !cancelled()`, so a failed build skips docker too (no image push) while a disabled or unchanged target (skipped, not failed) still lets docker push. *Prevents: a partial publish, e.g. a Docker image pushed while .NET publish failed and no release was cut.* A repo pushing two registry targets at once would need a build/publish split behind an all-builds gate, which none does today. - **D4.6 Deploy verification names the release.** Input: a deploy to a filesystem on a host the project owns that completes without error. Output: a check against the running host asserts **which release is answering**, not merely that it answers. The artifact stamps its own version into the configuration it ships, and the check compares that against the version just installed, **waiting for convergence to a bounded timeout** rather than sampling once, because content goes live the instant a pointer moves while server rules wait on an asynchronous reload. The same check asserts **which environment** answered, since several environments serve a byte-identical artifact and a proxy rule aimed at the wrong one answers healthily under the right hostname. An unreachable host is reported distinctly from an HTTP status. *Prevents: a green deploy over a host still serving the previous release's configuration, a URL contract checked against the wrong environment, and a dead config watcher read as a routing fault.* ### D5 - Resource Cleanup @@ -290,13 +290,8 @@ Each type maps the *applicable* S-scenarios onto its targets. The differences ar - **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, binding the same `environment:` on the caller's own job so the one crossing secret, `DEPLOY_SSH_PRIVATE_KEY`, resolves from the GitHub Environment store and can be mapped explicitly rather than through `secrets: inherit`, which a cross-repository reusable workflow cannot use. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. - **Operational (workflow model, not a build target).** A `workflowModel: operational` repo layers direct commits to `develop` onto the **source-only** release shape above. It has two workflows. The first is a **lint/validation** PR workflow that feeds the required `Check pull request workflow status job`. It uses the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator, with **no unit tests**. Examples include Home Assistant `hass --script check_config`, `esphome config`, or a firmware build. Its triggers differ from the `release` model. It runs on pushes to `develop`, pull requests to `[ main, develop ]`, and `workflow_dispatch`. Push validation is advisory. Pull request validation is enforced on `main` and reported but not required on `develop`. The second workflow is the standard **source-only publisher** with `releaseTrigger: dispatch-only`. NBGV and `version.json` own the tag. The reusable release task creates tag + source zip + README + LICENSE. **The PR trigger names both branches, and naming `main` alone is a defect.** Omitting `develop` starts no validation when a PR opens against `develop`. The aggregator then never reports, and the PR appears clean with an empty check list. D1.2 forbids that output. Naming both causes a duplicate run after a PR merge. The change validates on the PR and again on the resulting push, regardless of merge method. The operational `develop` ruleset prescribes no merge method. The concurrency group uses the workflow name plus `${{ github.ref }}` (Section 2). A pull request uses `refs/pull//merge`, while its push uses `refs/heads/develop`. The runs occupy different groups and neither cancels the other. Pay that cost. The lint-only gate costs only a few runner-minutes. Suppressing the push requires distinguishing a merge commit from a direct commit, which restores the ambiguity the trigger set removes. S1 applies to every PR, including promotion and `develop` PRs. The source-only S7, S8, S9, and S10 scenarios also apply. Bot-push and schedule paths in S5/S6 are N/A, as are every build and registry scenario. See the branch-model note in Section 3 and [GOVERNANCE.md "Branching Model"][governance-branching-model]. - - -[workflows]: ./.github/workflows/ - [codestyle]: ./CODESTYLE.md [governance-branching-model]: ./GOVERNANCE.md#branching-model [governance-operational-repositories]: ./GOVERNANCE.md#operational-repositories -[repo-config-readme]: ./repo-config/README.md diff --git a/repo-config/README.md b/repo-config/README.md deleted file mode 100644 index 2d57aa1..0000000 --- a/repo-config/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# repo-config - -Repository and branch configuration held as committed files, kept out of `.github/` (which holds the GitHub-consumed configuration - workflows, Dependabot). This mirrors the layout the fleet repos use. - -- `main.json` plus one `develop` variant, the branch rulesets as the writable API subset (`name`, `target`, `enforcement`, `conditions`, `rules`). The `develop` payload is `develop.json` (`release` repos) or `operational/develop.json` (`operational` repos), and this repo is operational, so it carries `operational/develop.json`. These are the canonical expected payloads that the audit (`AUDIT.md`) diffs the live rulesets against. `bypass_actors` is deliberately outside the subset: who may bypass a ruleset is a human decision taken in the UI, so no payload declares one and the tooling preserves whatever is live. -- `operational/develop.json`, the `develop` ruleset for **operational** repos (registry `workflowModel: operational`), taking direct signed pushes with no PR gate. Present in operational carries only, since a `release` repo does not have it. Read the dropped rules as an allowance rather than a prohibition, since a PR into `develop` remains legal and its lint result is reported rather than required. See "Rulesets" below. - -The script that applies these payloads is **hub-hosted and run from a hub checkout, not carried here**. It holds nothing per-repo and is one copy for the fleet, and it resolves every payload path against its own directory rather than against the target repo, so a run compares this repo against the hub's payloads and never reads the copies above. Name the target repository explicitly, since the command otherwise defaults to whichever repository the shell is sitting in. Two modes over the GitHub API: `apply` creates-or-updates the settings, the Dependabot security features, and the rulesets idempotently, and `check` is the read-only inverse that exits non-zero on drift. - -## Rulesets - -Two workflow models share `main.json` but differ on `develop` (registry `workflowModel`, default `release`): - -- **`release`** (`develop.json`): `develop` requires squash merges with linear history and a PR - the feature-branch pipeline. -- **`operational`** (`operational/develop.json`): `develop` takes **direct signed pushes**, carrying only `deletion`, `non_fast_forward`, and `required_signatures`, with no PR, no status-check, and no Copilot-on-push. CI runs on the push as advisory feedback. This is for live-service config repos that edit `develop` directly and promote a known-good snapshot to `main` via an occasional PR (see [GOVERNANCE.md "Branching Model"][governance-branching-model]). - -`main` (both models) requires merge-commit merges (no linear-history rule), signed commits, a passing `Check pull request workflow status job`, resolved review threads, and Copilot review, and blocks force-pushes and deletion, so a `develop -> main` promotion is always gated even when `develop` takes direct commits. Every ruleset intentionally leaves "Require branches to be up to date before merging" **off**, per [GOVERNANCE.md "Branching Model"][governance-branching-model]. - -**Configure by importing these JSON files, never by hand-building the rules** (hand reconstruction has gone wrong on past setups). The result must be **exactly two rulesets named `develop` and `main`**, since the names are load-bearing (`GOVERNANCE.md` and the workflows reference them), and only the `develop` *content* varies by model. First remove all legacy classic branch-protection rules and any stray rulesets, then run the hub's apply script from a hub checkout naming this repository (it picks the `develop` payload from the repo's `workflowModel`), or `gh api -X POST repos///rulesets --input repo-config/.json` per file (operational repos use `operational/develop.json` for `develop`). `gh ruleset` is read-only, so creation goes through `gh api`. The required check binds by name and only turns green after the repo's PR workflow runs once. To edit a ruleset, GET it, change the field, and PUT the whole writable subset back (a partial PUT `422`s). - -To change the canonical rulesets, edit the live rulesets (fleet-wide changes happen at the hub), then regenerate the committed files from the current repo: - -```sh -repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" -# Paginate so a name match on a later page is never missed - the same trap the hub's apply script guards against. -# --paginate with --jq '.[]' emits one JSON object per ruleset across all pages; jq -s re-assembles them -# into the single array the selections below expect. -rulesets=$(gh api --paginate "repos/$repo/rulesets" --jq '.[]' | jq -s '.') -for name in develop main; do - out="repo-config/$name.json" - # An operational carry keeps its develop payload at operational/develop.json (develop.json is absent). - [ "$name" = "develop" ] && [ ! -f "$out" ] && out="repo-config/operational/develop.json" - # Exactly one ruleset per name: zero or duplicates is declared drift - fail loudly, never regen from a guess. - count=$(jq --arg n "$name" '[.[] | select(.name==$n)] | length' <<<"$rulesets") - [ "$count" -eq 1 ] || { echo "expected exactly 1 ruleset named $name, found $count (drift)" >&2; exit 1; } - id=$(jq --arg n "$name" '.[] | select(.name==$n) | .id' <<<"$rulesets") - # bypass_actors is deliberately not projected, matching what the payloads declare. - # Including it would write the live bypass list back into the committed file and reintroduce - # the very field these payloads stopped carrying, so a regeneration must not restore it. - gh api "repos/$repo/rulesets/$id" \ - --jq '{name, target, enforcement, conditions, rules}' \ - | jq -S --indent 4 '.' > "$out" -done -``` - -## Secrets - -Publish credentials required per mechanism are enumerated in `spec/secrets.json`. A repo needs only the mechanisms its own publish targets use - a source-only repo needs none of the publish credentials below. NuGet and PyPI use keyless OIDC Trusted Publishing (no stored key; the publish job needs `id-token: write`, and PyPI additionally an `environment: pypi` gate). Docker Hub has no OIDC equivalent and uses a stored `DOCKER_HUB_USERNAME` + `DOCKER_HUB_ACCESS_TOKEN` in both the Actions and Dependabot secret stores. Codegen and merge-bot repos add a GitHub App (`CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` in both stores; the app must be installed, not just created). App-token call sites use `client-id`, never the deprecated `app-id`. - -## Repo Settings - -The fleet-standard general settings live in [`settings.json`][settings-json] and are applied idempotently by the hub's apply script alongside the rulesets (`gh api PATCH /repos/{owner}/{repo}`). The two settings that depend on per-repo state, `has_discussions` (visibility) and `default_branch` (main-must-exist), are computed at apply time rather than stored in the file. - -- **Default branch `main`** (the script sets it only when a `main` branch exists, never pointing the default at a missing branch). -- **Merge methods**: `Allow merge commits` and `Allow squash merging` on, **rebase off** - each branch ruleset then picks its method (merge on `main`, squash on `develop`). -- **Auto-merge on** (the merge-bot needs it) and **`Always suggest updating pull request branches` on**. -- **`Automatically delete head branches` is OFF, deliberately.** With it on, a `develop -> main` promotion (whose PR head is `develop`) would delete `develop`. There is no per-branch exemption, so the repo-wide toggle stays off to protect `develop`. **The CLI has the same trap: never `gh pr merge --delete-branch` a promotion PR whose head is `develop`**, since the explicit flag deletes `develop` regardless of this setting (see [GOVERNANCE.md "Branching Model"][governance-branching-model]). -- **Wikis and Projects off. Discussions on public repos only** (off on private). **Sponsorships off** - the button is driven by `.github/FUNDING.yml`, not a REST toggle, and the fleet ships none. -- **Actions / General**: allow GitHub Actions to create and approve pull requests (for the bots). - -## Brownfield Migration (Maintainer Only) - -`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] forbids agents from running. **An agent must never execute it, and must surface it to the maintainer instead**. Greenfield repos where signing is live before the first commit never hit this. - - - -[governance-branching-model]: ../GOVERNANCE.md#branching-model -[governance-git-and-commit-rules]: ../GOVERNANCE.md#git-and-commit-rules -[settings-json]: ./settings.json diff --git a/repo-config/main.json b/repo-config/main.json deleted file mode 100644 index a99ed24..0000000 --- a/repo-config/main.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "conditions": { - "ref_name": { - "exclude": [], - "include": [ - "refs/heads/main" - ] - } - }, - "enforcement": "active", - "name": "main", - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "required_signatures" - }, - { - "parameters": { - "allowed_merge_methods": [ - "merge" - ], - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_approving_review_count": 0, - "required_review_thread_resolution": true, - "required_reviewers": [] - }, - "type": "pull_request" - }, - { - "parameters": { - "do_not_enforce_on_create": false, - "required_status_checks": [ - { - "context": "Check pull request workflow status job", - "integration_id": 15368 - } - ], - "strict_required_status_checks_policy": false - }, - "type": "required_status_checks" - }, - { - "parameters": { - "review_draft_pull_requests": true, - "review_on_push": true - }, - "type": "copilot_code_review" - } - ], - "target": "branch" -} diff --git a/repo-config/operational/develop.json b/repo-config/operational/develop.json deleted file mode 100644 index 7657ac1..0000000 --- a/repo-config/operational/develop.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "conditions": { - "ref_name": { - "exclude": [], - "include": [ - "refs/heads/develop" - ] - } - }, - "enforcement": "active", - "name": "develop", - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "required_signatures" - } - ], - "target": "branch" -} diff --git a/repo-config/settings.json b/repo-config/settings.json deleted file mode 100644 index f0f3a2f..0000000 --- a/repo-config/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "has_wiki": false, - "has_projects": false, - "allow_merge_commit": true, - "allow_squash_merge": true, - "allow_rebase_merge": false, - "allow_auto_merge": true, - "allow_update_branch": true, - "delete_branch_on_merge": false -} diff --git a/spec/secrets.json b/spec/secrets.json deleted file mode 100644 index 9179b8f..0000000 --- a/spec/secrets.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "note": "Repo-scoped adaptation of the fleet hub's canonical spec/secrets.json, per the repo-config downstream carry. This repo is source-only with a github-release publish target (mechanism none), so only the fleet baseline applies: no publish-mechanism secrets. targetMechanisms and mechanisms are omitted deliberately rather than carried empty: the audit enumerates baseline plus mechanisms and never reads targetMechanisms, so an all-null routing map with no mechanisms beside it is dead weight that reads as a schema bug. AUDIT.md cross-checks these names (values are never read).", - "baseline": { - "requires": ["CODEGEN_APP_CLIENT_ID", "CODEGEN_APP_PRIVATE_KEY"], - "forbids": ["CODEGEN_APP_ID"], - "workflowNeeds": ["actions/create-github-app-token"], - "stores": ["actions", "dependabot"], - "note": "The App-token secrets power the App-signed merge-bot (auto-merge that re-triggers downstream workflows), which every fleet repo runs; also consumed by codegen and the upstream-version tracker where present. Used via actions/create-github-app-token with the client-id input (not the deprecated app-id). The CODEGEN_* name is historical, not codegen-specific." - } -} From 3ffc7cc3aef996373a1cb3ad4551846d0853c1b7 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 29 Aug 2026 10:44:43 -0700 Subject: [PATCH 08/11] Reformat pull-apk.sh to satisfy shfmt (#117) Pre-existing failure on develop, unrelated to the hub resync: mvdan/shfmt:latest expects 4-space indentation this script's brace-block error handlers didn't have. Confirmed independent of .editorconfig (reproduces against develop's pre-resync copy too). --- easystart/tools/pull-apk.sh | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/easystart/tools/pull-apk.sh b/easystart/tools/pull-apk.sh index 2bdefb0..ad1d5f6 100755 --- a/easystart/tools/pull-apk.sh +++ b/easystart/tools/pull-apk.sh @@ -20,14 +20,17 @@ OUTDIR="${2:-.}" clean() { tr -d '\r' | sed 's/^package://'; } [ "$(adb get-state 2>/dev/null || true)" = "device" ] || { - echo "no authorized adb device - plug in the phone, enable USB debugging, and approve the" \ - "on-screen prompt (run 'adb devices' to check)." >&2 - exit 1 + echo "no authorized adb device - plug in the phone, enable USB debugging, and approve the" \ + "on-screen prompt (run 'adb devices' to check)." >&2 + exit 1 } # Resolve the package name (exact, or first substring match). matches=$(adb shell pm list packages | clean | grep -iF -- "$QUERY" || true) -[ -n "$matches" ] || { echo "no installed package matching '$QUERY'" >&2; exit 1; } +[ -n "$matches" ] || { + echo "no installed package matching '$QUERY'" >&2 + exit 1 +} count=$(printf '%s\n' "$matches" | grep -c .) PKG=$(printf '%s\n' "$matches" | head -1) [ "$count" -eq 1 ] || echo "note: $count packages match '$QUERY'; using '$PKG'" >&2 @@ -44,21 +47,24 @@ echo "version: $VNAME (code $VCODE)" # APK path(s) - more than one means a split APK. PATHS=() while IFS= read -r line; do - [ -n "$line" ] && PATHS+=("$line") + [ -n "$line" ] && PATHS+=("$line") done < <(adb shell pm path "$PKG" | clean) -[ "${#PATHS[@]}" -gt 0 ] || { echo "no APK paths returned for $PKG" >&2; exit 1; } +[ "${#PATHS[@]}" -gt 0 ] || { + echo "no APK paths returned for $PKG" >&2 + exit 1 +} mkdir -p "$OUTDIR" BASE="${PKG}-${VNAME}-${VCODE}" if [ "${#PATHS[@]}" -eq 1 ]; then - DEST="$OUTDIR/${BASE}.apk" - adb pull "${PATHS[0]}" "$DEST" >/dev/null - echo "pulled: $DEST" + DEST="$OUTDIR/${BASE}.apk" + adb pull "${PATHS[0]}" "$DEST" >/dev/null + echo "pulled: $DEST" else - DEST="$OUTDIR/${BASE}" - mkdir -p "$DEST" - for p in "${PATHS[@]}"; do adb pull "$p" "$DEST/" >/dev/null; done - echo "pulled ${#PATHS[@]} split APKs to: $DEST/" - echo "(merge with apkeditor/apktool if a single APK is needed)" + DEST="$OUTDIR/${BASE}" + mkdir -p "$DEST" + for p in "${PATHS[@]}"; do adb pull "$p" "$DEST/" >/dev/null; done + echo "pulled ${#PATHS[@]} split APKs to: $DEST/" + echo "(merge with apkeditor/apktool if a single APK is needed)" fi From ad87ba809eb6b35af1c058848e2aaf3c910bf251 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 29 Aug 2026 11:01:43 -0700 Subject: [PATCH 09/11] Bump hub reusable-workflow pins to 2.0.518 and fix a semicolon (#118) The pinned validate-task.yml/merge-bot-task.yml/publish-plan-task.yml/ build-release-task.yml SHAs (2.0.483) predate the hub commit that added spec/secrets.json to prose_lint.py's HUB_HOSTED dead-path exemption (ptr727/ProjectTemplate#1030, 7787662), so every mention of that now-retired path in AUDIT.md/GOVERNANCE.md/OPERATIONS.md/ WORKFLOW.md fails the Lint sources job's dead-path check. Bumps to the current hub main (2.0.518), which includes the exemption. Also fixes a semicolon in OPERATIONS.md the same check flagged. --- .github/workflows/merge-bot-pull-request.yml | 2 +- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-pull-request.yml | 2 +- OPERATIONS.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 359dac0..61bcac0 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -19,7 +19,7 @@ jobs: merge-bot: name: Merge bot pull request job - uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 + uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@32582848ffb0505e1044e5552b5ed94c32500c57 # 2.0.518 secrets: CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }} CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e68b0c9..08adcaf 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: plan: name: Plan release job needs: [assert-dispatch-ref] - uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 + uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@32582848ffb0505e1044e5552b5ed94c32500c57 # 2.0.518 with: event_name: ${{ github.event_name }} actor: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@32582848ffb0505e1044e5552b5ed94c32500c57 # 2.0.518 permissions: contents: read @@ -46,7 +46,7 @@ jobs: name: Publish project release job needs: [plan, validate] if: ${{ needs.plan.outputs.publish == 'true' && needs.validate.result == 'success' }} - uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 + uses: ptr727/ProjectTemplate/.github/workflows/build-release-task.yml@32582848ffb0505e1044e5552b5ed94c32500c57 # 2.0.518 permissions: contents: write actions: write diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index c298400..6e8f4b8 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -32,7 +32,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@a33d7d77ffa7f6af52cddce01f7250be282546ed # 2.0.483 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@32582848ffb0505e1044e5552b5ed94c32500c57 # 2.0.518 permissions: contents: read diff --git a/OPERATIONS.md b/OPERATIONS.md index 78c7c62..b1f20ab 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -478,7 +478,7 @@ The steps below run ESPHome outside the live instance, on a workstation, which i Sharp edges in the tooling around this repository, each one learned by tripping over it. - **Never build a GitHub comment or reply body inside a double-quoted shell string.** Write it to a file and pass `--body-file`, or `-F body=@file` on a REST call. Backticks in a double-quoted string are command substitution, so a body that mentions a path in code formatting **executes that path**. This is not theoretical: a review reply naming the ruleset apply script in code formatting **executed** it, back when this repository carried its own copy, and that script writes by default, so the posted comment came out with its code spans replaced by command output. Escaping each backtick works and is one missed backslash from repeating the incident. The hazard is the shell's, not that script's, so it survives the copy being retired. -- **The ruleset apply script writes unless told otherwise, and it is hub-hosted rather than carried here.** Run it from a hub checkout and name this repository explicitly, since it otherwise targets whichever repository the shell is sitting in. Its default mode PATCHes repository settings, toggles Dependabot features, and PUTs both branch rulesets. Pass its `check` mode for read-only validation, which is what you almost always want. The payloads it compares against are the hub's own committed `repo-config/` files; this repository carries no local copy, per [AUDIT.md "General Settings and Rulesets"][audit-general-settings-and-rulesets]. +- **The ruleset apply script writes unless told otherwise, and it is hub-hosted rather than carried here.** Run it from a hub checkout and name this repository explicitly, since it otherwise targets whichever repository the shell is sitting in. Its default mode PATCHes repository settings, toggles Dependabot features, and PUTs both branch rulesets. Pass its `check` mode for read-only validation, which is what you almost always want. The payloads it compares against are the hub's own committed `repo-config/` files. This repository carries no local copy, per [AUDIT.md "General Settings and Rulesets"][audit-general-settings-and-rulesets]. - **This file uses reference-style links, and it is the one file in this repository that should not.** The fleet rule allows four agent-instruction files to keep inline links, [`AGENTS.md`][agents], [`GOVERNANCE.md`][governance], this one, and `.github/copilot-instructions.md`, on the grounds that they are read one section at a time so a definition at the foot of the file is never reached. The other three take that allowance and this one does not, which is a divergence to close deliberately rather than incidentally. Every other Markdown file in the repository is reference-style by the rule rather than by exception. Definitions live at the bottom of the file, grouped under `` and `` and alphabetized within each group. A reference name encodes what it points at, so `analog-max17048-link`, never `analog-en-products-link` after a URL path segment. Removing a link also removes its definition, since an orphan fails the lint. - **Inline HTML is limited to `
` and ``.** Those two are allowed because a collapsible has no markdown equivalent. Every other element still fails `MD033`, and that includes a `` nested inside a `` - use a markdown code span there instead. - **The spell-check gate covers `**/README.md` plus [`DEVICES.md`][devices] and `HISTORY.md`**, wider than the fleet default, so a nested README fails CI like any other. The CI workflow and the `Lint: Spelling` task carry the identical list. From dc4bcb69ed6174d71bc17f82071bf5d822f78671 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 29 Aug 2026 11:24:24 -0700 Subject: [PATCH 10/11] Correct AUDIT.md's ruleset claim and OPERATIONS.md's lint-snapshot note (#119) * Correct the ruleset-cardinality claim and the docker_lint.py snapshot note AUDIT.md: configure.sh check only warns on a duplicate expected ruleset name and never enumerates a stray one, so attribute that assertion to spec/audit.py instead of claiming configure.sh check alone reports it. OPERATIONS.md: docker_lint.py's own target discovery shells out to git -C "$root" ls-files, so the sanitized snapshot (deliberately holding only tracked-plus-unignored files, no .git) can't be passed as its --root: it fails before any linter runs. Removes the broken example, adds cleanup (chmod a+rX with no removal trap left the snapshot world-readable and behind) and --network=none to the direct docker run invocations, and points at ptr727/ProjectTemplate#1090 for the wrapper gap. * Add the missing python3 prefix to a spec/audit.py invocation * Fix the lint-snapshot recipe: cwd-independence and container UID access Runs git ls-files and the archiving tar from the repository root explicitly (git -C "$repo_root"), rather than relying on the caller's cwd: run from a subdirectory, the bare form both scoped the file list to that subdirectory and mismatched the paths tar extracted, silently producing an incomplete or wrong snapshot. Verified locally from both the root and a subdirectory (187 files either way). Restores chmod -R o+rX after the extraction: mktemp -d's default 0700 blocks a lint container running as a non-matching, non-root UID from reading the mount at all. The earlier fix dropped it entirely to address a separate finding about world-readability with no cleanup; the EXIT trap already added covers that instead. --- AUDIT.md | 8 +++++--- OPERATIONS.md | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 5c7acf2..4c5e802 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -30,9 +30,11 @@ repo-config/configure.sh check ptr727/ESPHome-Config operational This asserts the live repository settings against the hub's `settings.json`, and each of the `main` and `develop` rulesets against the hub's `main.json` and `operational/develop.json`, -exiting non-zero on drift. The result must be exactly two rulesets named `develop` and `main` -(a missing ruleset or a divergent payload is a **defect**, and a duplicate or stray ruleset is a -**drift finding**). +exiting non-zero on drift. A missing ruleset or a divergent payload is a **defect**. The result +must also be exactly two rulesets named `develop` and `main`, but `configure.sh check` only warns +on a duplicate expected name and does not enumerate a stray one, so a duplicate or stray ruleset +is confirmed instead by running `python3 spec/audit.py ESPHome-Config` from the same hub checkout, which +does report both as a **drift finding**. ## Secrets diff --git a/OPERATIONS.md b/OPERATIONS.md index b1f20ab..eddb3d0 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -482,17 +482,24 @@ Sharp edges in the tooling around this repository, each one learned by tripping - **This file uses reference-style links, and it is the one file in this repository that should not.** The fleet rule allows four agent-instruction files to keep inline links, [`AGENTS.md`][agents], [`GOVERNANCE.md`][governance], this one, and `.github/copilot-instructions.md`, on the grounds that they are read one section at a time so a definition at the foot of the file is never reached. The other three take that allowance and this one does not, which is a divergence to close deliberately rather than incidentally. Every other Markdown file in the repository is reference-style by the rule rather than by exception. Definitions live at the bottom of the file, grouped under `` and `` and alphabetized within each group. A reference name encodes what it points at, so `analog-max17048-link`, never `analog-en-products-link` after a URL path segment. Removing a link also removes its definition, since an orphan fails the lint. - **Inline HTML is limited to `
` and ``.** Those two are allowed because a collapsible has no markdown equivalent. Every other element still fails `MD033`, and that includes a `` nested inside a `` - use a markdown code span there instead. - **The spell-check gate covers `**/README.md` plus [`DEVICES.md`][devices] and `HISTORY.md`**, wider than the fleet default, so a nested README fails CI like any other. The CI workflow and the `Lint: Spelling` task carry the identical list. -- **Never mount the live checkout into a third-party lint container.** `secrets.yaml` holds the live secrets and is git-ignored. [`secrets._yaml`][secrets-example] is the tracked placeholder. Build a temporary snapshot from tracked and intended untracked files, then mount that snapshot read-only. The Git exclusion rules keep `secrets.yaml` out without maintaining a second exclusion list. **This includes the hub's `scripts/docker_lint.py` wrapper**: its `--root` argument becomes a plain read-only bind mount with no exclusion of its own (read-only stops the container writing back, not reading `secrets.yaml`), so pass the snapshot's path as `--root` here, never `"$PWD"` on the live checkout. +- **Never mount the live checkout into a third-party lint container.** `secrets.yaml` holds the live secrets and is git-ignored. [`secrets._yaml`][secrets-example] is the tracked placeholder. Build a temporary snapshot from tracked and intended untracked files, mount that snapshot read-only, and remove it when done. The Git exclusion rules keep `secrets.yaml` out without maintaining a second exclusion list. ```shell + repo_root="$(git rev-parse --show-toplevel)" lint_root="$(mktemp -d /tmp/esphome-lint.XXXXXX)" - git ls-files --cached --others --exclude-standard -z | tar --null -T - -cf - | tar -xf - -C "$lint_root" - chmod -R a+rX "$lint_root" - docker run --rm -v "$lint_root":/workdir:ro --workdir /workdir - # Or, for the hub's docker_lint.py wrapper: - python3 /path/to/ProjectTemplate/scripts/docker_lint.py --root "$lint_root" + trap 'rm -rf "$lint_root"' EXIT + git -C "$repo_root" ls-files --cached --others --exclude-standard -z | tar -C "$repo_root" --null -T - -cf - | tar -xf - -C "$lint_root" + chmod -R o+rX "$lint_root" + docker run --rm --network=none -v "$lint_root":/workdir:ro --workdir /workdir ``` + `chmod -R o+rX` stays: `mktemp -d`'s default `0700` blocks a lint container running as a + non-matching, non-root UID (the common case for these images) from reading the mount at all, + and the trap now removes the snapshot regardless, so the readable window is only the lint run + itself on a single-tenant CI runner or dev host. + + **This includes the hub's `scripts/docker_lint.py` wrapper, and it cannot currently take this snapshot as its `--root`.** Its own target discovery runs `git -C "$root" ls-files`, and the snapshot above deliberately holds only the `git ls-files` output, not `.git` itself, so `--root "$lint_root"` fails with "not a git repository" before any linter runs. Passing the live checkout instead would defeat the whole point of the snapshot. Until the wrapper accepts a plain snapshot or file manifest (tracked upstream as [ptr727/ProjectTemplate#1090][hub-issue-1090]), lint this repository with the direct `docker run` invocations above rather than `docker_lint.py`. + - **The installed `gh` is old enough that `gh pr edit` always fails**, on every repository, and the message names a GraphQL field rather than a permission. See [The `gh` CLI Is Too Old for `gh pr edit`][gh-cli-too-old] for the cause and the REST workaround. - **A compile job that fails in about 18 seconds is a registry timeout, not a template defect.** The compile jobs pull the [`ptr727/esphome-nonroot`][esphome-nonroot-link] image, and a slow Docker Hub kills the job with `registry-1.docker.io ... context deadline exceeded` before any ESPHome work starts. A real esp-idf compile runs for minutes, so the duration tells the two apart at a glance. Re-run the job rather than reading the template for a fault that is not there. - **Check an esphome.io link by page title, not HTTP status.** The site answers an unknown path with `200` and a `404 - Page Not Found | ESPHome` title, so a status-code sweep reports a dead link as healthy. The current link form carries no `.html` suffix and no trailing slash, the `guides/configuration-types` anchors have moved to the dedicated [`components/substitutions`][substitutions-link] and [`components/packages`][packages-link] pages, and the per-board pages live on [devices.esphome.io][devices-esphome-link]. An anchor is verified by fetching the page and matching the `id` attribute, since a renamed anchor silently lands the reader at the top of the page. @@ -557,6 +564,7 @@ Sharp edges in the tooling around this repository, each one learned by tripping [espressif32-versions-link]: https://registry.platformio.org/platforms/platformio/espressif32/versions [flo-r1der-link]: https://github.com/Flo-R1der/ESPHome_RGB-Status-LED_Package [framework-espidf-link]: https://registry.platformio.org/tools/platformio/framework-espidf +[hub-issue-1090]: https://github.com/ptr727/ProjectTemplate/issues/1090 [option-zero-link]: https://github.com/Option-Zero/esphome-components [packages-link]: https://esphome.io/components/packages [substitutions-link]: https://esphome.io/components/substitutions From 1cd3743dbe3adc736ed630496f14568a8c26a775 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 29 Aug 2026 11:54:08 -0700 Subject: [PATCH 11/11] Run the lint snapshot container as the host UID, not chmod'ed (#120) * Run the lint container as the host UID instead of chmod'ing the snapshot Replaces chmod -R o+rX (which opened the sanitized snapshot to every local account on the host) with docker run --user "$(id -u):$(id -g)", so the container reads it through the same owner permission bits mktemp -d's default 0700 already grants. Verified end to end against a real container. Adds set -Eeuo pipefail to the recipe per the fleet's shell convention, so a failed ls-files or tar stage stops the pipeline instead of silently running Docker on a partial or empty snapshot. Also names requiredSecrets[] alongside publish[] and types[] as an AUDIT.md "Secrets" input, matching what the hub audit actually reads from the registry entry. * Note the rootless-Docker exception for the lint-snapshot UID mapping --user "$(id -u):$(id -g)" assumes a rootful Docker daemon. Under rootless Docker the daemon's own user namespace remaps the host UID to an unrelated subordinate one, so the snapshot's 0700 root becomes unreadable instead. States the fallback (chmod -R o+rX, the earlier approach) for that case rather than claiming host-UID ownership works universally. Also joins the surrounding paragraph back onto one line: it had hard-wrapped mid inline-code-span. --- AUDIT.md | 11 ++++++----- OPERATIONS.md | 9 +++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 4c5e802..e22e292 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -39,11 +39,12 @@ does report both as a **drift finding**. ## Secrets This repo carries no local `spec/secrets.json` either, for the same reason: the required and -forbidden name lists resolve centrally from the registry entry (`publish[]` and `types[]`) rather -than from anything repo-specific, so a per-repo copy could only restate the hub's own computation -or drift from it. This repo declares `requiredSecrets: []` and `publish: [{ "target": -"github-release", "mechanism": "none" }]` in the hub's registry, which resolves to no required -secret beyond the fleet baseline. Confirm it from a hub checkout at `main`: +forbidden name lists resolve centrally from the registry entry (`publish[]`, `types[]`, and its +own `requiredSecrets[]`) rather than from anything repo-specific, so a per-repo copy could only +restate the hub's own computation or drift from it. This repo declares `requiredSecrets: []` and +`publish: [{ "target": "github-release", "mechanism": "none" }]` in the hub's registry, which +resolves to no required secret beyond the fleet baseline. Confirm it from a hub checkout at +`main`: ```sh python3 spec/audit.py ESPHome-Config diff --git a/OPERATIONS.md b/OPERATIONS.md index eddb3d0..d545afe 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -485,18 +485,15 @@ Sharp edges in the tooling around this repository, each one learned by tripping - **Never mount the live checkout into a third-party lint container.** `secrets.yaml` holds the live secrets and is git-ignored. [`secrets._yaml`][secrets-example] is the tracked placeholder. Build a temporary snapshot from tracked and intended untracked files, mount that snapshot read-only, and remove it when done. The Git exclusion rules keep `secrets.yaml` out without maintaining a second exclusion list. ```shell + set -Eeuo pipefail repo_root="$(git rev-parse --show-toplevel)" lint_root="$(mktemp -d /tmp/esphome-lint.XXXXXX)" trap 'rm -rf "$lint_root"' EXIT git -C "$repo_root" ls-files --cached --others --exclude-standard -z | tar -C "$repo_root" --null -T - -cf - | tar -xf - -C "$lint_root" - chmod -R o+rX "$lint_root" - docker run --rm --network=none -v "$lint_root":/workdir:ro --workdir /workdir + docker run --rm --network=none --user "$(id -u):$(id -g)" -v "$lint_root":/workdir:ro --workdir /workdir ``` - `chmod -R o+rX` stays: `mktemp -d`'s default `0700` blocks a lint container running as a - non-matching, non-root UID (the common case for these images) from reading the mount at all, - and the trap now removes the snapshot regardless, so the readable window is only the lint run - itself on a single-tenant CI runner or dev host. + `--user "$(id -u):$(id -g)"` runs the container as the invoking host user rather than the image's own default, so it reads `$lint_root` through the same owner permission bits `mktemp -d`'s default `0700` already grants, with no `chmod` opening it to any other local account. This assumes a standard rootful Docker daemon, where the container's user namespace is the host's own. Under **rootless** Docker the daemon runs in its own user namespace, so the host UID passed to `--user` maps to an unrelated subordinate UID inside the container rather than to the owner of the bind mount, and the snapshot becomes unreadable instead. On a rootless daemon, `chmod -R o+rX "$lint_root"` after the tar step is the fallback, accepting the wider local-account readability for the run's duration. The trap removes the snapshot on exit either way. **This includes the hub's `scripts/docker_lint.py` wrapper, and it cannot currently take this snapshot as its `--root`.** Its own target discovery runs `git -C "$root" ls-files`, and the snapshot above deliberately holds only the `git ls-files` output, not `.git` itself, so `--root "$lint_root"` fails with "not a git repository" before any linter runs. Passing the live checkout instead would defeat the whole point of the snapshot. Until the wrapper accepts a plain snapshot or file manifest (tracked upstream as [ptr727/ProjectTemplate#1090][hub-issue-1090]), lint this repository with the direct `docker run` invocations above rather than `docker_lint.py`.