From 0f0e32917beccf8da1bd2f8997d71eced16ba61c Mon Sep 17 00:00:00 2001 From: "David R. Cushman" <107054455+david-r-cushman@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:11:46 -0600 Subject: [PATCH] feat(readme): add downstream readme alignment workflow --- .../skills/change-delivery-workflow/SKILL.md | 125 +++ .../agents/openai.yaml | 4 + .../skills/downstream-guidance-sync/SKILL.md | 46 +- .../agents/openai.yaml | 4 +- .../skills/downstream-repo-cleanup/SKILL.md | 124 +++ .../agents/openai.yaml | 3 + .codex/skills/readme-alignment/SKILL.md | 115 +++ .../readme-alignment/agents/openai.yaml | 3 + .../skills/template-version-release/SKILL.md | 8 +- .github/copilot-instructions.md | 7 +- AGENTS.md | 8 +- CHANGELOG.md | 14 +- README.md | 40 +- VERSION | 2 +- docs/agent-workflows.md | 17 +- .../0002-downstream-readme-workflow.md | 51 ++ docs/template-evolution.md | 41 +- scripts/Get-TemplateHealth.ps1 | 27 +- scripts/Initialize-DownstreamRepo.ps1 | 801 ++++++++++++++++++ scripts/Invoke-ReadmeAlignment.ps1 | 516 +++++++++++ scripts/Invoke-TemplateGuidanceSync.ps1 | 12 + scripts/Update-GeneratedMarkdown.ps1 | 22 + templates/downstream/README.md | 99 +++ .../unit/Initialize-DownstreamRepo.Tests.ps1 | 287 +++++++ tests/unit/Invoke-ReadmeAlignment.Tests.ps1 | 144 ++++ .../Invoke-TemplateGuidanceSync.Tests.ps1 | 73 +- tests/unit/SkillScaffold.Tests.ps1 | 211 ++++- tests/unit/TemplateScaffold.Tests.ps1 | 3 +- 28 files changed, 2704 insertions(+), 103 deletions(-) create mode 100644 .codex/skills/change-delivery-workflow/SKILL.md create mode 100644 .codex/skills/change-delivery-workflow/agents/openai.yaml create mode 100644 .codex/skills/downstream-repo-cleanup/SKILL.md create mode 100644 .codex/skills/downstream-repo-cleanup/agents/openai.yaml create mode 100644 .codex/skills/readme-alignment/SKILL.md create mode 100644 .codex/skills/readme-alignment/agents/openai.yaml create mode 100644 docs/decisions/0002-downstream-readme-workflow.md create mode 100644 scripts/Initialize-DownstreamRepo.ps1 create mode 100644 scripts/Invoke-ReadmeAlignment.ps1 create mode 100644 templates/downstream/README.md create mode 100644 tests/unit/Initialize-DownstreamRepo.Tests.ps1 create mode 100644 tests/unit/Invoke-ReadmeAlignment.Tests.ps1 diff --git a/.codex/skills/change-delivery-workflow/SKILL.md b/.codex/skills/change-delivery-workflow/SKILL.md new file mode 100644 index 0000000..97dc5cd --- /dev/null +++ b/.codex/skills/change-delivery-workflow/SKILL.md @@ -0,0 +1,125 @@ +--- +name: change-delivery-workflow +description: Use when asked to perform ordinary repository changes through a consistent workflow that handles sandbox escalation, non-main branches, changelog updates, release decisions, conventional commits, ready pull requests, and post-merge cleanup without inventing repo-specific process. +--- + +# Change Delivery Workflow + +## Overview + +Use this skill for day-to-day repository maintenance and feature work that does not already belong to a more specialized workflow such as downstream cleanup, downstream guidance sync, runtime policy updates, or template release finalization. + +This skill standardizes how agents should deliver ordinary changes: inspect guidance first, work from a non-main branch, capture the change in `CHANGELOG.md`, decide whether the work is release-worthy, validate with the repository's existing checks, and finish with a normal ready PR to `main`. + +This workflow is intentionally guidance-oriented rather than script-driven. The controls are repository guidance, Git state, repo-specific validators, diff review, and human review. + +## Why This Exists + +Routine ordinary repository work was already following a repeated pattern, but that pattern was not captured in one reusable place. The result was avoidable drift in how branches, changelog entries, release decisions, PRs, and post-merge cleanup were handled. + +The recurring Windows sandbox failure also needs to be treated explicitly. Errors such as `helper_sid_resolve_failed` and `LookupAccountNameW ... CodexSandboxOffline` are environment/runtime failures, not repository defects. They should trigger a scoped escalation retry for the blocked command, not a process shortcut or a speculative repository change. + +## Required Context + +Before acting, identify: + +- the repository guidance entrypoint, normally `AGENTS.md` and `.github/copilot-instructions.md` +- the current branch and working tree state +- whether the repository already has a release/version contract such as `VERSION`, a changelog version validator, release docs, or tag conventions +- whether the requested change is ordinary maintenance, a release-worthy capability change, or post-merge cleanup + +If the repo does not provide a clear versioning contract, default to a `CHANGELOG.md` update under `## Unreleased` unless the user explicitly asks to introduce release metadata. + +## Preflight Workflow + +Use this sequence before editing: + +1. Read the repository guidance and inspect the working tree. +2. If the current branch is `main` or `master`, create and switch to a working branch before making changes. +3. Review the relevant implementation, tests, docs, and existing validators before choosing the smallest safe change. +4. Decide whether the repo already defines a release workflow that should be respected for this change. + +If the task clearly matches a more specialized repo-local skill, use that skill instead of this one. + +## Sandbox Recovery + +When a needed command fails because of the Windows sandbox runtime, retry the blocked command with: + +- `sandbox_permissions: require_escalated` +- a narrow, user-facing `justification` +- the smallest reasonable `prefix_rule` + +Use this recovery path only for commands that are important to the requested work. Keep the retry scoped to the blocked command rather than broadening the task. After escalation succeeds, continue normal diff review and validation. + +Do not describe sandbox failures as repository bugs unless repository code or configuration is actually responsible. + +## Change Workflow + +When the user wants the repository changed, use this sequence: + +1. Confirm the working branch is not `main` or `master`. +2. Make the smallest change that satisfies the request. +3. Update `CHANGELOG.md` for every meaningful change. +4. Run the repository's normal validation entrypoints when they exist. +5. Inspect the diff and stage only intended files. +6. Create a Conventional Commit message that matches the actual change. +7. Push the branch and open a ready PR to `main` unless the user explicitly asks for a draft PR. + +Use `## Unreleased` for ordinary maintenance notes by default. + +## Release Decision + +Use the repository's existing release posture instead of inventing one: + +- if the repo already has `VERSION`, release validators, tag conventions, or release docs, follow them +- if the change adds a meaningful new repo capability, prepare the next release entry using the repo's established SemVer policy +- if the change is not release-worthy, keep the note under `## Unreleased` + +When preparing a release-worthy change: + +1. Move relevant `Unreleased` notes into the new version section. +2. Update the repo's existing version metadata together. +3. Validate the release metadata with the repo's existing checks before opening the PR. +4. After the PR is merged, follow the repo's existing post-merge tag and release process. + +If the repo does not already define release metadata, stop short of inventing a new versioning system unless the user explicitly asks for that broader change. + +## Post-Merge Cleanup + +After a PR is merged: + +1. Switch to `main`. +2. Fast-forward from `origin/main`. +3. Prune deleted remote refs. +4. Confirm the working tree is clean. +5. Delete the merged local branch only after the remote branch deletion or merge is confirmed. + +If the repository has an established post-merge release step such as tag creation or GitHub Release publishing, perform it only when the repo's workflow calls for it. + +## Success Criteria + +The workflow is complete when: + +- repository guidance was reviewed before changes were made +- edits were performed from a non-main branch +- `CHANGELOG.md` was updated appropriately +- release metadata was updated only when the repo already supported that workflow and the change justified it +- validation was run, or a clear reason was reported when it was unavailable or intentionally skipped +- the branch was pushed and a ready PR to `main` was opened when requested +- post-merge cleanup or release follow-up was handled according to the repo's existing workflow + +## Stop Conditions + +Stop and report instead of improvising when: + +- repository guidance is missing +- the working tree is dirty in a way that conflicts with the requested change +- the repo's release/version contract is inconsistent across files or validators +- validation fails and the failure is not clearly unrelated to the requested change +- the user asks for a versioning or release process the repository does not currently define + +Do not bypass branch discipline, changelog updates, or diff review just because a task appears small. + +## Agent Role + +Treat repository guidance, Git state, changelog history, and repo-specific validators as the source of truth. The agent role is to coordinate a repeatable delivery workflow: inspect context, recover safely from sandbox failures when needed, keep changes off `main`, capture the work in the changelog, respect repo-specific release posture, validate, prepare commits, open ready PRs, and clean up local state after merge. diff --git a/.codex/skills/change-delivery-workflow/agents/openai.yaml b/.codex/skills/change-delivery-workflow/agents/openai.yaml new file mode 100644 index 0000000..e84bbfc --- /dev/null +++ b/.codex/skills/change-delivery-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Change Delivery Workflow" + short_description: "Deliver ordinary repo changes with branch, changelog, PR, and release discipline" + default_prompt: "Use $change-delivery-workflow to perform routine repository changes through the standard branch, changelog, validation, commit, PR, and post-merge cleanup workflow." diff --git a/.codex/skills/downstream-guidance-sync/SKILL.md b/.codex/skills/downstream-guidance-sync/SKILL.md index aad5cdb..38f830b 100644 --- a/.codex/skills/downstream-guidance-sync/SKILL.md +++ b/.codex/skills/downstream-guidance-sync/SKILL.md @@ -1,21 +1,23 @@ --- name: downstream-guidance-sync -description: Use when asked to sync, audit, update, or align AI guidance from powershell-dev-template into a downstream repository created from the template. Guides agents to operate scripts/Invoke-TemplateGuidanceSync.ps1 safely with audit-first behavior, non-main branches, diff review, validation, commits, and pull requests. +description: Use when asked to sync, audit, update, or align AI guidance and downstream cleanup workflow assets from powershell-dev-template into a downstream repository created from the template. Guides agents to operate scripts/Invoke-TemplateGuidanceSync.ps1 safely with audit-first behavior, non-main branches, diff review, validation, commits, and pull requests. --- # Downstream Guidance Sync ## Overview -Use this skill to synchronize template-owned AI guidance and ADR scaffolding from `powershell-dev-template` into a downstream repository. The deterministic tool is `scripts/Invoke-TemplateGuidanceSync.ps1`; this skill defines how an agent should operate that tool safely. +Use this skill to synchronize template-owned downstream guidance, workflow documentation, and cleanup workflow assets from `powershell-dev-template` into an existing downstream repository. The deterministic tool is `scripts/Invoke-TemplateGuidanceSync.ps1`; this skill defines how an agent should operate that tool safely. -The sync scope is intentionally narrow. Do not manually copy, edit, or invent files outside the script allowlist. +This workflow does not perform cleanup itself. For newly created downstream repositories, cleanup still runs locally from the downstream repo through `scripts/Initialize-DownstreamRepo.ps1`. + +The sync scope is intentionally narrow and migration-oriented. Do not manually copy, edit, or invent files outside the script allowlist. ## Why This Exists Repositories created from this template become independent projects after creation. Their source, tests, CI, analyzer settings, runtime policy, and scaffolds are project-owned and should not be clobbered by template updates. -AI guidance is the default sync target because it governs how AI-assisted work is produced, reviewed, and validated. The ADR scaffold README is also safe to sync because it provides a documentation convention without overwriting project-specific decisions. Keeping these files aligned helps downstream repositories inherit the current operating model without overwriting implementation choices. +AI guidance is the default sync target because it governs how AI-assisted work is produced, reviewed, and validated. The ADR scaffold README is also safe to sync because it provides a documentation convention without overwriting project-specific decisions. For downstream repositories that predate the cleanup workflow, the sync process can also deliver the cleanup script, cleanup skill, shared downstream README skeleton, README alignment workflow assets, and the runtime-policy README-generation assets that keep that README workflow functional. ## Required Context @@ -23,6 +25,7 @@ Before acting, identify: - the template repo path, normally the current `powershell-dev-template` checkout - the downstream repo path supplied by the user +- whether the downstream repo already contains the cleanup workflow assets - whether the user wants audit-only output or a branch/PR workflow If the downstream repo path is missing, ask for it. Do not guess from nearby folders. @@ -32,7 +35,7 @@ If the downstream repo path is missing, ask for it. Do not guess from nearby fol Run audit mode first from the template repo: ```powershell -powershell.exe -NoProfile -File ./scripts/Invoke-TemplateGuidanceSync.ps1 -Path ../downstream-repo +powershell.exe -NoProfile -File .\scripts\Invoke-TemplateGuidanceSync.ps1 -Path ..\downstream-repo ``` Use audit output to determine whether drift exists. Report `Current`, `Outdated`, `Missing`, or `Malformed` statuses in plain language. Do not apply changes during audit mode. @@ -40,9 +43,11 @@ Use audit output to determine whether drift exists. Report `Current`, `Outdated` For automation-friendly inspection, use: ```powershell -powershell.exe -NoProfile -File ./scripts/Invoke-TemplateGuidanceSync.ps1 -Path ../downstream-repo -OutputFormat Json +powershell.exe -NoProfile -File .\scripts\Invoke-TemplateGuidanceSync.ps1 -Path ..\downstream-repo -OutputFormat Json ``` +If the downstream repo is missing `scripts/Initialize-DownstreamRepo.ps1` or `.codex/skills/downstream-repo-cleanup/`, treat that as a signal that the repo predates the cleanup workflow and should receive those assets through sync before cleanup is run locally. + ## Apply Workflow When the user requests sync changes, use this sequence: @@ -51,24 +56,25 @@ When the user requests sync changes, use this sequence: 2. Create or switch to a non-main branch in the downstream repo, for example: ```powershell - git -C ../downstream-repo switch -c chore/sync-template-guidance-0.7.0 + git -C ../downstream-repo switch -c chore/sync-template-guidance ``` 3. Run the sync script from the template repo: ```powershell - powershell.exe -NoProfile -File ./scripts/Invoke-TemplateGuidanceSync.ps1 -Path ../downstream-repo -Apply + powershell.exe -NoProfile -File .\scripts\Invoke-TemplateGuidanceSync.ps1 -Path ..\downstream-repo -Apply ``` -4. Inspect the downstream diff and verify changes are limited to AI guidance files, `docs/decisions/README.md`, and the README template badge. -5. Run downstream validation if the repo provides a clear validation entrypoint, such as `scripts/Invoke-RepoChecks.ps1`. -6. Commit with a conventional docs message, for example: +4. Inspect the downstream diff and verify changes are limited to synced guidance files, the ADR scaffold README, the README template badge, and the cleanup or README workflow assets shipped from the template. +5. If the sync delivered `scripts/Initialize-DownstreamRepo.ps1`, the cleanup skill, or the shared README workflow assets into an older downstream repo, switch into the downstream repo and run cleanup or README alignment there before adding more project-specific changes. +6. Run downstream validation if the repo provides a clear validation entrypoint, such as `scripts/Invoke-RepoChecks.ps1`. +7. Commit with a conventional docs or chore message that reflects the actual change, for example: ```text - docs(ai): sync template guidance to 0.7.0 + chore(repo): sync template guidance and cleanup assets ``` -7. Open or draft a PR when requested, summarizing changed guidance and validation results. +8. Open or draft a PR when requested, summarizing changed guidance, delivered cleanup assets, and validation results. ## Success Criteria @@ -78,7 +84,7 @@ The workflow is complete when: - apply mode, when used, ran only from a non-main downstream branch - the downstream diff is limited to the sync allowlist, including only the ADR scaffold README under `docs/decisions/` - downstream validation was run, or a clear reason was reported when validation was unavailable or skipped -- the commit or PR summary states the synced template guidance version and validation result +- the commit or PR summary states whether the change was a guidance refresh, a cleanup-asset delivery, a README-workflow delivery, or a combination of those, along with the validation result ## Stop Conditions @@ -99,12 +105,20 @@ The script may update only: - `AGENTS.md` - `.github/copilot-instructions.md` +- `.codex/skills/downstream-repo-cleanup/` +- `.codex/skills/readme-alignment/` - selected AI governance and operating-model docs under `docs` +- `docs/agent-workflows.md` - `docs/decisions/README.md` +- `scripts/Initialize-DownstreamRepo.ps1` +- `scripts/Invoke-ReadmeAlignment.ps1` +- `scripts/Update-GeneratedMarkdown.ps1` +- `eng/runtime-policy.json` +- `templates/downstream/README.md` - the README template-version badge -It must not update downstream source, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, development-environment configuration, runtime policy, module manifests, scaffolds, or numbered project-specific ADRs. +It must not update downstream source, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, development-environment configuration, module manifests, scaffolds other than the cleanup and README workflow assets, or numbered project-specific ADRs. The one runtime-policy exception is the narrow set of README-generation assets required by the shared downstream README workflow. It also does not perform cleanup or README alignment itself; it only delivers the assets needed for those workflows to run locally in downstream repositories. ## Agent Role -Treat the script as the source of truth for deterministic behavior. The agent role is to orchestrate: run the script, interpret output, inspect diffs, run validation, prepare commits, and draft PR text. +Treat the script as the source of truth for deterministic behavior. The agent role is to orchestrate: run the script, interpret output, inspect diffs, identify whether cleanup assets were delivered, run validation, prepare commits, and draft PR text. \ No newline at end of file diff --git a/.codex/skills/downstream-guidance-sync/agents/openai.yaml b/.codex/skills/downstream-guidance-sync/agents/openai.yaml index ad7244e..8333abc 100644 --- a/.codex/skills/downstream-guidance-sync/agents/openai.yaml +++ b/.codex/skills/downstream-guidance-sync/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Downstream Guidance Sync" - short_description: "Sync template AI guidance into downstream repos" - default_prompt: "Use $downstream-guidance-sync to sync a downstream repository with the current powershell-dev-template AI guidance." + short_description: "Sync template guidance and cleanup assets into downstream repos" + default_prompt: "Use $downstream-guidance-sync to sync a downstream repository with the current powershell-dev-template guidance and cleanup workflow assets." diff --git a/.codex/skills/downstream-repo-cleanup/SKILL.md b/.codex/skills/downstream-repo-cleanup/SKILL.md new file mode 100644 index 0000000..8ee5a7b --- /dev/null +++ b/.codex/skills/downstream-repo-cleanup/SKILL.md @@ -0,0 +1,124 @@ +--- +name: downstream-repo-cleanup +description: Use when asked to handle downstream repository cleanup, normalization, initialization, or first-run preparation for a newly created repository from powershell-dev-template before project-specific work begins. Guides agents to operate scripts/Initialize-DownstreamRepo.ps1 safely in audit-first mode, preserve or insert the README template version badge, validate changes, and stop when the repository has already moved beyond the immediate post-create window. +--- + +# Downstream Repo Cleanup + +## Overview + +Use this skill to normalize a repository immediately after it is created from `powershell-dev-template`. The deterministic tool is `scripts/Initialize-DownstreamRepo.ps1`; this skill defines how an agent should operate that tool safely. + +This workflow is intentionally for first-run cleanup only. Do not use it as a general-purpose restructuring tool after meaningful project-specific work has started. + +## Why This Exists + +Repositories created from the template inherit both a useful development baseline and a set of template-maintainer artifacts. Running cleanup immediately after creation makes it clear which inherited files become downstream-owned and which ones should be removed before the repository starts diverging. + +The README template version badge should remain in the downstream repository. It indicates inherited guidance and baseline alignment, not full parity with the template. + +## Required Context + +Before acting, identify: + +- the downstream repo path, normally the current repository +- the intended `ProjectType`: `script`, `module`, or `hybrid` +- the repository name to use in downstream text normalization +- the module name when `ProjectType` is `module` or `hybrid` + +If the repository already contains project-specific source files, tests, ADRs, or materially rewritten guidance docs, stop and report that the repo is no longer in the intended immediate post-create window. + +## Audit Workflow + +Run audit mode first from the downstream repo: + +```powershell +powershell.exe -NoProfile -File .\scripts\Initialize-DownstreamRepo.ps1 -RepositoryName winget-powershell-automation +``` + +Choose `ProjectType` explicitly when the repo is not script-first: + +```powershell +powershell.exe -NoProfile -File .\scripts\Initialize-DownstreamRepo.ps1 -ProjectType module -ModuleName WingetAutomation -RepositoryName winget-powershell-automation +``` + +Use audit output to review planned `Remove`, `Rewrite`, `Rename`, `Keep`, and `ManualFollowUp` actions before changing files. + +For automation-friendly inspection, use: + +```powershell +powershell.exe -NoProfile -File .\scripts\Initialize-DownstreamRepo.ps1 -OutputFormat Json +``` + +## Apply Workflow + +When the user requests cleanup changes, use this sequence: + +1. Confirm the repository is still in the immediate post-create window. +2. Run the cleanup script with the intended project shape. +3. Preserve or insert the README template version badge during cleanup. +4. Inspect the diff and confirm changes are limited to template-maintainer artifact removal, inherited guidance rewrites, and project-type scaffold normalization. +5. Run downstream validation: + + ```powershell + powershell.exe -NoProfile -File .\scripts\Invoke-RepoChecks.ps1 + ``` + +6. Commit with a conventional message, for example: + + ```text + chore(repo): initialize downstream baseline + ``` + +## Success Criteria + +The workflow is complete when: + +- audit output has been reviewed and explained in plain language +- apply mode, when used, ran before project-specific work was added +- the README template version badge exists after cleanup +- template-maintainer files were removed or rewritten according to the workflow contract +- downstream validation was run, or a clear reason was reported when validation was unavailable or skipped + +## Stop Conditions + +Stop and report instead of improvising when: + +- the repo path is missing or is not a Git repo +- `ProjectType` is invalid +- `ModuleName` is missing for `module` or `hybrid` +- the repo already contains project-specific source files, tests, ADRs, or materially rewritten guidance +- validation fails and the failure is not clearly unrelated to cleanup + +Do not use this workflow to invent business logic, project-specific docs, or unrelated CI changes. + +## Cleanup Boundary + +The script may remove template-maintainer artifacts such as: + +- `VERSION` +- `CHANGELOG.md` +- template release and runtime-policy workflow skills +- template-maintainer tests and ADR seed files + +It should preserve downstream README workflow assets such as the shared README skeleton, `scripts/Invoke-ReadmeAlignment.ps1`, and `.codex/skills/readme-alignment/`. + +It may rewrite inherited downstream-owned guidance such as: + +- `README.md` +- `AGENTS.md` +- `.github/copilot-instructions.md` +- `docs/agent-workflows.md` +- `docs/decisions/README.md` + +It must preserve downstream baseline infrastructure such as: + +- CI workflow scaffolding +- runtime policy files +- `scripts/Invoke-RepoChecks.ps1` +- `scripts/Invoke-TemplateGuidanceSync.ps1` +- `.codex/skills/downstream-guidance-sync/` + +## Agent Role + +Treat the script as the source of truth for deterministic behavior. The agent role is to choose the right project profile, run the script, interpret audit output, inspect diffs, run validation, and summarize any remaining manual follow-up items. diff --git a/.codex/skills/downstream-repo-cleanup/agents/openai.yaml b/.codex/skills/downstream-repo-cleanup/agents/openai.yaml new file mode 100644 index 0000000..98da0f1 --- /dev/null +++ b/.codex/skills/downstream-repo-cleanup/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: "Downstream Repo Cleanup" +short_description: "Normalize a new downstream repo" +default_prompt: "Use $downstream-repo-cleanup to normalize this new downstream repository." diff --git a/.codex/skills/readme-alignment/SKILL.md b/.codex/skills/readme-alignment/SKILL.md new file mode 100644 index 0000000..8062b42 --- /dev/null +++ b/.codex/skills/readme-alignment/SKILL.md @@ -0,0 +1,115 @@ +--- +name: readme-alignment +description: Use when asked to bring a downstream repository README into alignment with the shared powershell-dev-template README skeleton while preserving repo-specific content and generated Markdown blocks. +--- + +# README Alignment + +## Overview + +Use this skill to audit or align a downstream repository `README.md` against the shared README skeleton shipped from `powershell-dev-template`. The deterministic tool is `scripts/Invoke-ReadmeAlignment.ps1`; this skill defines how an agent should operate that tool safely. + +This workflow is intended for template-derived downstream repositories created from `powershell-dev-template`. It is not a general-purpose README rewriter for unrelated repositories. + +## Why This Exists + +Downstream repositories should share a recognizable portfolio structure without losing truthful repo-specific documentation. The shared skeleton captures the durable README architecture, while this workflow lets maintainers realign structure later without manually reconstructing it from memory or diff history. + +The shared skeleton remains generator-driven. Generated Markdown blocks, the template version badge contract, and the runtime policy assets still matter in downstream repositories that keep that baseline. + +## Required Context + +Before acting, identify: + +- the downstream repo path, normally the current repository +- whether the repo still appears to be derived from `powershell-dev-template` +- whether the user wants audit-only output or a branch/apply workflow +- whether the repo already contains additional repo-specific README sections that must be preserved + +If the repo does not contain the shared skeleton asset or the runtime-policy README-generation assets, use downstream guidance sync first instead of improvising the missing files. + +## Audit Workflow + +Run audit mode first from the downstream repo: + +```powershell +powershell.exe -NoProfile -File .\scripts\Invoke-ReadmeAlignment.ps1 +``` + +Use audit output to review: + +- missing or out-of-order shared sections +- extra repo-specific sections that will be preserved +- whether the template version badge is currently present + +For automation-friendly inspection, use: + +```powershell +powershell.exe -NoProfile -File .\scripts\Invoke-ReadmeAlignment.ps1 -OutputFormat Json +``` + +## Apply Workflow + +When the user requests alignment changes, use this sequence: + +1. Confirm the repo is a supported downstream repository. +2. Create or switch to a non-main branch before applying changes. +3. Run the alignment script: + + ```powershell + powershell.exe -NoProfile -File .\scripts\Invoke-ReadmeAlignment.ps1 -Apply + ``` + +4. Inspect the diff and verify: + - shared sections were normalized to the skeleton order + - `Portfolio Context` and `Template Versioning` were preserved + - repo-specific extra sections were not discarded +5. Run downstream validation: + + ```powershell + powershell.exe -NoProfile -File .\scripts\Invoke-RepoChecks.ps1 + ``` + +6. Commit with a conventional docs or chore message that reflects the actual change. + +## Success Criteria + +The workflow is complete when: + +- audit output has been reviewed and explained in plain language +- apply mode, when used, ran from a non-main branch +- the resulting README follows the shared skeleton order +- repo-specific extra sections were preserved +- `Portfolio Context` and `Template Versioning` remain present +- downstream validation was run, or a clear reason was reported when validation was unavailable or skipped + +## Stop Conditions + +Stop and report instead of improvising when: + +- the repo is not a Git repository +- the repo does not appear to be derived from `powershell-dev-template` +- the shared skeleton asset is missing +- required README-generation assets are missing +- validation fails and the failure is not clearly unrelated to the README alignment + +Do not use this workflow to invent project-specific claims, implementation details, or CI behavior. + +## Boundary + +The script may: + +- align `README.md` to the shared downstream skeleton +- preserve repo-specific extra sections +- preserve the template version badge when it already exists +- use the synced runtime-policy README-generation assets that support generated blocks + +It must not: + +- overwrite source code, tests, CI, module manifests, or numbered project-specific ADRs +- fabricate repo-specific functionality or project history +- treat unrelated repositories as supported targets + +## Agent Role + +Treat the shared skeleton and `scripts/Invoke-ReadmeAlignment.ps1` as the source of truth for deterministic structure. The agent role is to run the script, interpret audit output, inspect diffs, preserve truthful repo-specific documentation, validate the result, and summarize any remaining manual follow-up. diff --git a/.codex/skills/readme-alignment/agents/openai.yaml b/.codex/skills/readme-alignment/agents/openai.yaml new file mode 100644 index 0000000..fdace9c --- /dev/null +++ b/.codex/skills/readme-alignment/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: "README Alignment" +short_description: "Align a downstream README to the shared portfolio skeleton" +default_prompt: "Use $readme-alignment to audit or align this downstream repository README to the shared powershell-dev-template skeleton while preserving repo-specific content." diff --git a/.codex/skills/template-version-release/SKILL.md b/.codex/skills/template-version-release/SKILL.md index 000b107..aa1e9c3 100644 --- a/.codex/skills/template-version-release/SKILL.md +++ b/.codex/skills/template-version-release/SKILL.md @@ -64,10 +64,10 @@ After the release PR is merged: 2. Switch to `main`. 3. Fast-forward `main` from `origin/main`. 4. Confirm `VERSION`, README badge, and `CHANGELOG.md` match the release version. -5. Create an annotated tag: +5. Create a lightweight tag: ```powershell - git tag -a vX.Y.Z -m "Release vX.Y.Z" + git tag vX.Y.Z ``` 6. Push the tag: @@ -98,9 +98,9 @@ The workflow is complete when: - `VERSION`, the README template badge, and `CHANGELOG.md` agree on the release version - `scripts/Test-TemplateVersion.ps1` passes before the release PR is opened or updated -- after merge, `main` is fast-forwarded and the annotated `vX.Y.Z` tag is pushed +- after merge, `main` is fast-forwarded and the lightweight `vX.Y.Z` tag is pushed - `scripts/Test-TemplateVersion.ps1 -CheckTag` passes after tagging -- a GitHub Release exists for `vX.Y.Z` with notes derived from the matching `CHANGELOG.md` section +- a GitHub Release exists for `vX.Y.Z` with notes derived from the matching `CHANGELOG.md` section`n- GitHub shows the pushed tag as verified and associated with the intended merged release commit - the merged local branch has been cleaned up after the remote branch deletion or merge is confirmed ## Stop Conditions diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2a8c2b7..4318589 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -84,10 +84,15 @@ Repo-local Codex skills are stored under `.codex/skills/`. When asked to synchro When asked to update the Windows PowerShell runtime, GitHub Actions runner, or pinned PowerShell tooling, use `.codex/skills/runtime-policy-update/SKILL.md`. Treat `eng/runtime-policy.json` as the source of truth, use `scripts/Update-GeneratedMarkdown.ps1` for generated Markdown blocks, and validate with `scripts/Test-VersionPolicy.ps1`. -When asked to prepare, validate, tag, publish, or clean up a template release version, use `.codex/skills/template-version-release/SKILL.md`. Keep `VERSION`, the README template-version badge, and `CHANGELOG.md` aligned, validate with `scripts/Test-TemplateVersion.ps1`, and create annotated `vX.Y.Z` tags plus GitHub Releases only after the release PR is merged to `main`. +When asked to prepare, validate, tag, publish, or clean up a template release version, use `.codex/skills/template-version-release/SKILL.md`. Keep `VERSION`, the README template-version badge, and `CHANGELOG.md` aligned, validate with `scripts/Test-TemplateVersion.ps1`, and create lightweight `vX.Y.Z` tags plus GitHub Releases only after the release PR is merged to `main`. When adding or updating repo-local skills, add or update Pester coverage in `tests/unit/SkillScaffold.Tests.ps1` for the skill file, metadata, required references, and agent discoverability. The Codex `quick_validate.py` helper may be used as an optional authoring check, but Pester is the repository validation standard. +When asked to perform ordinary repository changes, use `.codex/skills/change-delivery-workflow/SKILL.md` to coordinate sandbox escalation, non-main branches, changelog updates, release decisions, commits, pull requests, and post-merge cleanup. + +When asked to normalize a newly created downstream repository from this template, use `.codex/skills/downstream-repo-cleanup/SKILL.md` and operate `scripts/Initialize-DownstreamRepo.ps1` through the documented audit, diff review, validation, and commit workflow. Preserve the downstream README workflow assets and stop once the repository has moved beyond the immediate post-create window. + +When asked to audit or align a downstream README to the shared portfolio skeleton, use `.codex/skills/readme-alignment/SKILL.md` and operate `scripts/Invoke-ReadmeAlignment.ps1` through the documented audit, branch, validation, diff-review, and commit workflow. ## PowerShell Compatibility Target Windows PowerShell 5.1 Desktop unless `README.md`, `/docs`, or the task-specific template declares another version under `PowerShell Version`, `Requirements`, or `Compatibility`. For version conflicts, use this precedence: `README.md`, then `/docs`, then task-specific template. If version requirements are missing, invalid, outdated, or unsupported, provide a Windows PowerShell 5.1 Desktop-compatible fallback and document the assumption. diff --git a/AGENTS.md b/AGENTS.md index c6e485d..4d2ee42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,13 @@ If guidance in this file conflicts with `.github/copilot-instructions.md`, `.git Repo-local Codex skills are stored under `.codex/skills/`. -For downstream AI guidance synchronization, agents should use `.codex/skills/downstream-guidance-sync/SKILL.md` together with `scripts/Invoke-TemplateGuidanceSync.ps1` instead of manually copying guidance files. +For general repository change delivery, agents should use `.codex/skills/change-delivery-workflow/SKILL.md` to handle sandbox escalation, non-`main` branches, changelog updates, release decisions, commits, pull requests, and post-merge cleanup consistently. + +For immediate post-create normalization of repositories created from this template, agents should use `.codex/skills/downstream-repo-cleanup/SKILL.md` together with `scripts/Initialize-DownstreamRepo.ps1` before adding project-specific implementation, docs, tests, ADRs, or CI changes. + +For downstream AI guidance synchronization and delivery of newly added cleanup and README workflow assets into older downstream repositories, agents should use `.codex/skills/downstream-guidance-sync/SKILL.md` together with `scripts/Invoke-TemplateGuidanceSync.ps1` instead of manually copying guidance files. Cleanup itself still runs from the downstream repository through `scripts/Initialize-DownstreamRepo.ps1`. + +For shared README alignment after cleanup, agents should use `.codex/skills/readme-alignment/SKILL.md` together with `scripts/Invoke-ReadmeAlignment.ps1`. For template runtime and tooling policy updates, agents should use `.codex/skills/runtime-policy-update/SKILL.md` together with `eng/runtime-policy.json`, `scripts/Update-GeneratedMarkdown.ps1`, and `scripts/Test-VersionPolicy.ps1`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d8244..9fa5dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,19 @@ This project uses Semantic Versioning for the template itself. ### Changed -- Added a README portfolio-context section that frames this repository as the engineering platform behind the Windows PowerShell 5.1 portfolio while keeping the supporting capability claims aligned to the current template. +## 0.3.0 - 2026-07-10 + +### Added + +- Added a shared downstream README skeleton for repositories created from this template. +- Added deterministic downstream cleanup and downstream README alignment workflows, including repo-local cleanup and README-alignment skills. +- Added ADR 0002 documenting the downstream README workflow and starter README strategy. +- Added the change-delivery workflow skill so ordinary repository changes follow the same repeatable branch, changelog, validation, and PR discipline as the PowerShell Core template. + +### Changed + +- Expanded downstream guidance sync to deliver cleanup assets, README workflow assets, and the runtime-policy README-generation files required by those workflows. +- Updated generated Markdown, version-policy validation, template health reporting, README guidance, and skill discoverability to treat the new downstream README workflow as a maintained template capability. ## 0.2.0 - 2026-07-09 diff --git a/README.md b/README.md index afed6a8..5de61eb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![PowerShell 5.1](https://img.shields.io/badge/PowerShell-5.1-blue) -![Template Version](https://img.shields.io/badge/template-0.2.0-blue) +![Template Version](https://img.shields.io/badge/template-0.3.0-blue) A repeatable Windows PowerShell development template for building scripts, modules, and automation projects. @@ -17,8 +17,8 @@ This template provides a standardized starting point for PowerShell development - GitHub Actions CI on Windows - reusable script, function, module, and test scaffolds - AI governance and GitHub Copilot guidance -- downstream AI guidance synchronization -- repo-local agent workflows for change delivery, guidance sync, runtime policy updates, and template release management +- downstream AI guidance synchronization and starter README workflow delivery +- repo-local agent workflows for change delivery, downstream cleanup, guidance sync, README alignment, runtime policy updates, and template release management - Conventional Commit and repository hygiene standards Designed for engineers who want a consistent, AI-assisted PowerShell development workflow with validation and review guardrails. This repository also serves as the baseline template for my Windows PowerShell 5.1 portfolio projects, where downstream repositories demonstrate these standards applied to real Windows automation work. @@ -61,13 +61,19 @@ That same philosophy also shapes how AI assistance is used in this template and 1. Create a new repository from this template. 2. Open the repository locally in VS Code on Windows. -3. Confirm the PowerShell extension is using **Windows PowerShell** before adding project-specific docs, tests, ADRs, or CI changes. -4. Replace placeholder module metadata if the project is module-oriented. -5. Add scripts, functions, modules, or automation under `src`. -6. Add project-specific Pester tests under `tests`. -7. Copy and adapt scaffolds from `templates` for new functions, scripts, modules, and tests when they fit the work. -8. Review the AI-assisted development guidance in `AGENTS.md` and `.github/copilot-instructions.md` before using AI-generated changes. -9. Run local validation: +3. Run the downstream cleanup workflow immediately, before adding project-specific docs, tests, ADRs, or CI changes: + + ```powershell + powershell.exe -NoProfile -File .\scripts\Initialize-DownstreamRepo.ps1 -Apply -RepositoryName + ``` + +4. Confirm the PowerShell extension is using **Windows PowerShell** for ongoing local work. +5. Replace placeholder module metadata if the project is module-oriented. +6. Add scripts, functions, modules, or automation under `src`. +7. Add project-specific Pester tests under `tests`. +8. Copy and adapt scaffolds from `templates` for new functions, scripts, modules, and tests when they fit the work. +9. Review the AI-assisted development guidance in `AGENTS.md` and `.github/copilot-instructions.md` before using AI-generated changes. +10. Run local validation: ```powershell powershell.exe -NoProfile -File .\scripts\Invoke-RepoChecks.ps1 -IncludeTemplates @@ -114,6 +120,10 @@ This repository includes the environment, conventions, and approved templates us Core repository structure: +- emplates/downstream/README.md: shared downstream README skeleton used by cleanup and later README alignment +- scripts/Initialize-DownstreamRepo.ps1: deterministic first-run downstream cleanup +- scripts/Invoke-ReadmeAlignment.ps1: downstream README audit and alignment entrypoint + - `src/`: project source and optional module scaffold - `tests/`: Pester tests for the template itself and downstream project tests after repository creation - `templates/`: approved function, script, module, pattern, and test scaffolds @@ -157,13 +167,15 @@ Validation and maintenance also rely on: - [`docs/template-evolution.md`](docs/template-evolution.md) - [`docs/agent-workflows.md`](docs/agent-workflows.md) -This template includes repo-local agent workflows for repeatable maintenance. Guidance sync, runtime policy updates, and template release work are documented under `.codex/skills/` and indexed in [`docs/agent-workflows.md`](docs/agent-workflows.md). +This template includes repo-local agent workflows for repeatable maintenance. Change delivery, downstream cleanup, guidance sync, README alignment, runtime policy updates, and template release work are documented under `.codex/skills/` and indexed in [`docs/agent-workflows.md`](docs/agent-workflows.md). + +For ordinary repository changes, use .codex/skills/change-delivery-workflow/SKILL.md. For new downstream repo cleanup, use .codex/skills/downstream-repo-cleanup/SKILL.md. For later downstream README alignment, use .codex/skills/readme-alignment/SKILL.md. ## Downstream Guidance Sync -This template includes a local sync tool for repositories created from it. The sync can refresh AI guidance, guardrail documentation, the ADR scaffold README, and the README template-version badge so downstream repositories can stay aligned with template-owned guidance after project-specific work has begun. +This template includes a local sync tool for repositories created from it. The sync can refresh AI guidance, guardrail documentation, the ADR scaffold README, the README template version badge, the shared downstream README skeleton, the cleanup workflow assets, the README alignment workflow assets, and the runtime-policy README-generation assets those README workflows require. -The sync does not update project-owned source, tests, PSScriptAnalyzer settings, Pester configuration, CI workflows, runtime policy, or project-specific ADRs. +The sync does not update project-owned source, tests, PSScriptAnalyzer settings, Pester configuration, CI workflows, or project-specific ADRs. It delivers runtime-policy README-generation assets only because the shared downstream README workflow depends on them, and it still does not automatically rewrite an existing downstream README. A repo-local Codex skill is also provided at `.codex/skills/downstream-guidance-sync/SKILL.md` so agents can operate the sync script through the intended audit, branch, validation, commit, and pull request workflow. @@ -176,7 +188,7 @@ powershell.exe -NoProfile -File .\scripts\Invoke-TemplateGuidanceSync.ps1 -Path Audit mode is the default and reports drift without changing files. To apply the safe sync set, create or switch the downstream repo to a non-main branch first, then run: ```powershell -git -C ..\downstream-repo switch -c chore/sync-template-guidance-0.1.0 +git -C ..\downstream-repo switch -c chore/sync-template-guidance-0.3.0 powershell.exe -NoProfile -File .\scripts\Invoke-TemplateGuidanceSync.ps1 -Path ..\downstream-repo -Apply ``` diff --git a/VERSION b/VERSION index 0ea3a94..9325c3c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.3.0 \ No newline at end of file diff --git a/docs/agent-workflows.md b/docs/agent-workflows.md index d869cbd..42e730c 100644 --- a/docs/agent-workflows.md +++ b/docs/agent-workflows.md @@ -10,13 +10,18 @@ Repo-local skills live under `.codex/skills/`. They tell compatible agents how t | Workflow | Use When | Skill | Control | Validation | | --- | --- | --- | --- | --- | -| Downstream guidance sync | A repository created from this template needs current AI guidance, guardrail docs, and ADR scaffold guidance. | `.codex/skills/downstream-guidance-sync/SKILL.md` | `scripts/Invoke-TemplateGuidanceSync.ps1` | Audit output, downstream diff review, downstream validation | -| Runtime policy update | The template needs coordinated Windows PowerShell, GitHub Actions runner, or pinned tooling updates. | `.codex/skills/runtime-policy-update/SKILL.md` | `eng/runtime-policy.json` | `scripts/Update-GeneratedMarkdown.ps1 -Check`, `scripts/Test-VersionPolicy.ps1`, `scripts/Invoke-RepoChecks.ps1 -IncludeTemplates` | -| Template version release | The template version, changelog, README badge, release tag, and GitHub Release need to be prepared or finalized. | `.codex/skills/template-version-release/SKILL.md` | `VERSION`, `CHANGELOG.md`, README template badge, annotated `vX.Y.Z` tags, and GitHub Releases | `scripts/Test-TemplateVersion.ps1` and release PR review | +| Change delivery workflow | Ordinary repository work needs consistent branch, changelog, validation, PR, and post-merge cleanup discipline without inventing repo-specific process. | `.codex/skills/change-delivery-workflow/SKILL.md` | Repository guidance, Git state, repo-specific validators, diff review, and human review | Repo-specific validation entrypoints, staged diff review, and PR review | +| Downstream repo cleanup | A repository was just created from this template and needs immediate first-run normalization before project-specific work begins. | `.codex/skills/downstream-repo-cleanup/SKILL.md` | `scripts/Initialize-DownstreamRepo.ps1` | Audit output, downstream diff review, `scripts/Invoke-RepoChecks.ps1` | +| Downstream guidance sync | An existing downstream repository needs current AI guidance, guardrail docs, ADR scaffold guidance, or newly added README workflow assets from the template. | `.codex/skills/downstream-guidance-sync/SKILL.md` | `scripts/Invoke-TemplateGuidanceSync.ps1` | Audit output, downstream diff review, downstream validation | +| README alignment | A downstream repository README needs to be audited or realigned to the shared portfolio skeleton after cleanup. | `.codex/skills/readme-alignment/SKILL.md` | `scripts/Invoke-ReadmeAlignment.ps1` with `templates/downstream/README.md` | README audit output, downstream diff review, `scripts/Invoke-RepoChecks.ps1` | +| Runtime policy update | The template needs coordinated PowerShell, Ubuntu, GitHub Actions runner, or pinned tooling updates. | `.codex/skills/runtime-policy-update/SKILL.md` | `eng/runtime-policy.json` | `scripts/Update-GeneratedMarkdown.ps1 -Check`, `scripts/Test-VersionPolicy.ps1`, `scripts/Invoke-RepoChecks.ps1 -IncludeTemplates` | +| Template version release | The template version, changelog, README badge, lightweight release tag, and GitHub Release need to be prepared or finalized. | `.codex/skills/template-version-release/SKILL.md` | `VERSION`, `CHANGELOG.md`, README template badge, lightweight `vX.Y.Z` tags on merged `main` commits under the existing merge-commit ruleset, and GitHub Releases | `scripts/Test-TemplateVersion.ps1` and release PR review | ## Operating Model -Use the repo-local skill when the task matches one of the workflow areas above. The skill should guide the agent through the expected branch, script, validation, diff review, commit, and pull request process. +Use the repo-local skill when the task matches one of the workflow areas above. The skill should guide the agent through the expected branch or post-create state, script, validation, diff review, commit, and pull request process. + +The change delivery workflow is intentionally process-oriented. It standardizes branch, changelog, release-decision, PR, and cleanup behavior for everyday repository work, while still relying on repo guidance, existing validators, diff review, and human review as the controls. Before planning template maintenance work, agents can run `scripts/Get-TemplateHealth.ps1` for a quick report of generated Markdown, runtime policy, template version metadata, workflow discoverability, and Git release posture. @@ -28,6 +33,8 @@ Pester is the repository validation standard for repo-local skill behavior and d Repo-local skills do not make autonomous changes acceptable without review. They should help agents apply known workflows consistently, not broaden the task scope. -The downstream guidance sync workflow is intentionally narrow. It may update AI guidance, guardrail documentation, `docs/decisions/README.md`, and the README template-version badge in downstream repositories. It must not be used to overwrite downstream source code, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, development-environment configuration, runtime policy, module manifests, scaffolds, or numbered project-specific ADRs unless that broader work is requested as a separate repo-specific change. +The downstream repo cleanup workflow is intentionally a first-run normalization step. It should be run immediately after a repository is created from this template and before project-specific source, tests, ADRs, or CI changes are added. It may remove template-maintainer artifacts and rewrite inherited guidance into downstream form, but it must not invent project-specific business logic, tests, docs, or unrelated CI changes. + +The downstream guidance sync workflow is intentionally narrow. It may update AI guidance, guardrail documentation, `docs/decisions/README.md`, the README template-version badge, `docs/agent-workflows.md`, the shared downstream README skeleton, the README alignment workflow assets, and the runtime-policy README-generation assets required by that workflow. It must not be used to overwrite downstream source code, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, development-environment configuration, module manifests, scaffolds other than the README workflow assets, or numbered project-specific ADRs unless that broader work is requested as a separate repo-specific change. Cleanup itself still runs from the downstream repository. Runtime policy updates and template version releases are template-repository workflows. Downstream repositories should adopt those changes only through explicit, reviewed project work. diff --git a/docs/decisions/0002-downstream-readme-workflow.md b/docs/decisions/0002-downstream-readme-workflow.md new file mode 100644 index 0000000..ade9b5e --- /dev/null +++ b/docs/decisions/0002-downstream-readme-workflow.md @@ -0,0 +1,51 @@ +# 0007 - Downstream README Alignment Workflow + +## Status + +Accepted + +## Context + +Downstream repositories created from `powershell-dev-template` should present a recognizable portfolio structure without forcing every repository into identical prose. Earlier cleanup support rewrote downstream guidance and preserved runtime-policy README-generation assets, but downstream README structure still depended on inline cleanup logic or manual editing. + +The template now needs a durable shared downstream README skeleton that can be used in two different situations: + +- immediate post-create cleanup for brand new downstream repositories +- later README realignment for existing downstream repositories + +Because the downstream README uses generated Markdown blocks, the skeleton cannot be treated as an isolated Markdown file. The runtime-policy assets that drive those blocks must remain aligned as well. + +## Decision + +Store a shared downstream README skeleton in a template-owned non-root location and treat it as the canonical downstream README structure. + +Use that skeleton in two ways: + +1. `scripts/Initialize-DownstreamRepo.ps1` replaces the inherited root `README.md` with the skeleton during the immediate post-create cleanup window. +2. `scripts/Invoke-ReadmeAlignment.ps1` audits or aligns existing downstream `README.md` files to the same skeleton while preserving repo-specific sections. + +Keep the downstream skeleton generator-driven. It retains generated Markdown blocks for runtime and tooling sections, and it preserves the template version badge contract. + +Expand downstream guidance sync so older downstream repositories can receive the README skeleton, the README alignment workflow assets, and the runtime-policy README-generation assets required to keep the generated blocks functional. + +For portfolio downstream repositories, `Portfolio Context` and `Template Versioning` remain part of the shared structure and are intended to be customized rather than removed. + +## Alternatives Considered + +Keep downstream README creation fully manual. + +Continue generating downstream README content inline inside cleanup. + +Make the downstream skeleton fully static and remove generated Markdown dependencies. + +Create a single AI-only workflow that handles both first-run cleanup and later README restructuring. + +## Consequences + +The shared downstream README structure now has a durable source of truth that future maintainers do not need to rediscover from merged diffs. + +Brand new downstream repositories continue to normalize deterministically during cleanup without needing AI assistance. + +Existing downstream repositories can adopt the shared structure later through a reviewable script-and-skill workflow. + +Downstream guidance sync now has a slightly broader allowlist because the README workflow depends on the skeleton and runtime-policy README-generation assets, but it still does not rewrite downstream `README.md` automatically. diff --git a/docs/template-evolution.md b/docs/template-evolution.md index a9cea46..10cfc63 100644 --- a/docs/template-evolution.md +++ b/docs/template-evolution.md @@ -8,7 +8,7 @@ Its purpose is to provide a consistent starting point for development environmen Development templates age in several dimensions: -- PowerShell and base container images change +- PowerShell and Windows platform tooling change - tooling and extensions change - security assumptions change - linting and testing conventions mature @@ -56,22 +56,29 @@ Use version changes to communicate the impact of template evolution: - Minor versions: new template capabilities, scaffolds, workflows, tooling support, or conventions that add value without intentionally breaking existing template consumers. - Patch versions: corrections, documentation clarifications, policy wording updates, validation fixes, and low-risk maintenance that preserve the current template contract. -Update `VERSION`, the README template-version badge, and `CHANGELOG.md` together when preparing a release. Keep unreleased maintenance notes under the `Unreleased` heading until a release version is chosen. Use `scripts/Test-TemplateVersion.ps1` to verify release metadata before opening a PR. After the release PR is merged, create an annotated `vX.Y.Z` tag on `main`, push the tag, validate tag placement, and publish a GitHub Release from that tag using the matching changelog section as the release body. Agents should use `.codex/skills/template-version-release/SKILL.md` when coordinating this workflow. +Update `VERSION`, the README template-version badge, and `CHANGELOG.md` together when preparing a release. Keep unreleased maintenance notes under the `Unreleased` heading until a release version is chosen. Use `scripts/Test-TemplateVersion.ps1` to verify release metadata before opening a PR. After the release PR is merged, create a lightweight `vX.Y.Z` tag on `main`, push the tag, validate that it points at the merged release commit, and publish a GitHub Release from that tag using the matching changelog section as the release body. Agents should use `.codex/skills/template-version-release/SKILL.md` when coordinating this workflow. + +### Lightweight Release Tags + +Template releases use lightweight tags created on merged `main` commits. This matches the historical release behavior more closely than a signed-tag requirement, while the repository ruleset that permits only merge commits remains part of the release guardrail. + +`scripts/Test-TemplateVersion.ps1 -CheckTag` validates that the expected lightweight tag exists and points at `HEAD`. The final release check is still to confirm GitHub shows the pushed tag as verified. ## Runtime Update Workflow -The source of truth for the Windows PowerShell runtime contract, CI runner, and pinned tooling versions is `eng/runtime-policy.json`. GitHub Actions, VS Code settings, module manifests, and generated Markdown must agree with that policy. +Dependabot Docker pull requests are treated as runtime upgrade notifications, not as ordinary one-file dependency bumps. + +The source of truth for the current runtime, CI runner, and pinned PowerShell tooling versions is `eng/runtime-policy.json`. Files such as the Dockerfile, GitHub Actions workflow, and generated Markdown blocks should agree with that policy. Agents should use `.codex/skills/runtime-policy-update/SKILL.md` when coordinating this workflow. -When the runtime, runner, or pinned tooling changes: +When the pinned PowerShell or Ubuntu runtime changes: - update `eng/runtime-policy.json` first -- align CI, VS Code settings, and both module manifests in the same deliberate change +- update the Dockerfile, CI runner, and pinned tooling only as part of the same deliberate runtime change - run `scripts/Update-GeneratedMarkdown.ps1` to refresh managed documentation blocks -- run `scripts/Invoke-RepoChecks.ps1 -IncludeTemplates` under Windows PowerShell 5.1 before merging +- run `scripts/Invoke-RepoChecks.ps1 -IncludeTemplates` before merging - leave historical version references in `CHANGELOG.md` unchanged unless a release note is being added -- record any change to the Windows PowerShell 5.1 Desktop-only environment boundary in an ADR ### Generated Markdown Blocks @@ -86,13 +93,13 @@ Some Markdown content is managed by generated block markers: Do not edit the content inside those blocks by hand. Update `eng/runtime-policy.json`, then run: ```powershell -powershell.exe -NoProfile -File ./scripts/Update-GeneratedMarkdown.ps1 +powershell.exe -NoProfile -File .\\scripts\\Update-GeneratedMarkdown.ps1 ``` To check whether generated blocks are current without changing files, run: ```powershell -powershell.exe -NoProfile -File ./scripts/Update-GeneratedMarkdown.ps1 -Check +powershell.exe -NoProfile -File .\\scripts\\Update-GeneratedMarkdown.ps1 -Check ``` ### Version Policy Validation @@ -100,16 +107,16 @@ powershell.exe -NoProfile -File ./scripts/Update-GeneratedMarkdown.ps1 -Check The repository checks validate that policy-managed values stay aligned across configuration and documentation: ```powershell -powershell.exe -NoProfile -File ./scripts/Invoke-RepoChecks.ps1 -IncludeTemplates +powershell.exe -NoProfile -File .\\scripts\\Invoke-RepoChecks.ps1 -IncludeTemplates ``` For focused troubleshooting, run the version policy check directly: ```powershell -powershell.exe -NoProfile -File ./scripts/Test-VersionPolicy.ps1 +powershell.exe -NoProfile -File .\\scripts\\Test-VersionPolicy.ps1 ``` -The validation covers the development environment, CI runner, pinned tooling, and module compatibility metadata so the Windows PowerShell 5.1 Desktop contract cannot drift silently. +The validation intentionally focuses on the development environment, CI runner, and pinned tooling. PowerShell module compatibility metadata, such as `PowerShellVersion` in module manifests, remains a compatibility decision for each project and is not automatically changed by this workflow. ## What This Means For Downstream Repositories @@ -119,7 +126,7 @@ This template may continue to evolve after a downstream repository is created, b ### Downstream Guidance Sync -AI guidance and guardrail documentation are the default downstream sync targets because they describe how AI-assisted changes should be generated, reviewed, and validated. Keeping those files aligned helps downstream repositories inherit the current operating model without replacing project-specific implementation choices. +AI guidance and guardrail documentation are the default downstream sync targets because they describe how AI-assisted changes should be generated, reviewed, and validated. Keeping those files aligned helps downstream repositories inherit the current operating model without replacing project-specific implementation choices. For repositories that predate the cleanup workflow, sync may also deliver the cleanup script, cleanup skill, shared downstream README skeleton, README alignment workflow assets, and the runtime-policy README-generation assets that support that README workflow. The downstream guidance sync process is intentionally narrow. It may update: @@ -129,16 +136,16 @@ The downstream guidance sync process is intentionally narrow. It may update: - `docs/decisions/README.md` as the ADR scaffold - the README template-version badge -It does not update source code, tests, Pester configuration, PSScriptAnalyzer settings, GitHub Actions workflows, development-environment configuration, runtime policy, module manifests, scaffolds, or numbered project-specific ADRs. Those files become downstream-owned after repository creation. +It does not update source code, tests, Pester configuration, PSScriptAnalyzer settings, GitHub Actions workflows, development-environment configuration, module manifests, scaffolds other than the cleanup and README workflow assets, or numbered project-specific ADRs. The one runtime-policy exception is the narrow set of README-generation assets required to keep the shared downstream README skeleton functional in older downstream repositories. Those files become downstream-owned after repository creation or, for cleanup-owned guidance surfaces, after the immediate cleanup step. -Any broader synchronization should be explicit, repo-specific, and reviewed as normal project work. +Cleanup itself remains a downstream-repository action performed through `scripts/Initialize-DownstreamRepo.ps1`. Any broader synchronization should be explicit, repo-specific, and reviewed as normal project work. ## Maintenance Mindset For a quick maintainer posture check, run: ```powershell -powershell.exe -NoProfile -File ./scripts/Get-TemplateHealth.ps1 +powershell.exe -NoProfile -File .\\scripts\\Get-TemplateHealth.ps1 ``` The health report summarizes generated Markdown, runtime policy, template version metadata, repo-local agent workflow discoverability, and Git release posture. It is a report by default, not a replacement for the full repository checks. @@ -146,7 +153,7 @@ The health report summarizes generated Markdown, runtime policy, template versio The template should be reviewed periodically with questions such as: - does the documentation still match the implementation -- do the container and editor configurations still reflect current intent +- do the Windows-native editor and validation configurations still reflect current intent - are the security assumptions still valid - are the default tools and conventions still useful - has the template become clearer or more confusing over time diff --git a/scripts/Get-TemplateHealth.ps1 b/scripts/Get-TemplateHealth.ps1 index 0420a44..bd592d1 100644 --- a/scripts/Get-TemplateHealth.ps1 +++ b/scripts/Get-TemplateHealth.ps1 @@ -20,7 +20,7 @@ param( [Parameter()] [ValidateNotNullOrEmpty()] - [string]$RepoRoot, + [string]$RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path, [Parameter()] [switch]$AsJson, @@ -31,9 +31,6 @@ param( $ErrorActionPreference = 'Stop' -if ([string]::IsNullOrWhiteSpace($RepoRoot)) { - $RepoRoot = Join-Path -Path $PSScriptRoot -ChildPath '..' -} $resolvedRepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path $items = [System.Collections.Generic.List[object]]::new() @@ -198,18 +195,9 @@ function Invoke-GitText { [string[]]$Arguments ) - $previousErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Continue' - $result = & git -C $resolvedRepoRoot @Arguments 2>&1 - $exitCode = $LASTEXITCODE - } - finally { - $ErrorActionPreference = $previousErrorActionPreference - } - + $result = & git -C $resolvedRepoRoot @Arguments 2>&1 return [pscustomobject]@{ - ExitCode = $exitCode + ExitCode = $LASTEXITCODE Text = ($result | Out-String).Trim() } } @@ -220,7 +208,10 @@ Invoke-TemplateHealthScript -Area 'Template Version' -Name 'Template version met $agentArea = 'Agent Workflows' $agentFiles = @( + @{ Name = 'Change delivery workflow skill'; Path = '.codex/skills/change-delivery-workflow/SKILL.md' }, + @{ Name = 'Downstream repo cleanup skill'; Path = '.codex/skills/downstream-repo-cleanup/SKILL.md' }, @{ Name = 'Downstream guidance sync skill'; Path = '.codex/skills/downstream-guidance-sync/SKILL.md' }, + @{ Name = 'README alignment skill'; Path = '.codex/skills/readme-alignment/SKILL.md' }, @{ Name = 'Runtime policy update skill'; Path = '.codex/skills/runtime-policy-update/SKILL.md' }, @{ Name = 'Template version release skill'; Path = '.codex/skills/template-version-release/SKILL.md' }, @{ Name = 'Agent workflow documentation'; Path = 'docs/agent-workflows.md' } @@ -233,8 +224,14 @@ foreach ($agentFile in $agentFiles) { $agentReferences = @( @{ Name = 'README workflow pointer'; Path = 'README.md'; Text = 'docs/agent-workflows.md' }, @{ Name = 'Workflow health pointer'; Path = 'docs/agent-workflows.md'; Text = 'scripts/Get-TemplateHealth.ps1' }, + @{ Name = 'Change delivery skill reference'; Path = 'docs/agent-workflows.md'; Text = '.codex/skills/change-delivery-workflow/SKILL.md' }, + @{ Name = 'Cleanup skill reference'; Path = 'docs/agent-workflows.md'; Text = '.codex/skills/downstream-repo-cleanup/SKILL.md' }, + @{ Name = 'Cleanup script reference'; Path = 'docs/agent-workflows.md'; Text = 'scripts/Initialize-DownstreamRepo.ps1' }, @{ Name = 'Downstream skill reference'; Path = 'docs/agent-workflows.md'; Text = '.codex/skills/downstream-guidance-sync/SKILL.md' }, @{ Name = 'Downstream sync script reference'; Path = 'docs/agent-workflows.md'; Text = 'scripts/Invoke-TemplateGuidanceSync.ps1' }, + @{ Name = 'README alignment skill reference'; Path = 'docs/agent-workflows.md'; Text = '.codex/skills/readme-alignment/SKILL.md' }, + @{ Name = 'README alignment script reference'; Path = 'docs/agent-workflows.md'; Text = 'scripts/Invoke-ReadmeAlignment.ps1' }, + @{ Name = 'README skeleton reference'; Path = 'docs/agent-workflows.md'; Text = 'templates/downstream/README.md' }, @{ Name = 'Runtime skill reference'; Path = 'docs/agent-workflows.md'; Text = '.codex/skills/runtime-policy-update/SKILL.md' }, @{ Name = 'Runtime policy reference'; Path = 'docs/agent-workflows.md'; Text = 'eng/runtime-policy.json' }, @{ Name = 'Release skill reference'; Path = 'docs/agent-workflows.md'; Text = '.codex/skills/template-version-release/SKILL.md' }, diff --git a/scripts/Initialize-DownstreamRepo.ps1 b/scripts/Initialize-DownstreamRepo.ps1 new file mode 100644 index 0000000..f0f7e27 --- /dev/null +++ b/scripts/Initialize-DownstreamRepo.ps1 @@ -0,0 +1,801 @@ +<# +.SYNOPSIS + Normalizes a newly created downstream repository from powershell-dev-template. + +.DESCRIPTION + Audits or applies the first-run cleanup workflow intended for repositories + created from powershell-dev-template. The workflow removes template-maintainer + artifacts, rewrites inherited guidance into downstream form, and preserves + the README template version badge as a visible baseline marker. +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path, + + [Parameter()] + [ValidateSet('script', 'module', 'hybrid')] + [string]$ProjectType = 'script', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$RepositoryName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$ModuleName, + + [Parameter()] + [switch]$Apply, + + [Parameter()] + [ValidateSet('Text', 'Json')] + [string]$OutputFormat = 'Text' +) + +$ErrorActionPreference = 'Stop' + +function Resolve-AbsolutePath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + return (Resolve-Path -LiteralPath $Path).Path +} + +function Get-RepoPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$BasePath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RelativePath + ) + + return (Join-Path -Path $BasePath -ChildPath $RelativePath) +} + +function Get-RelativeRepoFilePath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$BasePath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + $baseUri = [Uri]((Resolve-Path -LiteralPath $BasePath).Path.TrimEnd('\') + '\') + $targetUri = [Uri](Resolve-Path -LiteralPath $Path).Path + return $baseUri.MakeRelativeUri($targetUri).ToString().Replace('/', '\').Replace('\', '/') +} + +function Test-GitRepository { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + & git -C $Path rev-parse --show-toplevel 2>$null | Out-Null + return ($LASTEXITCODE -eq 0) +} + +function Get-FileContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return $null + } + + return Get-Content -Raw -LiteralPath $Path +} + +function Write-Utf8File { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Content + ) + + $parent = Split-Path -Path $Path -Parent + if ($parent -and (-not (Test-Path -LiteralPath $parent))) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + + [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false)) +} + +function Add-ReportItem { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.List[object]]$Items, + + [Parameter(Mandatory)] + [ValidateSet('Remove', 'Rewrite', 'Rename', 'Keep', 'ManualFollowUp')] + [string]$Action, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Reason, + + [Parameter()] + [AllowEmptyString()] + [string]$Status = '' + ) + + $Items.Add([pscustomobject]@{ + Action = $Action + Path = $Path + Reason = $Reason + Status = $Status + }) +} + +function Remove-RepoItem { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return + } + + if ($PSCmdlet.ShouldProcess($Path, 'Remove template-maintainer artifact')) { + Remove-Item -LiteralPath $Path -Recurse -Force + } +} + +function Rename-RepoItem { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Destination + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return + } + + $destinationParent = Split-Path -Path $Destination -Parent + if ($destinationParent -and (-not (Test-Path -LiteralPath $destinationParent))) { + New-Item -ItemType Directory -Path $destinationParent -Force | Out-Null + } + + if ($PSCmdlet.ShouldProcess($Path, ('Rename to {0}' -f $Destination))) { + Move-Item -LiteralPath $Path -Destination $Destination -Force + } +} + +function Get-TemplateVersion { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RepoPath + ) + + $versionPath = Get-RepoPath -BasePath $RepoPath -RelativePath 'VERSION' + if (Test-Path -LiteralPath $versionPath -PathType Leaf) { + return (Get-Content -Raw -LiteralPath $versionPath).Trim() + } + + $readmePath = Get-RepoPath -BasePath $RepoPath -RelativePath 'README.md' + $readmeContent = Get-FileContent -Path $readmePath + if ($null -eq $readmeContent) { + return $null + } + + $match = [regex]::Match($readmeContent, '!\[Template Version\]\(https://img\.shields\.io/badge/template-(?[^)]+)-blue\)') + if ($match.Success) { + return $match.Groups['Version'].Value + } + + return $null +} + +function Get-TemplateVersionBadgeLine { + [CmdletBinding()] + param( + [Parameter()] + [string]$TemplateVersion + ) + + if ([string]::IsNullOrWhiteSpace($TemplateVersion)) { + return $null + } + + return '![Template Version](https://img.shields.io/badge/template-{0}-blue)' -f $TemplateVersion +} + +function Get-SafeHelperName { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Name + ) + + $safe = ($Name -replace '[^A-Za-z0-9]', '') + if ([string]::IsNullOrWhiteSpace($safe)) { + return 'Module' + } + + return $safe +} + +function Get-DownstreamReadmeContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RepoRoot, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RepoName, + + [Parameter()] + [string]$TemplateVersion + ) + + $skeletonPath = Get-RepoPath -BasePath $RepoRoot -RelativePath 'templates/downstream/README.md' + $skeletonContent = Get-FileContent -Path $skeletonPath + if ($null -eq $skeletonContent) { + throw ('Downstream README skeleton not found: {0}' -f $skeletonPath) + } + + $badgeLine = Get-TemplateVersionBadgeLine -TemplateVersion $TemplateVersion + $rendered = $skeletonContent.Replace('{{REPOSITORY_NAME}}', $RepoName) + $rendered = $rendered.Replace('{{TEMPLATE_VERSION_BADGE}}', $badgeLine) + $rendered = $rendered.Replace('{{REPOSITORY_SUMMARY}}', 'Add a concise repository summary that explains what this project does and why it exists.') + + return ($rendered.TrimEnd("`r", "`n") + "`n") +} + +function Get-DownstreamAgentsContent { + [CmdletBinding()] + param() + + return @' +# AGENTS.md + +## Repository Instructions + +This repository's primary AI guidance is maintained in: + +```text +.github/copilot-instructions.md +``` + +Before performing coding, review, documentation, test, automation, or repository-maintenance work, coding agents must read and follow `.github/copilot-instructions.md`. + +That file is the authoritative instruction source for: + +- AI governance requirements +- conflict resolution rules +- code generation standards +- complexity management guidance +- PowerShell compatibility requirements +- external service guidance +- commit message conventions +- review expectations + +If `.github/copilot-instructions.md` is missing or unavailable, stop and report that the repository guidance cannot be loaded rather than guessing. + +If guidance in this file conflicts with `.github/copilot-instructions.md`, `.github/copilot-instructions.md` is authoritative. + +## Repo-Local Skills + +Repo-local Codex skills are stored under `.codex/skills/`. + +For immediate post-create normalization of this downstream repository, agents should use `.codex/skills/downstream-repo-cleanup/SKILL.md` together with `scripts/Initialize-DownstreamRepo.ps1`. + +For downstream AI guidance synchronization from `powershell-dev-template`, agents should use `.codex/skills/downstream-guidance-sync/SKILL.md` together with `scripts/Invoke-TemplateGuidanceSync.ps1` instead of manually copying guidance files. + +For shared README alignment after cleanup, agents should use `.codex/skills/readme-alignment/SKILL.md` together with `scripts/Invoke-ReadmeAlignment.ps1`. +'@ +} + +function Get-DownstreamAgentWorkflowsContent { + [CmdletBinding()] + param() + + return @' +# Agent Workflows + +This repository includes repo-local agent workflows for downstream setup and ongoing template guidance alignment. + +The workflows are designed around a simple rule: agents may coordinate the work, but deterministic scripts, Pester tests, and human review remain the controls that make the work reliable. + +Repo-local skills live under `.codex/skills/`. They tell compatible agents how to use the repository's existing scripts, validation commands, and review expectations. They are not a substitute for reading the repository guidance, inspecting diffs, or opening reviewed pull requests. + +## Workflow Index + +| Workflow | Use When | Skill | Control | Validation | +| --- | --- | --- | --- | --- | +| Change delivery workflow | Ordinary repository work needs consistent branch, changelog, validation, PR, and post-merge cleanup discipline. | .codex/skills/change-delivery-workflow/SKILL.md | Repository guidance, Git state, repo-specific validators, diff review, and human review | Repo-specific validation, staged diff review, and PR review | +| Downstream repo cleanup | A repository was just created from `powershell-dev-template` and needs immediate first-run normalization before project-specific work begins. | `.codex/skills/downstream-repo-cleanup/SKILL.md` | `scripts/Initialize-DownstreamRepo.ps1` | Audit output, downstream diff review, `scripts/Invoke-RepoChecks.ps1` | +| Downstream guidance sync | A downstream repository needs current AI guidance, guardrail docs, README workflow assets, and ADR scaffold guidance from `powershell-dev-template`. | `.codex/skills/downstream-guidance-sync/SKILL.md` | `scripts/Invoke-TemplateGuidanceSync.ps1` | Audit output, downstream diff review, downstream validation | +| README alignment | A downstream repository README needs to be audited or realigned to the shared portfolio skeleton after cleanup. | `.codex/skills/readme-alignment/SKILL.md` | `scripts/Invoke-ReadmeAlignment.ps1` | README audit output, downstream diff review, `scripts/Invoke-RepoChecks.ps1` | + +## Operating Model + +Use the repo-local skill when the task matches one of the workflow areas above. The skill should guide the agent through the expected audit, apply, validation, diff review, commit, and pull request process. + +The change delivery workflow is intentionally process-oriented. It standardizes branch, changelog, release-decision, pull request, and cleanup behavior for everyday downstream work while still relying on repo guidance, repo validators, diff review, and human review as the controls. + +The agent should not invent a separate process when a deterministic script already exists. If a script reports an error, the correct response is to inspect the failure, fix the cause, and rerun the documented validation. Recovery should still be explicit, reviewable, and consistent with the repository guidance. + +## Boundaries + +Repo-local skills do not make autonomous changes acceptable without review. They should help agents apply known workflows consistently, not broaden the task scope. + +The downstream cleanup workflow is intentionally a first-run normalization step. It may remove template-maintainer artifacts and rewrite inherited guidance into downstream form, but it must not invent business logic, project-specific tests, repo-specific documentation, or unrelated CI changes. + +The downstream guidance sync workflow is intentionally narrow. It may update AI guidance, guardrail documentation, `docs/decisions/README.md`, the README template-version badge, the shared downstream README skeleton, the README alignment workflow assets, and the runtime-policy README-generation assets required by that workflow. It must not be used to overwrite downstream source code, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, Dev Container files, module manifests, scaffolds outside the README workflow assets, or numbered project-specific ADRs unless that broader work is requested as a separate repo-specific change. +'@ +} + +function Get-DownstreamDecisionsReadmeContent { + [CmdletBinding()] + param() + + return @' +# Architecture Decision Records + +This directory captures durable repository decisions that future maintainers should be able to understand without reconstructing pull request discussion. + +ADRs are not required for routine documentation updates, patch fixes, or implementation plans that are fully explained by the pull request. Use a decision record when a change introduces or changes a durable repository capability, workflow policy, ownership boundary, or non-obvious tradeoff. + +Good ADR candidates include decisions that affect: + +- repository-specific architecture or operational boundaries +- AI-assisted development workflow or governance +- validation and CI expectations +- runtime or tooling policy +- release policy +- ownership boundaries between this repository and external systems + +## Format + +Use a short, numbered Markdown file with this structure: + +```markdown +# 0001 - Decision Title + +## Status + +Accepted + +## Context + +## Decision + +## Alternatives Considered + +## Consequences +``` + +Keep ADRs brief and outcome-focused. They should explain durable reasoning, not replay every implementation step. +'@ +} + +function Get-DownstreamCopilotInstruction { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExistingContent + ) + + $updatedContent = $ExistingContent + $updatedContent = $updatedContent -replace 'This repository is a GitHub template for PowerShell projects\.', 'This repository was created from the powershell-dev-template GitHub template and is now treated as its own PowerShell project.' + + $pattern = '(?s)## Repo-Local Skills\r?\n.*?\r?\n## PowerShell Compatibility' + $replacement = @' +## Repo-Local Skills + +Repo-local Codex skills are stored under `.codex/skills/`. When asked to normalize a newly created downstream repository from `powershell-dev-template`, use `.codex/skills/downstream-repo-cleanup/SKILL.md` and operate `scripts/Initialize-DownstreamRepo.ps1` through the documented audit, apply, validation, and diff-review workflow. Use this only as an immediate post-create cleanup step before project-specific work begins. + +When asked to synchronize downstream AI guidance from `powershell-dev-template`, use `.codex/skills/downstream-guidance-sync/SKILL.md` and operate `scripts/Invoke-TemplateGuidanceSync.ps1` through the documented audit, branch, validation, commit, and pull request workflow. Do not manually edit downstream guidance files outside the sync script allowlist unless the user explicitly asks for manual repair after a script failure. + +When asked to audit or align a downstream README to the shared portfolio skeleton, use `.codex/skills/readme-alignment/SKILL.md` and operate `scripts/Invoke-ReadmeAlignment.ps1` through the documented audit, branch, validation, diff-review, and commit workflow. + +Runtime policy, generated Markdown, CI, tests, scaffolds, and release metadata become downstream-owned after cleanup. Treat changes to those surfaces as normal repository work, except when downstream guidance sync is intentionally delivering the README workflow assets that depend on `eng/runtime-policy.json` and `scripts/Update-GeneratedMarkdown.ps1`. + +For ordinary downstream repository changes after cleanup, use .codex/skills/change-delivery-workflow/SKILL.md to coordinate sandbox escalation, non-main branches, changelog updates, release decisions, commits, pull requests, and post-merge cleanup. + +When adding or updating repo-local skills, add or update Pester coverage in `tests/unit/SkillScaffold.Tests.ps1` for the skill file, metadata, required references, and agent discoverability. The Codex `quick_validate.py` helper may be used as an optional authoring check, but Pester is the repository validation standard. + +## PowerShell Compatibility +'@ + + if (-not [regex]::IsMatch($updatedContent, $pattern)) { + throw 'Unable to rewrite the repo-local skills section in .github/copilot-instructions.md.' + } + + $updatedContent = [regex]::Replace($updatedContent, $pattern, $replacement) + return ($updatedContent.TrimEnd("`r", "`n") + "`n") +} + +function Get-DownstreamModuleManifestContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExistingContent, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ModuleFileName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RepoName + ) + + $updated = $ExistingContent + $updated = $updated -replace "RootModule = 'TemplateModule\.psm1'", ("RootModule = '{0}'" -f $ModuleFileName) + $updated = $updated -replace "Author = 'Template Author'", "Author = 'Repository Maintainer'" + $updated = $updated -replace "CompanyName = 'Template Company'", "CompanyName = ''" + $updated = $updated.Replace("Copyright = '(c) Template Author. All rights reserved.'", ("Copyright = '(c) {0}. Update ownership before release.'" -f $RepoName)) + $updated = $updated -replace "Description = 'Module scaffold for repositories created from this template\. Rename and update metadata\.'", ("Description = 'PowerShell module for {0}. Update metadata before release.'" -f $RepoName) + $updated = $updated -replace "Tags = @\('PowerShell', 'Template'\)", ("Tags = @('PowerShell', '{0}')" -f $RepoName) + + return ($updated.TrimEnd("`r", "`n") + "`n") +} + +function Get-DownstreamModuleEntryPointContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExistingContent, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ModuleNameValue + ) + + $helperName = Get-SafeHelperName -Name $ModuleNameValue + $updated = $ExistingContent -replace 'Import-TemplateModuleScript', ('Import-{0}Script' -f $helperName) + $updated = $updated -replace 'TemplateModule', $ModuleNameValue + return ($updated.TrimEnd("`r", "`n") + "`n") +} + +function Remove-StaleGitKeepFile { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RepoPath + ) + + $gitKeepFiles = Get-ChildItem -LiteralPath $RepoPath -Filter '.gitkeep' -Recurse -File -ErrorAction SilentlyContinue + foreach ($gitKeepFile in $gitKeepFiles) { + $siblings = Get-ChildItem -LiteralPath $gitKeepFile.Directory.FullName -Force | Where-Object { $_.Name -ne '.gitkeep' } + if ($siblings.Count -gt 0 -and $PSCmdlet.ShouldProcess($gitKeepFile.FullName, 'Remove stale .gitkeep')) { + Remove-Item -LiteralPath $gitKeepFile.FullName -Force + } + } +} + +if (($ProjectType -in @('module', 'hybrid')) -and [string]::IsNullOrWhiteSpace($ModuleName)) { + throw ('ModuleName is required when ProjectType is "{0}".' -f $ProjectType) +} + +$resolvedRepoRoot = Resolve-AbsolutePath -Path $RepoRoot +if (-not (Test-GitRepository -Path $resolvedRepoRoot)) { + throw ('Cleanup workflow requires a Git repository: {0}' -f $resolvedRepoRoot) +} + +if ([string]::IsNullOrWhiteSpace($RepositoryName)) { + $RepositoryName = Split-Path -Path $resolvedRepoRoot -Leaf +} + +$reportItems = [System.Collections.Generic.List[object]]::new() +$manualFollowUp = [System.Collections.Generic.List[string]]::new() +$stopReasons = [System.Collections.Generic.List[string]]::new() + +$knownTemplateTests = @( + 'tests/unit/Invoke-TemplateGuidanceSync.Tests.ps1' + 'tests/unit/TemplateHealth.Tests.ps1' + 'tests/unit/TemplateScaffold.Tests.ps1' + 'tests/unit/TemplateVersion.Tests.ps1' + 'tests/unit/SkillScaffold.Tests.ps1' + 'tests/unit/Initialize-DownstreamRepo.Tests.ps1' + 'tests/unit/Invoke-ReadmeAlignment.Tests.ps1' +) + +$expectedSourceFiles = @( + 'src/.gitkeep' + 'src/README.md' + 'src/TemplateModule.psd1' + 'src/TemplateModule.psm1' + 'src/Public/.gitkeep' + 'src/Private/.gitkeep' + 'src/Classes/.gitkeep' +) + +$customTestFiles = Get-ChildItem -LiteralPath (Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'tests') -Filter '*.Tests.ps1' -Recurse -File -ErrorAction SilentlyContinue | + ForEach-Object { + Get-RelativeRepoFilePath -BasePath $resolvedRepoRoot -Path $_.FullName + } | + Where-Object { $_ -notin $knownTemplateTests } + +if ($customTestFiles) { + $stopReasons.Add('Repository contains project-specific test files. Run cleanup before adding downstream tests.') +} + +$customAdrFiles = Get-ChildItem -LiteralPath (Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'docs/decisions') -Filter '*.md' -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^\d{4}-' -and $_.Name -notmatch '^000[1-2]-' } | + Select-Object -ExpandProperty Name + +if ($customAdrFiles) { + $stopReasons.Add('Repository contains numbered ADRs outside the template seed set. Run cleanup before adding repository-specific ADRs.') +} + +$customSourceFiles = Get-ChildItem -LiteralPath (Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'src') -Recurse -File -ErrorAction SilentlyContinue | + ForEach-Object { + Get-RelativeRepoFilePath -BasePath $resolvedRepoRoot -Path $_.FullName + } | + Where-Object { + $_ -notin $expectedSourceFiles -and + $_ -notmatch '^src/(Public|Private|Classes)/\.gitkeep$' + } + +if ($customSourceFiles) { + $stopReasons.Add('Repository contains source files beyond the template scaffold. Run cleanup before adding downstream implementation files.') +} + +$readmePath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'README.md' +$readmeContent = Get-FileContent -Path $readmePath +if ($null -eq $readmeContent -or $readmeContent -notmatch 'Windows PowerShell Development Template') { + $stopReasons.Add('README.md no longer appears to be in the expected immediate post-create state.') +} + +$agentsPath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'AGENTS.md' +$agentsContent = Get-FileContent -Path $agentsPath +if ($null -eq $agentsContent -or $agentsContent -notmatch 'template version release') { + $stopReasons.Add('AGENTS.md no longer appears to be in the expected immediate post-create state.') +} + +if ($stopReasons.Count -gt 0) { + foreach ($reason in $stopReasons) { + Add-ReportItem -Items $reportItems -Action ManualFollowUp -Path '.' -Reason $reason -Status 'Blocked' + } + + if ($OutputFormat -eq 'Json') { + [pscustomobject]@{ + RepoRoot = $resolvedRepoRoot + RepositoryName = $RepositoryName + ProjectType = $ProjectType + Applied = $false + Blocked = $true + TemplateVersion = $null + Report = $reportItems + ManualFollowUp = @() + } | ConvertTo-Json -Depth 5 + return + } + + $message = ($stopReasons -join ' ') + throw ('Cleanup workflow can only run during the immediate post-create window. {0}' -f $message) +} + +$templateVersion = Get-TemplateVersion -RepoPath $resolvedRepoRoot + +$removePaths = @( + 'VERSION' + 'CHANGELOG.md' + 'TestResults.xml' + 'docs/template-evolution.md' + 'scripts/Get-TemplateHealth.ps1' + 'scripts/Test-TemplateVersion.ps1' + '.codex/skills/runtime-policy-update' + '.codex/skills/template-version-release' + 'docs/decisions/0001-windows-powershell-development-baseline.md' + 'docs/decisions/0002-downstream-readme-workflow.md' + 'tests/unit/TemplateHealth.Tests.ps1' + 'tests/unit/TemplateVersion.Tests.ps1' + 'tests/unit/SkillScaffold.Tests.ps1' + 'tests/unit/Initialize-DownstreamRepo.Tests.ps1' + 'tests/unit/Invoke-ReadmeAlignment.Tests.ps1' +) + +if ($ProjectType -eq 'script') { + $removePaths += @( + 'src/TemplateModule.psd1' + 'src/TemplateModule.psm1' + ) +} + +$rewritePaths = @( + 'README.md' + 'AGENTS.md' + '.github/copilot-instructions.md' + 'docs/agent-workflows.md' + 'docs/decisions/README.md' +) + +$keepPaths = @( + '.github/workflows/ci.yml' + 'scripts/Initialize-DownstreamRepo.ps1' + 'scripts/Invoke-RepoChecks.ps1' + 'scripts/Test-VersionPolicy.ps1' + 'scripts/Update-GeneratedMarkdown.ps1' + 'scripts/Invoke-TemplateGuidanceSync.ps1' + 'scripts/Invoke-ReadmeAlignment.ps1' + 'eng/runtime-policy.json' + 'templates' + 'templates/downstream/README.md' + 'PesterConfiguration.psd1' + 'PSScriptAnalyzerSettings.psd1' + '.codex/skills/change-delivery-workflow' + '.codex/skills/downstream-guidance-sync' + '.codex/skills/downstream-repo-cleanup' + '.codex/skills/readme-alignment' +) + +foreach ($path in $removePaths) { + $absolutePath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath $path + $status = if (Test-Path -LiteralPath $absolutePath) { 'Pending' } else { 'Absent' } + Add-ReportItem -Items $reportItems -Action Remove -Path $path -Reason 'Template-maintainer artifact is not part of the downstream baseline.' -Status $status +} + +foreach ($path in $rewritePaths) { + Add-ReportItem -Items $reportItems -Action Rewrite -Path $path -Reason 'Rewrite inherited guidance into downstream form while preserving the template version badge contract.' -Status 'Pending' +} + +if ($ProjectType -in @('module', 'hybrid')) { + Add-ReportItem -Items $reportItems -Action Rename -Path 'src/TemplateModule.psd1' -Reason ('Rename the scaffold manifest to {0}.psd1.' -f $ModuleName) -Status 'Pending' + Add-ReportItem -Items $reportItems -Action Rename -Path 'src/TemplateModule.psm1' -Reason ('Rename the scaffold module entry point to {0}.psm1.' -f $ModuleName) -Status 'Pending' +} + +foreach ($path in $keepPaths) { + Add-ReportItem -Items $reportItems -Action Keep -Path $path -Reason 'Retain as downstream-owned baseline infrastructure or workflow guidance.' -Status 'Keep' +} + +$manualFollowUp.Add('Replace the placeholder README summary for the repository with project-specific content.') +$manualFollowUp.Add('Add repository-specific ADRs only after cleanup is complete.') +$manualFollowUp.Add('Update module manifest metadata beyond safe placeholders before release.') | Out-Null +$manualFollowUp.Add('Review project-specific docs and issue templates once the repository scope is clear.') | Out-Null + +foreach ($item in $manualFollowUp) { + Add-ReportItem -Items $reportItems -Action ManualFollowUp -Path '.' -Reason $item -Status 'Manual' +} + +if ($Apply) { + foreach ($path in $removePaths) { + Remove-RepoItem -Path (Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath $path) + } + + $newReadme = Get-DownstreamReadmeContent -RepoRoot $resolvedRepoRoot -RepoName $RepositoryName -TemplateVersion $templateVersion + if ($PSCmdlet.ShouldProcess($readmePath, 'Rewrite downstream README')) { + Write-Utf8File -Path $readmePath -Content $newReadme + } + + if ($PSCmdlet.ShouldProcess($agentsPath, 'Rewrite downstream AGENTS.md')) { + Write-Utf8File -Path $agentsPath -Content (Get-DownstreamAgentsContent) + } + + $copilotInstructionsPath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath '.github/copilot-instructions.md' + $copilotContent = Get-FileContent -Path $copilotInstructionsPath + if ($null -eq $copilotContent) { + throw '.github/copilot-instructions.md is required for downstream cleanup.' + } + + if ($PSCmdlet.ShouldProcess($copilotInstructionsPath, 'Rewrite downstream Copilot instructions')) { + Write-Utf8File -Path $copilotInstructionsPath -Content (Get-DownstreamCopilotInstruction -ExistingContent $copilotContent) + } + + $agentWorkflowsPath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'docs/agent-workflows.md' + if ($PSCmdlet.ShouldProcess($agentWorkflowsPath, 'Rewrite downstream agent workflows guide')) { + Write-Utf8File -Path $agentWorkflowsPath -Content (Get-DownstreamAgentWorkflowsContent) + } + + $decisionsReadmePath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'docs/decisions/README.md' + if ($PSCmdlet.ShouldProcess($decisionsReadmePath, 'Rewrite downstream ADR README')) { + Write-Utf8File -Path $decisionsReadmePath -Content (Get-DownstreamDecisionsReadmeContent) + } + + if ($ProjectType -in @('module', 'hybrid')) { + $manifestPath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'src/TemplateModule.psd1' + $modulePath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'src/TemplateModule.psm1' + $newManifestPath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath ('src/{0}.psd1' -f $ModuleName) + $newModulePath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath ('src/{0}.psm1' -f $ModuleName) + + $manifestContent = Get-FileContent -Path $manifestPath + $moduleContent = Get-FileContent -Path $modulePath + + if ($manifestContent) { + if ($PSCmdlet.ShouldProcess($manifestPath, ('Rewrite module manifest for {0}' -f $ModuleName))) { + Write-Utf8File -Path $manifestPath -Content (Get-DownstreamModuleManifestContent -ExistingContent $manifestContent -ModuleFileName ('{0}.psm1' -f $ModuleName) -RepoName $RepositoryName) + } + + Rename-RepoItem -Path $manifestPath -Destination $newManifestPath + } + + if ($moduleContent) { + if ($PSCmdlet.ShouldProcess($modulePath, ('Rewrite module entry point for {0}' -f $ModuleName))) { + Write-Utf8File -Path $modulePath -Content (Get-DownstreamModuleEntryPointContent -ExistingContent $moduleContent -ModuleNameValue $ModuleName) + } + + Rename-RepoItem -Path $modulePath -Destination $newModulePath + } + } + + Remove-StaleGitKeepFile -RepoPath $resolvedRepoRoot +} + +$result = [pscustomobject]@{ + RepoRoot = $resolvedRepoRoot + RepositoryName = $RepositoryName + ProjectType = $ProjectType + Applied = [bool]$Apply + Blocked = $false + TemplateVersion = $templateVersion + Report = $reportItems + ManualFollowUp = @($manualFollowUp) +} + +if ($OutputFormat -eq 'Json') { + $result | ConvertTo-Json -Depth 6 + return +} + +Write-Output ('Repository: {0}' -f $resolvedRepoRoot) +Write-Output ('RepositoryName: {0}' -f $RepositoryName) +Write-Output ('ProjectType: {0}' -f $ProjectType) +Write-Output ('TemplateVersion: {0}' -f $(if ($templateVersion) { $templateVersion } else { 'Unknown' })) +Write-Output ('Applied: {0}' -f $Apply.IsPresent) +Write-Output '' + +foreach ($group in 'Remove', 'Rewrite', 'Rename', 'Keep', 'ManualFollowUp') { + $groupItems = $reportItems | Where-Object { $_.Action -eq $group } + if (-not $groupItems) { + continue + } + + Write-Output ('{0}:' -f $group) + foreach ($item in $groupItems) { + Write-Output ('- {0} [{1}] - {2}' -f $item.Path, $item.Status, $item.Reason) + } + + Write-Output '' +} diff --git a/scripts/Invoke-ReadmeAlignment.ps1 b/scripts/Invoke-ReadmeAlignment.ps1 new file mode 100644 index 0000000..abdb130 --- /dev/null +++ b/scripts/Invoke-ReadmeAlignment.ps1 @@ -0,0 +1,516 @@ +<# +.SYNOPSIS + Audits or aligns a downstream README to the shared powershell-dev-template skeleton. + +.DESCRIPTION + Uses the shared downstream README skeleton shipped with powershell-dev-template + to report drift or rewrite README.md for template-derived downstream + repositories while preserving repo-specific sections and generated blocks. +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$SkeletonPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$RepositoryName, + + [Parameter()] + [switch]$Apply, + + [Parameter()] + [ValidateSet('Text', 'Json')] + [string]$OutputFormat = 'Text' +) + +$ErrorActionPreference = 'Stop' + +function Resolve-DirectoryPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$LiteralPath + ) + + if (-not (Test-Path -LiteralPath $LiteralPath -PathType Container)) { + throw ('Directory not found: {0}' -f $LiteralPath) + } + + return (Resolve-Path -LiteralPath $LiteralPath).Path +} + +function Get-RepoPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$BasePath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RelativePath + ) + + return (Join-Path -Path $BasePath -ChildPath $RelativePath) +} + +function Get-FileContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return $null + } + + return Get-Content -Raw -LiteralPath $Path +} + +function Write-Utf8File { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Content + ) + + [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false)) +} + +function Get-NewLine { + [CmdletBinding()] + param( + [Parameter()] + [AllowEmptyString()] + [string]$Content + ) + + if ($Content -match "`r`n") { + return "`r`n" + } + + return "`n" +} + +function Test-GitRepository { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$RepoPath + ) + + $output = & git -C $RepoPath rev-parse --is-inside-work-tree 2>&1 + return ($LASTEXITCODE -eq 0 -and ($output | Select-Object -First 1) -eq 'true') +} + +function Get-ReadmeDocument { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Content + ) + + $lines = [System.Collections.Generic.List[string]]::new() + $lines.AddRange([string[]]($Content -split "`r?`n")) + + $lead = [System.Collections.Generic.List[string]]::new() + $sections = [System.Collections.Generic.List[object]]::new() + $currentHeading = $null + $currentBody = [System.Collections.Generic.List[string]]::new() + + foreach ($line in $lines) { + if ($line -match '^##\s+(?.+?)\s*$') { + if ($null -ne $currentHeading) { + $sections.Add([pscustomobject]@{ + Heading = $currentHeading + Body = @($currentBody) + }) + } + + $currentHeading = $Matches.Heading + $currentBody = [System.Collections.Generic.List[string]]::new() + continue + } + + if ($null -eq $currentHeading) { + $lead.Add($line) + } + else { + $currentBody.Add($line) + } + } + + if ($null -ne $currentHeading) { + $sections.Add([pscustomobject]@{ + Heading = $currentHeading + Body = @($currentBody) + }) + } + + return [pscustomobject]@{ + Lead = @($lead) + Sections = @($sections) + } +} + +function Get-LeadTitle { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string[]]$LeadLines + ) + + foreach ($line in $LeadLines) { + if ($line -match '^#\s+(?.+?)\s*$') { + return $Matches.Title + } + } + + return $null +} + +function Get-TemplateBadgeLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$ReadmeContent + ) + + $match = [regex]::Match($ReadmeContent, '!\[Template Version\]\(https://img\.shields\.io/badge/template-[^)]+\)') + if ($match.Success) { + return $match.Value + } + + return $null +} + +function Get-LeadSummaryBlock { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string[]]$LeadLines + ) + + $summary = [System.Collections.Generic.List[string]]::new() + $capturing = $false + + foreach ($line in $LeadLines) { + if ($line -match '^Quick navigation:\s*$') { + break + } + + if ($line -match '^#\s+' -or + $line -match '^<!-- .* -->$' -or + $line -match '^!\[.+\]\(.+\)$' -or + $line -match '^\[!\[.+\]\(.+\)\]\(.+\)$' -or + $line -match '^\s*-\s+\[.+\]\(#.+\)\s*$') { + continue + } + + if ([string]::IsNullOrWhiteSpace($line)) { + if ($capturing) { + $summary.Add('') + } + + continue + } + + $capturing = $true + $summary.Add($line) + } + + while ($summary.Count -gt 0 -and [string]::IsNullOrWhiteSpace($summary[0])) { + $summary.RemoveAt(0) + } + + while ($summary.Count -gt 0 -and [string]::IsNullOrWhiteSpace($summary[$summary.Count - 1])) { + $summary.RemoveAt($summary.Count - 1) + } + + return @($summary) +} + +function Get-RenderedSkeletonContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$SkeletonContent, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ResolvedRepositoryName, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string[]]$SummaryLines, + + [Parameter()] + [string]$TemplateBadgeLine + ) + + $rendered = $SkeletonContent.Replace('{{REPOSITORY_NAME}}', $ResolvedRepositoryName) + $summaryText = if ($SummaryLines.Count -gt 0) { + ($SummaryLines -join "`n") + } + else { + 'Add a concise repository summary that explains what this project does and why it exists.' + } + + $badgeText = if ($TemplateBadgeLine) { $TemplateBadgeLine } else { '' } + $rendered = $rendered.Replace('{{REPOSITORY_SUMMARY}}', $summaryText) + $rendered = $rendered.Replace('{{TEMPLATE_VERSION_BADGE}}', $badgeText) + + return ($rendered.TrimEnd("`r", "`n") + "`n") +} + +function Convert-ReadmeDocumentToText { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [object]$Document, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$NewLine + ) + + $lines = [System.Collections.Generic.List[string]]::new() + $leadLines = [string[]]$Document.Lead + + for ($index = 0; $index -lt $leadLines.Count; $index++) { + $lines.Add($leadLines[$index]) + } + + while ($lines.Count -gt 0 -and [string]::IsNullOrWhiteSpace($lines[$lines.Count - 1])) { + $lines.RemoveAt($lines.Count - 1) + } + + foreach ($section in $Document.Sections) { + if ($lines.Count -gt 0) { + $lines.Add('') + } + + $lines.Add('## {0}' -f $section.Heading) + + $bodyLines = [System.Collections.Generic.List[string]]::new() + $bodyLines.AddRange([string[]]$section.Body) + while ($bodyLines.Count -gt 0 -and [string]::IsNullOrWhiteSpace($bodyLines[$bodyLines.Count - 1])) { + $bodyLines.RemoveAt($bodyLines.Count - 1) + } + + foreach ($bodyLine in $bodyLines) { + $lines.Add($bodyLine) + } + } + + return (($lines -join $NewLine).TrimEnd("`r", "`n") + $NewLine) +} + +$resolvedRepoRoot = Resolve-DirectoryPath -LiteralPath $RepoRoot +if (-not (Test-GitRepository -RepoPath $resolvedRepoRoot)) { + throw ('Repository is not a Git work tree: {0}' -f $resolvedRepoRoot) +} + +$resolvedSkeletonPath = if ($SkeletonPath) { + (Resolve-Path -LiteralPath $SkeletonPath).Path +} +else { + Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'templates/downstream/README.md' +} + +$requiredPaths = @( + 'README.md' + 'AGENTS.md' + 'eng/runtime-policy.json' + 'scripts/Update-GeneratedMarkdown.ps1' + 'scripts/Invoke-RepoChecks.ps1' + 'scripts/Invoke-TemplateGuidanceSync.ps1' + '.codex/skills/downstream-guidance-sync/SKILL.md' +) + +$missingRequiredPaths = [System.Collections.Generic.List[string]]::new() +foreach ($relativePath in $requiredPaths) { + if (-not (Test-Path -LiteralPath (Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath $relativePath))) { + $missingRequiredPaths.Add($relativePath) + } +} + +if (-not (Test-Path -LiteralPath $resolvedSkeletonPath -PathType Leaf)) { + $missingRequiredPaths.Add('templates/downstream/README.md') +} + +if ($missingRequiredPaths.Count -gt 0) { + throw ('README alignment is only supported for template-derived downstream repositories with synced README assets. Missing: {0}' -f [string]::Join(', ', $missingRequiredPaths.ToArray())) +} + +$readmePath = Get-RepoPath -BasePath $resolvedRepoRoot -RelativePath 'README.md' +$currentContent = Get-FileContent -Path $readmePath +if ($null -eq $currentContent) { + throw ('README.md not found: {0}' -f $readmePath) +} + +$resolvedRepositoryName = if ($RepositoryName) { + $RepositoryName +} +else { + $currentTitle = Get-LeadTitle -LeadLines (Get-ReadmeDocument -Content $currentContent).Lead + if ($currentTitle) { + $currentTitle + } + else { + Split-Path -Path $resolvedRepoRoot -Leaf + } +} + +$currentDocument = Get-ReadmeDocument -Content $currentContent +$skeletonContent = Get-FileContent -Path $resolvedSkeletonPath +$summaryLines = Get-LeadSummaryBlock -LeadLines $currentDocument.Lead +$templateBadgeLine = Get-TemplateBadgeLine -ReadmeContent $currentContent +$renderedSkeletonContent = Get-RenderedSkeletonContent -SkeletonContent $skeletonContent -ResolvedRepositoryName $resolvedRepositoryName -SummaryLines $summaryLines -TemplateBadgeLine $templateBadgeLine +$skeletonDocument = Get-ReadmeDocument -Content $renderedSkeletonContent + +$standardSections = [System.Collections.Generic.List[string]]::new() +foreach ($section in $skeletonDocument.Sections) { + $standardSections.Add($section.Heading) +} + +$currentSectionMap = @{} +$currentSectionOrder = [System.Collections.Generic.List[string]]::new() +foreach ($section in $currentDocument.Sections) { + $currentSectionMap[$section.Heading.ToLowerInvariant()] = $section + $currentSectionOrder.Add($section.Heading) +} + +$alignedSections = [System.Collections.Generic.List[object]]::new() +$sectionStates = [System.Collections.Generic.List[object]]::new() + +foreach ($skeletonSection in $skeletonDocument.Sections) { + $key = $skeletonSection.Heading.ToLowerInvariant() + if ($currentSectionMap.ContainsKey($key)) { + $alignedSections.Add([pscustomobject]@{ + Heading = $skeletonSection.Heading + Body = @($currentSectionMap[$key].Body) + }) + + $originalIndex = $currentSectionOrder.IndexOf($currentSectionMap[$key].Heading) + $expectedIndex = $standardSections.IndexOf($skeletonSection.Heading) + $status = if ($originalIndex -eq $expectedIndex) { 'Current' } else { 'Realigned' } + + $sectionStates.Add([pscustomobject]@{ + Heading = $skeletonSection.Heading + Status = $status + Source = 'Existing' + }) + } + else { + $alignedSections.Add([pscustomobject]@{ + Heading = $skeletonSection.Heading + Body = @($skeletonSection.Body) + }) + + $sectionStates.Add([pscustomobject]@{ + Heading = $skeletonSection.Heading + Status = 'Missing' + Source = 'Skeleton' + }) + } +} + +$extraSections = [System.Collections.Generic.List[object]]::new() +foreach ($section in $currentDocument.Sections) { + if ($standardSections -notcontains $section.Heading) { + $alignedSections.Add([pscustomobject]@{ + Heading = $section.Heading + Body = @($section.Body) + }) + + $extraSections.Add([pscustomobject]@{ + Heading = $section.Heading + Status = 'Preserved' + }) + } +} + +$alignedDocument = [pscustomobject]@{ + Lead = @($skeletonDocument.Lead) + Sections = @($alignedSections) +} + +$newLine = Get-NewLine -Content $currentContent +$alignedContent = Convert-ReadmeDocumentToText -Document $alignedDocument -NewLine $newLine +$hasChanges = $alignedContent -ne $currentContent + +$manualFollowUp = [System.Collections.Generic.List[string]]::new() +if ($summaryLines.Count -eq 0) { + $manualFollowUp.Add('Replace the placeholder repository summary near the top of README.md.') +} + +if (-not $templateBadgeLine) { + $manualFollowUp.Add('Add or refresh the template version badge through downstream guidance sync before relying on template alignment metadata.') +} + +$result = [pscustomobject]@{ + RepoRoot = $resolvedRepoRoot + ReadmePath = $readmePath + SkeletonPath = $resolvedSkeletonPath + RepositoryName = $resolvedRepositoryName + Applied = [bool]$Apply + HasChanges = [bool]$hasChanges + TemplateBadgePresent = [bool]($null -ne $templateBadgeLine) + StandardSections = @($sectionStates) + ExtraSections = @($extraSections) + ManualFollowUp = @($manualFollowUp) +} + +if ($Apply -and $hasChanges) { + if ($PSCmdlet.ShouldProcess($readmePath, 'Align README to shared downstream skeleton')) { + Write-Utf8File -Path $readmePath -Content $alignedContent + } +} + +if ($OutputFormat -eq 'Json') { + $result | ConvertTo-Json -Depth 6 +} +else { + Write-Output ('Repository: {0}' -f $result.RepoRoot) + Write-Output ('README: {0}' -f $result.ReadmePath) + Write-Output ('Skeleton: {0}' -f $result.SkeletonPath) + Write-Output ('Changes needed: {0}' -f $result.HasChanges) + Write-Output ('Template badge present: {0}' -f $result.TemplateBadgePresent) + Write-Output '' + Write-Output 'Standard sections:' + $result.StandardSections | Format-Table -Property Heading, Status, Source -AutoSize | Out-String | Write-Output + + if ($result.ExtraSections.Count -gt 0) { + Write-Output 'Extra sections:' + $result.ExtraSections | Format-Table -Property Heading, Status -AutoSize | Out-String | Write-Output + } + + if ($result.ManualFollowUp.Count -gt 0) { + Write-Output 'Manual follow-up:' + foreach ($item in $result.ManualFollowUp) { + Write-Output ('- {0}' -f $item) + } + } +} diff --git a/scripts/Invoke-TemplateGuidanceSync.ps1 b/scripts/Invoke-TemplateGuidanceSync.ps1 index b4e1c9c..150fac8 100644 --- a/scripts/Invoke-TemplateGuidanceSync.ps1 +++ b/scripts/Invoke-TemplateGuidanceSync.ps1 @@ -67,11 +67,23 @@ if ([string]::IsNullOrWhiteSpace($TemplatePath)) { $guidanceFiles = @( 'AGENTS.md' '.github/copilot-instructions.md' + '.codex/skills/change-delivery-workflow/SKILL.md' + '.codex/skills/change-delivery-workflow/agents/openai.yaml' + '.codex/skills/downstream-repo-cleanup/SKILL.md' + '.codex/skills/downstream-repo-cleanup/agents/openai.yaml' + '.codex/skills/readme-alignment/SKILL.md' + '.codex/skills/readme-alignment/agents/openai.yaml' + 'docs/agent-workflows.md' 'docs/ai-behavioral-contract.md' 'docs/ai-interaction-loop.md' 'docs/copilot-instructions-reference.md' 'docs/powershell-ai-operating-model.md' 'docs/decisions/README.md' + 'scripts/Initialize-DownstreamRepo.ps1' + 'scripts/Invoke-ReadmeAlignment.ps1' + 'scripts/Update-GeneratedMarkdown.ps1' + 'eng/runtime-policy.json' + 'templates/downstream/README.md' ) function Resolve-DirectoryPath { diff --git a/scripts/Update-GeneratedMarkdown.ps1 b/scripts/Update-GeneratedMarkdown.ps1 index 10cf931..a20db8f 100644 --- a/scripts/Update-GeneratedMarkdown.ps1 +++ b/scripts/Update-GeneratedMarkdown.ps1 @@ -108,6 +108,28 @@ Set-GeneratedMarkdownBlock -RelativePath 'README.md' -BlockName 'readme-runtime- $baseRuntimeLine ) + +Set-GeneratedMarkdownBlock -RelativePath 'templates/downstream/README.md' -BlockName 'readme-powershell-badge' -Lines @( + '![PowerShell {0}](https://img.shields.io/badge/PowerShell-{0}-blue)' -f $policy.runtime.powershellVersion +) + +Set-GeneratedMarkdownBlock -RelativePath 'templates/downstream/README.md' -BlockName 'readme-runtime-focus' -Lines @( + '- Windows PowerShell {0} development' -f $policy.runtime.powershellVersion +) + +Set-GeneratedMarkdownBlock -RelativePath 'templates/downstream/README.md' -BlockName 'readme-runtime-stack' -Lines @( + $runtimeLine +) + +Set-GeneratedMarkdownBlock -RelativePath 'templates/downstream/README.md' -BlockName 'readme-tooling-list' -Lines @( + ('- **Pester {0}:** For unit and integration testing' -f $policy.tooling.pesterVersion), + ('- **PSScriptAnalyzer {0}:** To enforce PowerShell best practices and security rules' -f $policy.tooling.psScriptAnalyzerVersion), + ('- **PSReadLine {0}:** Configured for a more efficient terminal experience' -f $policy.tooling.psReadLineVersion) +) + +Set-GeneratedMarkdownBlock -RelativePath 'templates/downstream/README.md' -BlockName 'readme-runtime-philosophy' -Lines @( + $baseRuntimeLine +) Set-GeneratedMarkdownBlock -RelativePath '.github/Instructions/environment-setup.md' -BlockName 'environment-runtime-stack' -Lines @( $runtimeLine ) diff --git a/templates/downstream/README.md b/templates/downstream/README.md new file mode 100644 index 0000000..acfa7de --- /dev/null +++ b/templates/downstream/README.md @@ -0,0 +1,99 @@ +# {{REPOSITORY_NAME}} + +<!-- BEGIN generated:readme-powershell-badge --> +![PowerShell 5.1](https://img.shields.io/badge/PowerShell-5.1-blue) +<!-- END generated:readme-powershell-badge --> +{{TEMPLATE_VERSION_BADGE}} + +{{REPOSITORY_SUMMARY}} + +Quick navigation: + +- [Portfolio Context](#portfolio-context) +- [Engineering Principles in Practice](#engineering-principles-in-practice) +- [Validation And Maintenance](#validation-and-maintenance) +- [Repository Structure](#repository-structure) + +<!-- BEGIN generated:readme-runtime-focus --> +- Windows PowerShell 5.1 development +<!-- END generated:readme-runtime-focus --> + +## Portfolio Context + +This repository is part of a Windows PowerShell 5.1 portfolio built from `powershell-dev-template`. Customize this section to explain what this repository demonstrates, why it exists, and what a reviewer should pay attention to first. + +## Engineering Principles in Practice + +<!-- BEGIN generated:readme-runtime-philosophy --> +- **Deterministic Runtime:** Development and validation use Windows PowerShell 5.1 Desktop edition, with CI pinned to windows-2022 +<!-- END generated:readme-runtime-philosophy --> +- **Deterministic Workflows:** Prefer repeatable scripts, explicit validation, and reviewable changes over one-off manual steps +- **Small Safe Changes:** Solve the requested problem with the smallest reasonable change before introducing more structure +- **Validation First:** Use analyzer, tests, and diff review as normal delivery controls rather than optional cleanup +- **Human Accountability:** AI assistance accelerates drafting, but review and ownership remain human responsibilities + +For the deeper operating model behind that approach, see [`docs/powershell-ai-operating-model.md`](docs/powershell-ai-operating-model.md). Add repository-specific decisions under [`docs/decisions/`](docs/decisions/) when durable implementation choices need to be explained. + +## Use This Repository + +1. Replace the placeholder summary near the top of this README with a repository-specific description. +2. Update `Portfolio Context` so readers understand the goal, scope, and value of the repository. +3. Add repository-specific implementation under `src/`. +4. Add repository-specific tests under `tests/`. +5. Review `AGENTS.md` and `.github/copilot-instructions.md` before using AI-generated changes. + +## Runtime And Environment + +<!-- BEGIN generated:readme-runtime-stack --> +- **Runtime:** Windows PowerShell 5.1 Desktop edition on Windows +<!-- END generated:readme-runtime-stack --> +- **Development Mode:** Native Windows with VS Code +- **CI:** GitHub-hosted `windows-2022` runner using `powershell.exe` +- **Compatibility:** Windows PowerShell 5.1 Desktop is the sole supported runtime +- **Environment Ownership:** Customize runtime details when the repository intentionally diverges from the template baseline + +## Tooling + +<!-- BEGIN generated:readme-tooling-list --> +- **Pester 6.0.0:** For unit and integration testing +- **PSScriptAnalyzer 1.25.0:** To enforce PowerShell best practices and security rules +- **PSReadLine 2.4.5:** Configured for a more efficient terminal experience +<!-- END generated:readme-tooling-list --> + +When runtime or tooling versions are updated, keep `eng/runtime-policy.json`, generated Markdown, and validation scripts aligned. + +## Repository Structure + +- `src/`: repository implementation +- `tests/`: Pester tests and test helpers +- `scripts/`: validation, maintenance, and workflow entrypoints +- `docs/`: operating model, decisions, and supporting documentation +- `templates/`: reusable scaffolds and template-owned starter assets + +## Validation And Maintenance + +Run the standard repository checks before committing meaningful changes: + +```powershell +powershell.exe -NoProfile -File .\scripts\Invoke-RepoChecks.ps1 +``` + +If this repository keeps the template-managed generated Markdown blocks, refresh or validate them through: + +```powershell +powershell.exe -NoProfile -File .\scripts\Update-GeneratedMarkdown.ps1 -Check +``` + +## Downstream Guidance Sync + +Use `.codex/skills/downstream-guidance-sync/SKILL.md` with `scripts/Invoke-TemplateGuidanceSync.ps1` when you want to adopt newer template guidance or README workflow assets from `powershell-dev-template` without overwriting repository-owned implementation. + +## Prerequisites And Setup + +- Use Windows PowerShell 5.1 on Windows. +- Install pinned modules for local validation when they are not already available. +- Review `docs/agent-workflows.md` for repo-local workflow entrypoints before asking an agent to make changes. + +## Template Versioning + +The template version badge tracks inherited guidance and workflow baseline alignment. It does not mean this repository remains fully identical to the source template. Customize this section if you need to explain intentional divergence from the parent template. diff --git a/tests/unit/Initialize-DownstreamRepo.Tests.ps1 b/tests/unit/Initialize-DownstreamRepo.Tests.ps1 new file mode 100644 index 0000000..ee50899 --- /dev/null +++ b/tests/unit/Initialize-DownstreamRepo.Tests.ps1 @@ -0,0 +1,287 @@ +Describe 'Initialize-DownstreamRepo' { + BeforeAll { + $script:RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..\..')).Path + $script:ScriptPath = Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Initialize-DownstreamRepo.ps1' + $script:TemplateVersion = '0.3.0' + + function New-TemplateLikeDownstreamRepo { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter()] + [switch]$OmitReadmeBadge + ) + + New-Item -ItemType Directory -Path $Path -Force | Out-Null + + $files = @{ + 'VERSION' = $script:TemplateVersion + 'CHANGELOG.md' = "# Changelog`n" + 'AGENTS.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'AGENTS.md') + 'README.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'README.md') + '.github/copilot-instructions.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.github/copilot-instructions.md') + '.github/Instructions/environment-setup.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.github/Instructions/environment-setup.md') + 'docs/agent-workflows.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'docs/agent-workflows.md') + 'docs/decisions/README.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'docs/decisions/README.md') + 'docs/template-evolution.md' = 'template evolution' + 'docs/decisions/0001-windows-powershell-development-baseline.md' = 'seed ADR' + 'docs/decisions/0002-downstream-readme-workflow.md' = 'seed ADR' + 'scripts/Get-TemplateHealth.ps1' = 'Write-Output "health"' + 'scripts/Test-TemplateVersion.ps1' = 'Write-Output "version"' + 'scripts/Initialize-DownstreamRepo.ps1' = Get-Content -Raw -LiteralPath $script:ScriptPath + 'scripts/Invoke-RepoChecks.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-RepoChecks.ps1') + 'scripts/Test-VersionPolicy.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Test-VersionPolicy.ps1') + 'scripts/Update-GeneratedMarkdown.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Update-GeneratedMarkdown.ps1') + 'scripts/Invoke-TemplateGuidanceSync.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-TemplateGuidanceSync.ps1') + 'scripts/Invoke-ReadmeAlignment.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-ReadmeAlignment.ps1') + '.github/workflows/ci.yml' = 'name: CI' + '.devcontainer/devcontainer.json' = '{}' + 'eng/runtime-policy.json' = '{"runtime":{"powershellVersion":"5.1","powershellVersionLabel":"5.1","edition":"Desktop","executable":"powershell.exe","platform":"Windows"},"githubActions":{"runnerImage":"windows-2022"},"tooling":{"pesterVersion":"6.0.0","psScriptAnalyzerVersion":"1.25.0","psReadLineVersion":"2.4.5"}}' + 'PesterConfiguration.psd1' = '@{}' + 'PSScriptAnalyzerSettings.psd1' = '@{}' + 'src/README.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'src/README.md') + 'src/TemplateModule.psd1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'src/TemplateModule.psd1') + 'src/TemplateModule.psm1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'src/TemplateModule.psm1') + 'src/.gitkeep' = '' + 'src/Public/.gitkeep' = '' + 'src/Private/.gitkeep' = '' + 'src/Classes/.gitkeep' = '' + 'templates/README.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'templates/README.md') + 'templates/downstream/README.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'templates/downstream/README.md') + '.codex/skills/downstream-guidance-sync/SKILL.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/downstream-guidance-sync/SKILL.md') + '.codex/skills/downstream-guidance-sync/agents/openai.yaml' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/downstream-guidance-sync/agents/openai.yaml') + '.codex/skills/downstream-repo-cleanup/SKILL.md' = "placeholder" + '.codex/skills/downstream-repo-cleanup/agents/openai.yaml' = @' +display_name: "Downstream Repo Cleanup" +short_description: "Normalize a new downstream repo" +default_prompt: "Use $downstream-repo-cleanup to normalize this new downstream repository." +'@ + '.codex/skills/readme-alignment/SKILL.md' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/readme-alignment/SKILL.md') + '.codex/skills/readme-alignment/agents/openai.yaml' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/readme-alignment/agents/openai.yaml') + '.codex/skills/runtime-policy-update/SKILL.md' = 'runtime skill' + '.codex/skills/runtime-policy-update/agents/openai.yaml' = 'display_name: "Runtime Policy Update"' + '.codex/skills/template-version-release/SKILL.md' = 'release skill' + '.codex/skills/template-version-release/agents/openai.yaml' = 'display_name: "Template Version Release"' + 'tests/unit/Invoke-TemplateGuidanceSync.Tests.ps1' = 'Describe "x" {}' + 'tests/unit/TemplateHealth.Tests.ps1' = 'Describe "x" {}' + 'tests/unit/TemplateScaffold.Tests.ps1' = 'Describe "x" {}' + 'tests/unit/TemplateVersion.Tests.ps1' = 'Describe "x" {}' + 'tests/unit/SkillScaffold.Tests.ps1' = 'Describe "x" {}' + 'tests/unit/Initialize-DownstreamRepo.Tests.ps1' = 'Describe "x" {}' + 'tests/unit/Invoke-ReadmeAlignment.Tests.ps1' = 'Describe "x" {}' + 'tests/.gitkeep' = '' + 'tests/unit/.gitkeep' = '' + 'tests/testhelpers/.gitkeep' = '' + } + + foreach ($relativePath in $files.Keys) { + $fullPath = Join-Path -Path $Path -ChildPath $relativePath + $parent = Split-Path -Path $fullPath -Parent + if ($parent -and (-not (Test-Path -LiteralPath $parent))) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + + [System.IO.File]::WriteAllText($fullPath, $files[$relativePath], [System.Text.UTF8Encoding]::new($false)) + } + + if ($OmitReadmeBadge) { + $readmePath = Join-Path -Path $Path -ChildPath 'README.md' + $readmeContent = Get-Content -Raw -LiteralPath $readmePath + $readmeContent = $readmeContent -replace '!\[Template Version\]\(https://img\.shields\.io/badge/template-[^)]+\)\r?\n', '' + [System.IO.File]::WriteAllText($readmePath, $readmeContent, [System.Text.UTF8Encoding]::new($false)) + } + + & git -C $Path init -q -b main | Out-Null + & git -C $Path config user.email 'test@example.invalid' | Out-Null + & git -C $Path config user.name 'Test User' | Out-Null + & git -C $Path add . | Out-Null + & git -C $Path commit -m 'initial downstream template copy' | Out-Null + } + + function Invoke-CleanupScript { + param( + [Parameter(Mandatory)] + [string]$RepoPath, + + [Parameter()] + [string[]]$ExtraArguments = @() + ) + + $arguments = @( + '-NoProfile' + '-File' + $script:ScriptPath + '-RepoRoot' + $RepoPath + ) + $ExtraArguments + + $output = & powershell.exe @arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw ($output -join [Environment]::NewLine) + } + + return $output + } + } + + BeforeEach { + $script:TempRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('downstream-cleanup-{0}' -f [guid]::NewGuid()) + New-Item -ItemType Directory -Path $script:TempRoot -Force | Out-Null + $script:DownstreamRepo = Join-Path -Path $script:TempRoot -ChildPath 'winget-pwsh-automation' + New-TemplateLikeDownstreamRepo -Path $script:DownstreamRepo + } + + AfterEach { + if (Test-Path -LiteralPath $script:TempRoot) { + Remove-Item -LiteralPath $script:TempRoot -Recurse -Force + } + } + + It 'reports expected actions in audit mode without changing files' { + $output = & Invoke-CleanupScript -RepoPath $script:DownstreamRepo + $text = $output -join "`n" + + $text | Should -Match 'Remove:' + $text | Should -Match 'Rewrite:' + $text | Should -Match 'Keep:' + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'VERSION') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'CHANGELOG.md') | Should -BeTrue + } + + It 'removes template-only files and rewrites docs in script mode' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'VERSION') | Should -BeFalse + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'CHANGELOG.md') | Should -BeFalse + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/TemplateModule.psd1') | Should -BeFalse + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/TemplateModule.psm1') | Should -BeFalse + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath '.codex/skills/runtime-policy-update') | Should -BeFalse + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath '.codex/skills/template-version-release') | Should -BeFalse + + $readme = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'README.md') + $readme | Should -Match '# winget-pwsh-automation' + $readme | Should -Match 'template-0\.3\.0-blue' + $readme | Should -Match '## Portfolio Context' + $readme | Should -Match '## Template Versioning' + $readme | Should -Match 'Quick navigation:' + + $agents = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'AGENTS.md') + $agents | Should -Match 'downstream-repo-cleanup' + $agents | Should -Not -Match 'template-version-release' + } + + It 'requires ModuleName for module cleanup' { + { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-ProjectType', 'module') + } | Should -Throw -ExpectedMessage '*ModuleName is required*' + } + + It 'renames module files and updates placeholder metadata in module mode' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-ProjectType', 'module', '-ModuleName', 'WingetAutomation', '-Apply') | Out-Null + + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/WingetAutomation.psd1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/WingetAutomation.psm1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/TemplateModule.psd1') | Should -BeFalse + + $manifest = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/WingetAutomation.psd1') + $manifest | Should -Match "RootModule = 'WingetAutomation\.psm1'" + $manifest | Should -Match "Author = 'Repository Maintainer'" + $manifest | Should -Match "Description = 'PowerShell module for winget-pwsh-automation\. Update metadata before release\.'" + + $moduleContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'src/WingetAutomation.psm1') + $moduleContent | Should -Not -Match 'TemplateModule' + } + + It 'preserves downstream guidance sync assets and runtime policy infrastructure' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath '.codex/skills/downstream-guidance-sync/SKILL.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath '.codex/skills/downstream-repo-cleanup/SKILL.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath '.codex/skills/readme-alignment/SKILL.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'scripts/Invoke-ReadmeAlignment.ps1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'eng/runtime-policy.json') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'scripts/Update-GeneratedMarkdown.ps1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'templates/downstream/README.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'scripts/Invoke-RepoChecks.ps1') | Should -BeTrue + } + + It 'rewrites docs and copilot instructions into downstream form' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + $copilot = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath '.github/copilot-instructions.md') + $copilot | Should -Match 'created from the powershell-dev-template GitHub template' + $copilot | Should -Match 'downstream-repo-cleanup' + $copilot | Should -Match 'readme-alignment' + $copilot | Should -Not -Match 'runtime-policy-update' + $copilot | Should -Not -Match 'template-version-release' + + $workflows = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'docs/agent-workflows.md') + $workflows | Should -Match 'Downstream repo cleanup' + $workflows | Should -Match 'Downstream guidance sync' + $workflows | Should -Match 'README alignment' + $workflows | Should -Not -Match 'Runtime policy update' + + $decisionsReadme = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'docs/decisions/README.md') + $decisionsReadme | Should -Match 'durable repository decisions' + $decisionsReadme | Should -Not -Match 'durable template decisions' + } + + It 'preserves the README template version badge when already present' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + $readme = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'README.md') + $readme | Should -Match '!\[Template Version\]\(https://img\.shields\.io/badge/template-0\.3\.0-blue\)' + } + + It 'inserts the README template version badge when it is missing' { + Remove-Item -LiteralPath $script:DownstreamRepo -Recurse -Force + New-TemplateLikeDownstreamRepo -Path $script:DownstreamRepo -OmitReadmeBadge + + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + $readme = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'README.md') + $readme | Should -Match '!\[Template Version\]\(https://img\.shields\.io/badge/template-0\.3\.0-blue\)' + } + + It 'uses the repo name in downstream text rewrites' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + $readme = Get-Content -Raw -LiteralPath (Join-Path -Path $script:DownstreamRepo -ChildPath 'README.md') + $readme | Should -Match '# winget-pwsh-automation' + $readme | Should -Match 'Add a concise repository summary that explains what this project does and why it exists.' + } + + It 'produces a downstream README that is compatible with generated Markdown validation' { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-Apply') | Out-Null + + $scriptPath = Join-Path -Path $script:DownstreamRepo -ChildPath 'scripts/Update-GeneratedMarkdown.ps1' + $output = & powershell.exe -NoProfile -File $scriptPath -Check 2>&1 + if ($LASTEXITCODE -ne 0) { + throw ($output -join [Environment]::NewLine) + } + } + + It 'stops when the repo has moved beyond the immediate cleanup window' { + $customSourcePath = Join-Path -Path $script:DownstreamRepo -ChildPath 'src/Public/Get-WingetPackage.ps1' + [System.IO.File]::WriteAllText($customSourcePath, 'function Get-WingetPackage {}', [System.Text.UTF8Encoding]::new($false)) + & git -C $script:DownstreamRepo add src/Public/Get-WingetPackage.ps1 | Out-Null + & git -C $script:DownstreamRepo commit -m 'add custom source' | Out-Null + + { + & Invoke-CleanupScript -RepoPath $script:DownstreamRepo + } | Should -Throw -ExpectedMessage '*immediate post-create window*' + } + + It 'returns parseable JSON output' { + $json = & Invoke-CleanupScript -RepoPath $script:DownstreamRepo -ExtraArguments @('-OutputFormat', 'Json') + $report = $json | ConvertFrom-Json + + $report.RepositoryName | Should -Be 'winget-pwsh-automation' + $report.ProjectType | Should -Be 'script' + $report.TemplateVersion | Should -Be '0.3.0' + $report.Report.Action | Should -Contain 'Remove' + $report.Report.Action | Should -Contain 'Rewrite' + } +} diff --git a/tests/unit/Invoke-ReadmeAlignment.Tests.ps1 b/tests/unit/Invoke-ReadmeAlignment.Tests.ps1 new file mode 100644 index 0000000..6e6bdf9 --- /dev/null +++ b/tests/unit/Invoke-ReadmeAlignment.Tests.ps1 @@ -0,0 +1,144 @@ +Describe 'Invoke-ReadmeAlignment' { + BeforeAll { + $script:RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..\..')).Path + $script:ScriptPath = Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-ReadmeAlignment.ps1' + $script:SkeletonPath = Join-Path -Path $script:RepoRoot -ChildPath 'templates/downstream/README.md' + + function New-ReadmeAlignmentRepo { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter()] + [string]$ReadmeContent = @' +# winget-pwsh-automation + +![Template Version](https://img.shields.io/badge/template-0.15.0-blue) + +Custom summary for this repository. + +## Engineering Principles in Practice + +Repository-owned engineering guidance. + +## Portfolio Context + +Repository-owned portfolio context. + +## Extra Context + +Repository-specific extra section. +'@ + ) + + New-Item -ItemType Directory -Path $Path -Force | Out-Null + + $files = @{ + 'README.md' = $ReadmeContent + 'AGENTS.md' = 'derived from powershell-dev-template' + '.github/copilot-instructions.md' = 'derived from powershell-dev-template' + '.codex/skills/downstream-guidance-sync/SKILL.md' = 'sync skill' + 'eng/runtime-policy.json' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'eng/runtime-policy.json') + 'scripts/Update-GeneratedMarkdown.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Update-GeneratedMarkdown.ps1') + 'scripts/Invoke-RepoChecks.ps1' = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-RepoChecks.ps1') + 'scripts/Invoke-TemplateGuidanceSync.ps1' = 'sync script' + 'templates/downstream/README.md' = Get-Content -Raw -LiteralPath $script:SkeletonPath + } + + foreach ($relativePath in $files.Keys) { + $fullPath = Join-Path -Path $Path -ChildPath $relativePath + $parent = Split-Path -Path $fullPath -Parent + if ($parent -and (-not (Test-Path -LiteralPath $parent))) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + + [System.IO.File]::WriteAllText($fullPath, $files[$relativePath], [System.Text.UTF8Encoding]::new($false)) + } + + & git -C $Path init -q -b work/readme | Out-Null + & git -C $Path config user.email 'test@example.invalid' | Out-Null + & git -C $Path config user.name 'Test User' | Out-Null + & git -C $Path add . | Out-Null + & git -C $Path commit -m 'initial repo' | Out-Null + } + + function Invoke-AlignmentScript { + param( + [Parameter(Mandatory)] + [string]$RepoPath, + + [Parameter()] + [string[]]$ExtraArguments = @() + ) + + $arguments = @( + '-NoProfile' + '-File' + $script:ScriptPath + '-RepoRoot' + $RepoPath + ) + $ExtraArguments + + $output = & powershell.exe @arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw ($output -join [Environment]::NewLine) + } + + return $output + } + } + + BeforeEach { + $script:TempRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('readme-alignment-{0}' -f [guid]::NewGuid()) + $script:RepoPath = Join-Path -Path $script:TempRoot -ChildPath 'winget-pwsh-automation' + New-ReadmeAlignmentRepo -Path $script:RepoPath + } + + AfterEach { + if (Test-Path -LiteralPath $script:TempRoot) { + Remove-Item -LiteralPath $script:TempRoot -Recurse -Force + } + } + + It 'reports missing and realigned shared sections in audit mode' { + $output = Invoke-AlignmentScript -RepoPath $script:RepoPath + $text = $output -join "`n" + + $text | Should -Match 'Portfolio Context' + $text | Should -Match 'Engineering Principles in Practice' + $text | Should -Match 'Missing' + $text | Should -Match 'Extra sections:' + $text | Should -Match 'Extra Context' + } + + It 'realigns the README while preserving portfolio and extra sections' { + Invoke-AlignmentScript -RepoPath $script:RepoPath -ExtraArguments @('-Apply') | Out-Null + + $readme = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoPath -ChildPath 'README.md') + $readme | Should -Match 'Quick navigation:' + $readme | Should -Match '## Portfolio Context' + $readme | Should -Match 'Repository-owned portfolio context\.' + $readme | Should -Match '## Engineering Principles in Practice' + $readme | Should -Match '## Validation And Maintenance' + $readme | Should -Match '## Template Versioning' + $readme | Should -Match '## Extra Context' + } + + It 'returns parseable JSON output' { + $json = Invoke-AlignmentScript -RepoPath $script:RepoPath -ExtraArguments @('-OutputFormat', 'Json') + $report = $json | ConvertFrom-Json + + $report.RepositoryName | Should -Be 'winget-pwsh-automation' + $report.HasChanges | Should -BeTrue + $report.StandardSections.Heading | Should -Contain 'Portfolio Context' + } + + It 'stops when required template-derived assets are missing' { + Remove-Item -LiteralPath (Join-Path -Path $script:RepoPath -ChildPath 'eng/runtime-policy.json') -Force + + { + Invoke-AlignmentScript -RepoPath $script:RepoPath + } | Should -Throw -ExpectedMessage '*template-derived downstream repositories*' + } +} diff --git a/tests/unit/Invoke-TemplateGuidanceSync.Tests.ps1 b/tests/unit/Invoke-TemplateGuidanceSync.Tests.ps1 index 278188c..e994317 100644 --- a/tests/unit/Invoke-TemplateGuidanceSync.Tests.ps1 +++ b/tests/unit/Invoke-TemplateGuidanceSync.Tests.ps1 @@ -5,11 +5,23 @@ Describe 'Invoke-TemplateGuidanceSync' { $script:GuidanceFiles = @( 'AGENTS.md' '.github/copilot-instructions.md' + '.codex/skills/change-delivery-workflow/SKILL.md' + '.codex/skills/change-delivery-workflow/agents/openai.yaml' + '.codex/skills/downstream-repo-cleanup/SKILL.md' + '.codex/skills/downstream-repo-cleanup/agents/openai.yaml' + '.codex/skills/readme-alignment/SKILL.md' + '.codex/skills/readme-alignment/agents/openai.yaml' + 'docs/agent-workflows.md' 'docs/ai-behavioral-contract.md' 'docs/ai-interaction-loop.md' 'docs/copilot-instructions-reference.md' 'docs/powershell-ai-operating-model.md' 'docs/decisions/README.md' + 'scripts/Initialize-DownstreamRepo.ps1' + 'scripts/Invoke-ReadmeAlignment.ps1' + 'scripts/Update-GeneratedMarkdown.ps1' + 'eng/runtime-policy.json' + 'templates/downstream/README.md' ) $script:InvokeSyncScript = { @@ -18,28 +30,27 @@ Describe 'Invoke-TemplateGuidanceSync' { [string[]]$ExtraArguments = @() ) - $parameters = @{ - TemplatePath = $script:TemplateRepo - Path = $script:TargetRepo + $arguments = @( + '-NoProfile' + '-File' + $script:SyncScript + '-TemplatePath' + $script:TemplateRepo + '-Path' + $script:TargetRepo + ) + $ExtraArguments + + $output = & powershell.exe @arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw ($output -join [Environment]::NewLine) } - for ($index = 0; $index -lt $ExtraArguments.Count; $index++) { - $name = $ExtraArguments[$index].TrimStart('-') - if (($index + 1) -lt $ExtraArguments.Count -and $ExtraArguments[$index + 1] -notmatch '^-') { - $parameters[$name] = $ExtraArguments[$index + 1] - $index++ - } - else { - $parameters[$name] = $true - } - } - - return & $script:SyncScript @parameters + return $output } } BeforeEach { - $script:TestRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('powershell-template-sync-{0}' -f [guid]::NewGuid()) + $script:TestRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('pwsh-template-sync-{0}' -f [guid]::NewGuid()) New-Item -ItemType Directory -Path $script:TestRoot | Out-Null $script:TemplateRepo = Join-Path -Path $script:TestRoot -ChildPath 'powershell-dev-template' $script:TargetRepo = Join-Path -Path $script:TestRoot -ChildPath 'downstream-repo' @@ -73,7 +84,7 @@ Describe 'Invoke-TemplateGuidanceSync' { } } - It 'detects missing guidance files and a missing README badge' { + It 'detects missing sync files, cleanup assets, and a missing README badge' { $output = & $script:InvokeSyncScript $output | Should -Contain 'Drift: True' @@ -82,7 +93,7 @@ Describe 'Invoke-TemplateGuidanceSync' { $output -join "`n" | Should -Match 'README\.md\s+Missing' } - It 'applies only approved guidance files and the README badge' { + It 'applies approved sync files, cleanup assets, and the README badge' { Set-Content -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'local-only.txt') -Value 'keep me' -Encoding utf8 & git -C $script:TargetRepo add local-only.txt | Out-Null & git -C $script:TargetRepo commit -m 'add local file' | Out-Null @@ -100,6 +111,22 @@ Describe 'Invoke-TemplateGuidanceSync' { Should -Match '!\[Template Version\]\(https://img\.shields\.io/badge/template-0\.6\.2-blue\)' } + It 'syncs cleanup workflow assets into downstream repos that predate cleanup support' { + & $script:InvokeSyncScript -ExtraArguments @('-Apply') | Out-Null + + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath '.codex/skills/change-delivery-workflow/SKILL.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath '.codex/skills/change-delivery-workflow/agents/openai.yaml') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'scripts/Initialize-DownstreamRepo.ps1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath '.codex/skills/downstream-repo-cleanup/SKILL.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath '.codex/skills/downstream-repo-cleanup/agents/openai.yaml') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath '.codex/skills/readme-alignment/SKILL.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath '.codex/skills/readme-alignment/agents/openai.yaml') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'scripts/Invoke-ReadmeAlignment.ps1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'scripts/Update-GeneratedMarkdown.ps1') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'eng/runtime-policy.json') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'templates/downstream/README.md') | Should -BeTrue + Test-Path -LiteralPath (Join-Path -Path $script:TargetRepo -ChildPath 'docs/agent-workflows.md') | Should -BeTrue + } It 'does not sync numbered ADR files' { $templateDecisionPath = Join-Path -Path $script:TemplateRepo -ChildPath 'docs/decisions/0001-template-decision.md' $targetDecisionPath = Join-Path -Path $script:TargetRepo -ChildPath 'docs/decisions/0001-downstream-decision.md' @@ -140,7 +167,7 @@ Describe 'Invoke-TemplateGuidanceSync' { '# Downstream Repo' '' '[![CI](https://example.invalid/ci.svg)](https://example.invalid/ci)' - '![PowerShell 5.1](https://img.shields.io/badge/PowerShell-5.1-blue)' + '![PowerShell 5.1](https://img.shields.io/badge/PowerShell-7.4-blue)' '' 'Project-owned README content.' ) -Encoding utf8 @@ -188,7 +215,7 @@ Describe 'Invoke-TemplateGuidanceSync' { { & $script:InvokeSyncScript -ExtraArguments @('-FailOnDrift') } | Should -Throw -ExpectedMessage '*drift detected*' } - It 'reports current state after apply' { + It 'reports current state after applying guidance and cleanup assets' { & $script:InvokeSyncScript -ExtraArguments @('-Apply') | Out-Null & git -C $script:TargetRepo add . | Out-Null & git -C $script:TargetRepo commit -m 'sync guidance' | Out-Null @@ -197,6 +224,7 @@ Describe 'Invoke-TemplateGuidanceSync' { $output | Should -Contain 'Drift: False' $output -join "`n" | Should -Match 'AGENTS\.md\s+Current' + $output -join "`n" | Should -Match 'Initialize-DownstreamRepo\.ps1\s+Current' $output -join "`n" | Should -Match 'README\.md\s+Current' } @@ -205,7 +233,10 @@ Describe 'Invoke-TemplateGuidanceSync' { New-Item -ItemType Directory -Path $nonGitTarget | Out-Null { - & $script:SyncScript -TemplatePath $script:TemplateRepo -Path $nonGitTarget + $output = & powershell.exe -NoProfile -File $script:SyncScript -TemplatePath $script:TemplateRepo -Path $nonGitTarget 2>&1 + if ($LASTEXITCODE -ne 0) { + throw ($output -join [Environment]::NewLine) + } } | Should -Throw -ExpectedMessage '*not a Git repository*' } } diff --git a/tests/unit/SkillScaffold.Tests.ps1 b/tests/unit/SkillScaffold.Tests.ps1 index 9077f69..449e4ae 100644 --- a/tests/unit/SkillScaffold.Tests.ps1 +++ b/tests/unit/SkillScaffold.Tests.ps1 @@ -1,17 +1,115 @@ Describe 'Repo-local skills' { BeforeAll { $script:RepoRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..\..')).Path + $script:ChangeDeliverySkillPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/change-delivery-workflow/SKILL.md' + $script:ChangeDeliverySkillMetadataPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/change-delivery-workflow/agents/openai.yaml' + $script:CleanupSkillPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/downstream-repo-cleanup/SKILL.md' + $script:CleanupSkillMetadataPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/downstream-repo-cleanup/agents/openai.yaml' $script:SkillPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/downstream-guidance-sync/SKILL.md' $script:SkillMetadataPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/downstream-guidance-sync/agents/openai.yaml' + $script:ReadmeAlignmentSkillPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/readme-alignment/SKILL.md' + $script:ReadmeAlignmentSkillMetadataPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/readme-alignment/agents/openai.yaml' $script:RuntimeSkillPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/runtime-policy-update/SKILL.md' $script:RuntimeSkillMetadataPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/runtime-policy-update/agents/openai.yaml' $script:VersionSkillPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/template-version-release/SKILL.md' $script:VersionSkillMetadataPath = Join-Path -Path $script:RepoRoot -ChildPath '.codex/skills/template-version-release/agents/openai.yaml' + $script:CleanupScriptPath = Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Initialize-DownstreamRepo.ps1' $script:SyncScriptPath = Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-TemplateGuidanceSync.ps1' + $script:ReadmeAlignmentScriptPath = Join-Path -Path $script:RepoRoot -ChildPath 'scripts/Invoke-ReadmeAlignment.ps1' $script:RuntimePolicyPath = Join-Path -Path $script:RepoRoot -ChildPath 'eng/runtime-policy.json' $script:AgentWorkflowsPath = Join-Path -Path $script:RepoRoot -ChildPath 'docs/agent-workflows.md' } + It 'includes the change delivery workflow skill' { + Test-Path -LiteralPath $script:ChangeDeliverySkillPath -PathType Leaf | Should -BeTrue + Test-Path -LiteralPath $script:ChangeDeliverySkillMetadataPath -PathType Leaf | Should -BeTrue + } + + It 'uses valid required change delivery skill frontmatter' { + $content = Get-Content -Raw -LiteralPath $script:ChangeDeliverySkillPath + + $content | Should -Match '^---\s*\r?\nname: change-delivery-workflow\r?\n' + $content | Should -Match '\r?\ndescription: .+change.+workflow.+\r?\n---' + } + + It 'documents sandbox recovery, branch discipline, and changelog expectations' { + $content = Get-Content -Raw -LiteralPath $script:ChangeDeliverySkillPath + + $content | Should -Match 'helper_sid_resolve_failed' + $content | Should -Match 'CodexSandboxOffline' + $content | Should -Match 'sandbox_permissions: require_escalated' + $content | Should -Match 'CHANGELOG\.md' + $content | Should -Match '## Unreleased' + $content | Should -Match 'non-main branch' + $content | Should -Match 'ready PR' + $content | Should -Match 'Conventional Commit' + } + + It 'uses valid change delivery skill UI metadata with an explicit skill prompt' { + $content = Get-Content -Raw -LiteralPath $script:ChangeDeliverySkillMetadataPath + + $content | Should -Match 'display_name: "Change Delivery Workflow"' + $content | Should -Match 'short_description: "Deliver ordinary repo changes with branch, changelog, PR, and release discipline"' + $content | Should -Match 'default_prompt: "Use \$change-delivery-workflow' + } + + It 'makes the change delivery workflow discoverable from repository agent instructions' { + $readmeContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'README.md') + $agentsContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'AGENTS.md') + $copilotContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.github/copilot-instructions.md') + $workflowContent = Get-Content -Raw -LiteralPath $script:AgentWorkflowsPath + + $readmeContent | Should -Match '\.codex/skills/change-delivery-workflow/SKILL\.md' + $agentsContent | Should -Match '\.codex/skills/change-delivery-workflow/SKILL\.md' + $copilotContent | Should -Match '\.codex/skills/change-delivery-workflow/SKILL\.md' + $workflowContent | Should -Match '\.codex/skills/change-delivery-workflow/SKILL\.md' + } + It 'includes the downstream repo cleanup skill' { + Test-Path -LiteralPath $script:CleanupSkillPath -PathType Leaf | Should -BeTrue + Test-Path -LiteralPath $script:CleanupSkillMetadataPath -PathType Leaf | Should -BeTrue + } + + It 'uses valid required cleanup skill frontmatter' { + $content = Get-Content -Raw -LiteralPath $script:CleanupSkillPath + + $content | Should -Match '^---\s*\r?\nname: downstream-repo-cleanup\r?\n' + $content | Should -Match '\r?\ndescription: .+downstream.+cleanup.+\r?\n---' + } + + It 'references the authoritative cleanup script and README badge contract' { + $content = Get-Content -Raw -LiteralPath $script:CleanupSkillPath + + $content | Should -Match 'Initialize-DownstreamRepo\.ps1' + $content | Should -Match 'README template version badge' + $content | Should -Match 'immediate post-create' + Test-Path -LiteralPath $script:CleanupScriptPath -PathType Leaf | Should -BeTrue + } + + It 'uses valid cleanup skill UI metadata with an explicit skill prompt' { + $content = Get-Content -Raw -LiteralPath $script:CleanupSkillMetadataPath + + $content | Should -Match 'display_name: "Downstream Repo Cleanup"' + $content | Should -Match 'short_description: "Normalize a new downstream repo"' + $content | Should -Match 'default_prompt: "Use \$downstream-repo-cleanup' + } + + It 'makes the cleanup skill discoverable from repository agent instructions' { + $readmeContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'README.md') + $agentsContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'AGENTS.md') + $copilotContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.github/copilot-instructions.md') + $workflowContent = Get-Content -Raw -LiteralPath $script:AgentWorkflowsPath + + $readmeContent | Should -Match 'Initialize-DownstreamRepo\.ps1' + $readmeContent | Should -Match 'template version badge' + $agentsContent | Should -Match '\.codex/skills/downstream-repo-cleanup/SKILL\.md' + $agentsContent | Should -Match 'Initialize-DownstreamRepo\.ps1' + $copilotContent | Should -Match '\.codex/skills/downstream-repo-cleanup/SKILL\.md' + $copilotContent | Should -Match 'Initialize-DownstreamRepo\.ps1' + $workflowContent | Should -Match '\.codex/skills/change-delivery-workflow/SKILL\.md' + $workflowContent | Should -Match '\.codex/skills/downstream-repo-cleanup/SKILL\.md' + $workflowContent | Should -Match 'scripts/Initialize-DownstreamRepo\.ps1' + } + It 'includes the downstream guidance sync skill' { Test-Path -LiteralPath $script:SkillPath -PathType Leaf | Should -BeTrue Test-Path -LiteralPath $script:SkillMetadataPath -PathType Leaf | Should -BeTrue @@ -24,10 +122,12 @@ Describe 'Repo-local skills' { $content | Should -Match '\r?\ndescription: .+downstream.+sync.+\r?\n---' } - It 'references the authoritative sync script' { + It 'references the authoritative sync script and cleanup asset delivery path' { $content = Get-Content -Raw -LiteralPath $script:SkillPath $content | Should -Match 'Invoke-TemplateGuidanceSync\.ps1' + $content | Should -Match 'Initialize-DownstreamRepo\.ps1' + $content | Should -Match 'does not perform cleanup itself' Test-Path -LiteralPath $script:SyncScriptPath -PathType Leaf | Should -BeTrue } @@ -48,15 +148,20 @@ Describe 'Repo-local skills' { $content | Should -Match '\.github/copilot-instructions\.md' $content | Should -Match 'README template-version badge' $content | Should -Match 'docs/decisions/README\.md' - $content | Should -Match 'must not update downstream source, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, development-environment configuration, runtime policy, module manifests, scaffolds, or numbered project-specific ADRs' + $content | Should -Match 'scripts/Initialize-DownstreamRepo\.ps1' + $content | Should -Match '\.codex/skills/downstream-repo-cleanup/' + $content | Should -Match '\.codex/skills/readme-alignment/' + $content | Should -Match 'does not perform cleanup or README alignment itself' + $content | Should -Match 'must not update downstream source, tests, Pester configuration, PSScriptAnalyzer settings, CI workflows, development-environment configuration, module manifests, scaffolds other than the cleanup and README workflow assets, or numbered project-specific ADRs' } It 'uses valid skill UI metadata with an explicit skill prompt' { $content = Get-Content -Raw -LiteralPath $script:SkillMetadataPath $content | Should -Match 'display_name: "Downstream Guidance Sync"' - $content | Should -Match 'short_description: "Sync template AI guidance into downstream repos"' + $content | Should -Match 'short_description: "Sync template guidance and cleanup assets into downstream repos"' $content | Should -Match 'default_prompt: "Use \$downstream-guidance-sync' + $content | Should -Match 'cleanup workflow assets' } It 'is discoverable from repository agent instructions' { @@ -67,8 +172,54 @@ Describe 'Repo-local skills' { $agentsContent | Should -Match '\.codex/skills/downstream-guidance-sync/SKILL\.md' $agentsContent | Should -Match 'Invoke-TemplateGuidanceSync\.ps1' + $agentsContent | Should -Match 'Initialize-DownstreamRepo\.ps1' $copilotContent | Should -Match '\.codex/skills/downstream-guidance-sync/SKILL\.md' $copilotContent | Should -Match 'Invoke-TemplateGuidanceSync\.ps1' + $copilotContent | Should -Match 'Initialize-DownstreamRepo\.ps1' + } + + It 'includes the README alignment skill' { + Test-Path -LiteralPath $script:ReadmeAlignmentSkillPath -PathType Leaf | Should -BeTrue + Test-Path -LiteralPath $script:ReadmeAlignmentSkillMetadataPath -PathType Leaf | Should -BeTrue + } + + It 'uses valid required README alignment skill frontmatter' { + $content = Get-Content -Raw -LiteralPath $script:ReadmeAlignmentSkillPath + + $content | Should -Match '^---\s*\r?\nname: readme-alignment\r?\n' + $content | Should -Match '\r?\ndescription: .+README.+align.+\r?\n---' + } + + It 'references the authoritative README alignment script and shared skeleton boundary' { + $content = Get-Content -Raw -LiteralPath $script:ReadmeAlignmentSkillPath + + $content | Should -Match 'Invoke-ReadmeAlignment\.ps1' + $content | Should -Match 'template-derived downstream repositories' + $content | Should -Match 'Portfolio Context' + $content | Should -Match 'Template Versioning' + Test-Path -LiteralPath $script:ReadmeAlignmentScriptPath -PathType Leaf | Should -BeTrue + } + + It 'uses valid README alignment skill UI metadata with an explicit skill prompt' { + $content = Get-Content -Raw -LiteralPath $script:ReadmeAlignmentSkillMetadataPath + + $content | Should -Match 'display_name: "README Alignment"' + $content | Should -Match 'short_description: "Align a downstream README to the shared portfolio skeleton"' + $content | Should -Match 'default_prompt: "Use \$readme-alignment' + } + + It 'makes the README alignment skill discoverable from repository agent instructions' { + $readmeContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'README.md') + $agentsContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'AGENTS.md') + $copilotContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath '.github/copilot-instructions.md') + $workflowContent = Get-Content -Raw -LiteralPath $script:AgentWorkflowsPath + + $readmeContent | Should -Match '\.codex/skills/readme-alignment/SKILL\.md' + $agentsContent | Should -Match '\.codex/skills/readme-alignment/SKILL\.md' + $copilotContent | Should -Match '\.codex/skills/readme-alignment/SKILL\.md' + $workflowContent | Should -Match '\.codex/skills/readme-alignment/SKILL\.md' + $workflowContent | Should -Match 'scripts/Invoke-ReadmeAlignment\.ps1' + $workflowContent | Should -Match 'templates/downstream/README\.md' } It 'includes the runtime policy update skill' { @@ -143,6 +294,8 @@ Describe 'Repo-local skills' { $content | Should -Match 'docs/template-evolution\.md' $content | Should -Match 'README\.md' $content | Should -Match 'vX\.Y\.Z' + $content | Should -Match 'git tag vX.Y.Z' + $content | Should -Match 'verified' $content | Should -Match 'GitHub Release' } @@ -176,8 +329,14 @@ Describe 'Repo-local skills' { $workflowContent = Get-Content -Raw -LiteralPath $script:AgentWorkflowsPath $readmeContent = Get-Content -Raw -LiteralPath (Join-Path -Path $script:RepoRoot -ChildPath 'README.md') + $workflowContent | Should -Match '\.codex/skills/change-delivery-workflow/SKILL\.md' + $workflowContent | Should -Match '\.codex/skills/downstream-repo-cleanup/SKILL\.md' + $workflowContent | Should -Match 'scripts/Initialize-DownstreamRepo\.ps1' $workflowContent | Should -Match '\.codex/skills/downstream-guidance-sync/SKILL\.md' $workflowContent | Should -Match 'scripts/Invoke-TemplateGuidanceSync\.ps1' + $workflowContent | Should -Match '\.codex/skills/readme-alignment/SKILL\.md' + $workflowContent | Should -Match 'scripts/Invoke-ReadmeAlignment\.ps1' + $workflowContent | Should -Match 'templates/downstream/README\.md' $workflowContent | Should -Match '\.codex/skills/runtime-policy-update/SKILL\.md' $workflowContent | Should -Match 'eng/runtime-policy\.json' $workflowContent | Should -Match '\.codex/skills/template-version-release/SKILL\.md' @@ -188,15 +347,36 @@ Describe 'Repo-local skills' { } It 'documents success criteria for each repo-local workflow' { + $changeDeliveryContent = Get-Content -Raw -LiteralPath $script:ChangeDeliverySkillPath + $cleanupContent = Get-Content -Raw -LiteralPath $script:CleanupSkillPath $syncContent = Get-Content -Raw -LiteralPath $script:SkillPath + $readmeAlignmentContent = Get-Content -Raw -LiteralPath $script:ReadmeAlignmentSkillPath $runtimeContent = Get-Content -Raw -LiteralPath $script:RuntimeSkillPath $versionContent = Get-Content -Raw -LiteralPath $script:VersionSkillPath + $changeDeliveryContent | Should -Match '## Success Criteria' + $changeDeliveryContent | Should -Match 'CHANGELOG\.md' + $changeDeliveryContent | Should -Match 'non-main branch' + $changeDeliveryContent | Should -Match 'ready PR' + $changeDeliveryContent | Should -Match 'post-merge cleanup' + + $cleanupContent | Should -Match '## Success Criteria' + $cleanupContent | Should -Match 'README template version badge' + $cleanupContent | Should -Match 'project-specific work' + $cleanupContent | Should -Match 'downstream validation' + $syncContent | Should -Match '## Success Criteria' $syncContent | Should -Match 'audit output' $syncContent | Should -Match 'non-main downstream branch' $syncContent | Should -Match 'sync allowlist' - $syncContent | Should -Match 'synced template guidance version' + $syncContent | Should -Match 'cleanup-asset delivery' + $syncContent | Should -Match 'validation result' + + $readmeAlignmentContent | Should -Match '## Success Criteria' + $readmeAlignmentContent | Should -Match 'shared skeleton order' + $readmeAlignmentContent | Should -Match 'Portfolio Context' + $readmeAlignmentContent | Should -Match 'Template Versioning' + $readmeAlignmentContent | Should -Match 'downstream validation' $runtimeContent | Should -Match '## Success Criteria' $runtimeContent | Should -Match 'eng/runtime-policy\.json' @@ -209,21 +389,44 @@ Describe 'Repo-local skills' { $versionContent | Should -Match 'README template badge' $versionContent | Should -Match 'CHANGELOG\.md' $versionContent | Should -Match 'vX\.Y\.Z' + $versionContent | Should -Match 'lightweight' $versionContent | Should -Match 'Test-TemplateVersion\.ps1 -CheckTag' + $versionContent | Should -Match 'verified' $versionContent | Should -Match 'GitHub Release exists' } It 'documents rationale for workflow boundaries' { + $changeDeliveryContent = Get-Content -Raw -LiteralPath $script:ChangeDeliverySkillPath + $cleanupContent = Get-Content -Raw -LiteralPath $script:CleanupSkillPath $syncContent = Get-Content -Raw -LiteralPath $script:SkillPath + $readmeAlignmentContent = Get-Content -Raw -LiteralPath $script:ReadmeAlignmentSkillPath $runtimeContent = Get-Content -Raw -LiteralPath $script:RuntimeSkillPath $versionContent = Get-Content -Raw -LiteralPath $script:VersionSkillPath + $changeDeliveryContent | Should -Match '## Why This Exists' + $changeDeliveryContent | Should -Match 'helper_sid_resolve_failed' + $changeDeliveryContent | Should -Match 'Windows sandbox' + $changeDeliveryContent | Should -Match 'release/version contract' + $changeDeliveryContent | Should -Match 'ordinary repository work' + + $cleanupContent | Should -Match '## Why This Exists' + $cleanupContent | Should -Match 'template-maintainer artifacts' + $cleanupContent | Should -Match 'README template version badge' + $cleanupContent | Should -Match 'first-run cleanup' + $syncContent | Should -Match '## Why This Exists' $syncContent | Should -Match 'independent projects' $syncContent | Should -Match 'AI guidance' + $syncContent | Should -Match 'cleanup script' + $syncContent | Should -Match 'runtime-policy README-generation assets' $syncContent | Should -Match 'project-owned' $syncContent | Should -Match 'should not be clobbered' + $readmeAlignmentContent | Should -Match '## Why This Exists' + $readmeAlignmentContent | Should -Match 'shared skeleton' + $readmeAlignmentContent | Should -Match 'generator-driven' + $readmeAlignmentContent | Should -Match 'repo-specific sections' + $runtimeContent | Should -Match '## Why This Exists' $runtimeContent | Should -Match 'Runtime and tooling pins' $runtimeContent | Should -Match 'eng/runtime-policy\.json' diff --git a/tests/unit/TemplateScaffold.Tests.ps1 b/tests/unit/TemplateScaffold.Tests.ps1 index fa0a913..64ea343 100644 --- a/tests/unit/TemplateScaffold.Tests.ps1 +++ b/tests/unit/TemplateScaffold.Tests.ps1 @@ -45,8 +45,9 @@ Describe 'Template scaffold' { It 'documents the Windows PowerShell baseline decision as the first repository ADR' { $decisionsPath = Join-Path -Path $script:RepoRoot -ChildPath 'docs/decisions' $numberedAdrs = @(Get-ChildItem -LiteralPath $decisionsPath -Filter '*.md' -File | Where-Object { $_.Name -match '^\d{4}-' }) - $numberedAdrs.Count | Should -Be 1 + $numberedAdrs.Count | Should -Be 2 $numberedAdrs[0].Name | Should -Be '0001-windows-powershell-development-baseline.md' + $numberedAdrs[1].Name | Should -Be '0002-downstream-readme-workflow.md' $adrPath = Join-Path -Path $decisionsPath -ChildPath '0001-windows-powershell-development-baseline.md' Test-Path -LiteralPath $adrPath -PathType Leaf | Should -BeTrue