diff --git a/AGENTS.md b/AGENTS.md index a3c149d..5966e0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,29 +1,72 @@ -# Agents +# Agent Instructions ## Main directive -Everything is a work in progress and can be updated and improved. -If you find a problem, fix it if it's small; otherwise, register it as an issue in the respective repo. +Everything is a work in progress and can be updated and improved. Fix a small problem when it is directly in scope; register a larger or unrelated problem as an issue in the repository that owns it. -## Install the ecosystem +This repository belongs to `github.com/MSXOrg`. -1. Create a folder in the home directory called `.msx`: -2. Clone the ecosystem locally: - 1. — requires PRs to be updated. - - Clone as bare and use worktrees. - - Create a worktree for every branch. Use a concise `-` worktree folder for a topic branch named `/-`. - 2. — work directly towards main. - - Simple clone, only main. +## Install and synchronize the ecosystem -To install: -- Clone the repos in the users home folder under a sub-folder named `.msx`. -- Set configs locally to each of these repos using the GitHub username and email. +The agent workspace lives under `~/.msx`: -## Working with the ecosystem +| Repository | Local path | Purpose | Change model | +| --- | --- | --- | --- | +| `MSXOrg/docs` | `~/.msx/docs.git` + `~/.msx/docs` | Bare backing repository plus clean readable main worktree for reviewed organization context. | Pull requests through topic worktrees only. | +| `MSXOrg/memory` | `~/.msx/memory` | Durable organization memory: prior decisions, gotchas, and reusable working knowledge. | Commit and push directly to `main`, per that repository's policy. | -1. Get to know this repo first: - - [README](README.md) for what this repository is and how it builds. - - [CONTRIBUTING](CONTRIBUTING.md) for how to contribute and the review process. -2. Read `~/.msx/docs` - start with the index to get an overview of what is here. -3. Read `~/.msx/memory` - start with the index to get an overview of what is here. Use this while working - commit your memories here for work inside the PSModule organization. -4. While working with the code, do small micro commits and push on every commit. This will make it easier to review and merge your changes. +From this repository, install missing context repositories and synchronize every existing clone before use: + +```powershell +pwsh bootstrap/Initialize-MsxWorkspace.ps1 ` + -UserName '' ` + -UserEmail '' +``` + +Projects that add their own docs and memory provide plug-in coordinates without changing the synchronization implementation: + +```powershell +$projects = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = 'https://github.com/MSXOrg/docs.git' + MemoryUrl = 'https://github.com/MSXOrg/memory.git' + } + @{ + Name = 'PSModule' + Path = 'projects/PSModule' + DocsUrl = 'https://github.com/PSModule/docs.git' + MemoryUrl = 'https://github.com/PSModule/memory.git' + } +) +& ./bootstrap/Initialize-MsxWorkspace.ps1 -Project $projects +``` + +The bootstrap writes identity only to each context repository's local git configuration. It must succeed before any context is read; do not continue with missing, dirty, diverged, wrong-branch, unreachable, or stale context. + +Use a dedicated worktree for every topic branch. Follow [Git Worktrees](src/docs/Ways-of-Working/Git-Worktrees.md) for the local layout and [Branching and Merging](src/docs/Ways-of-Working/Branching-and-Merging.md) for `/-` branch names. + +## Canonical context + +- Docs root: [src/docs/index.md](src/docs/index.md) +- Organization memory: `~/.msx/memory/index.md` + +## Before acting + +1. Segment the work by host, organization, repository, path, and task. +2. Synchronize every canonical context repository to its remote default branch; stop if any context may be stale. +3. Start at the docs root index and follow [Ways of Working](src/docs/Ways-of-Working/index.md) to the canonical [Workflow](src/docs/Ways-of-Working/Workflow.md). +4. Infer the current stage from the task and its artifacts, then read the linked stage procedure. +5. Read [README.md](README.md), [CONTRIBUTING.md](CONTRIBUTING.md), relevant standards, and organization memory. +6. Apply path-specific local rules only when they match the files in scope. + +## Working in this repository + +1. Use [README.md](README.md) to understand what this repository is and how it builds. +2. Use [CONTRIBUTING.md](CONTRIBUTING.md) for its contribution and review contract. +3. Keep work reviewable with small, descriptive micro-commits. +4. Push every commit so the remote branch, CI, and draft pull request reflect current work. +5. Improve organization memory when a verified lesson is likely to matter again; commit and push MSXOrg memory directly to `main`. + +This file owns bootstrap and repository-specific operating instructions. The linked documentation owns reusable process knowledge; this file does not redefine a workflow stage, coding standard, or review convention. diff --git a/bootstrap/AGENTS.template.md b/bootstrap/AGENTS.template.md index 13f7014..dd56c2c 100644 --- a/bootstrap/AGENTS.template.md +++ b/bootstrap/AGENTS.template.md @@ -2,28 +2,134 @@ The single starting point for any agent, in any repository. Before doing anything else, make sure the central workspace exists locally, then read from it. +## Main directive + +Everything is a work in progress and can be updated and improved. Fix a small problem when it is directly in scope; register a larger or unrelated problem as an issue in the repository that owns it. + ## First — bootstrap the workspace -The workspace is a git-isolated clone of the central repositories under `~/.msx`. Set it up (idempotent — clones what is missing, attempts to fast-forward the rest): +The workspace is a git-isolated clone of the central repositories under `~/.msx`. Set it up before reading context. Existing context repositories must be clean, on their default branch, and exactly synchronized with the remote: ```powershell -$docs = Join-Path $HOME '.msx/docs' +$workspaceRoot = if ($env:MSX_WORKSPACE_ROOT) { $env:MSX_WORKSPACE_ROOT } else { Join-Path $HOME '.msx' } +$docsUrl = if ($env:MSX_DOCS_URL) { $env:MSX_DOCS_URL } else { 'https://github.com/MSXOrg/docs.git' } +$memoryUrl = if ($env:MSX_MEMORY_URL) { $env:MSX_MEMORY_URL } else { 'https://github.com/MSXOrg/memory.git' } +$docs = Join-Path $workspaceRoot 'docs' +$docsBacking = "$docs.git" if ((Test-Path $docs) -and -not (Test-Path (Join-Path $docs '.git'))) { throw "$docs exists but is not a git repository. Remove it and re-run." } if (-not (Test-Path (Join-Path $docs '.git'))) { - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $docs) | Out-Null - git clone https://github.com/MSXOrg/docs.git $docs + if (-not (Test-Path $docsBacking)) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $docs) | Out-Null + git clone --bare $docsUrl $docsBacking + if ($LASTEXITCODE -ne 0) { + throw "Bare clone of MSXOrg/docs failed (exit $LASTEXITCODE). Check network access and credentials." + } + } + if ((git --git-dir=$docsBacking rev-parse --is-bare-repository) -ne 'true') { + throw "$docsBacking exists but is not a bare repository." + } + if ((git --git-dir=$docsBacking remote get-url origin) -ne $docsUrl) { + throw "$docsBacking origin does not match canonical $docsUrl." + } + $refspec = '+refs/heads/*:refs/remotes/origin/*' + if ($refspec -notin @(git --git-dir=$docsBacking config --get-all remote.origin.fetch)) { + git --git-dir=$docsBacking config --add remote.origin.fetch $refspec + if ($LASTEXITCODE -ne 0) { throw "Could not configure $docsBacking." } + } + git --git-dir=$docsBacking fetch origin --prune --quiet + if ($LASTEXITCODE -ne 0) { throw "Could not refresh $docsBacking. Do not use stale context." } + git --git-dir=$docsBacking remote set-head origin --auto | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Could not detect the MSXOrg/docs default branch." } + $defaultRef = (git --git-dir=$docsBacking symbolic-ref --short refs/remotes/origin/HEAD | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { throw "Could not resolve origin/HEAD in $docsBacking." } + $defaultBranch = $defaultRef -replace '^origin/', '' + $remoteHead = (git --git-dir=$docsBacking rev-parse $defaultRef | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { throw "Could not resolve $defaultRef in $docsBacking." } + $localRef = "refs/heads/$defaultBranch" + $localHead = (git --git-dir=$docsBacking rev-parse --verify $localRef 2>$null | Out-String).Trim() + if ($LASTEXITCODE -eq 128) { + git --git-dir=$docsBacking update-ref $localRef $remoteHead + } elseif ($LASTEXITCODE -ne 0) { + throw "Could not inspect $localRef in $docsBacking." + } elseif ($localHead -ne $remoteHead) { + git --git-dir=$docsBacking merge-base --is-ancestor $localHead $remoteHead + if ($LASTEXITCODE -ne 0) { throw "$localRef is ahead or diverged in $docsBacking." } + if ("branch $localRef" -in @(git --git-dir=$docsBacking worktree list --porcelain)) { + throw "$localRef is checked out elsewhere. Update that worktree first." + } + git --git-dir=$docsBacking update-ref $localRef $remoteHead $localHead + } + if ($LASTEXITCODE -ne 0 -or (git --git-dir=$docsBacking rev-parse $localRef) -ne $remoteHead) { + throw "$localRef is not exactly synchronized with $defaultRef." + } + git --git-dir=$docsBacking worktree add $docs $defaultBranch + if ($LASTEXITCODE -ne 0) { + throw "Could not create the canonical MSXOrg/docs worktree at $docs." + } +} else { + if ((git -C $docs remote get-url origin) -ne $docsUrl) { + throw "$docs origin does not match canonical $docsUrl." + } + $refspec = '+refs/heads/*:refs/remotes/origin/*' + if ($refspec -notin @(git -C $docs config --get-all remote.origin.fetch)) { + git -C $docs config --add remote.origin.fetch $refspec + if ($LASTEXITCODE -ne 0) { + throw "Could not configure remote tracking branches for MSXOrg/docs (exit $LASTEXITCODE)." + } + } + git -C $docs fetch origin --prune --quiet if ($LASTEXITCODE -ne 0) { - throw "git clone of MSXOrg/docs failed (exit $LASTEXITCODE). Check network access and github.com credentials, then re-run." + throw "git fetch of MSXOrg/docs failed (exit $LASTEXITCODE). Do not use stale context." + } + git -C $docs remote set-head origin --auto | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Could not detect the MSXOrg/docs default branch." } + $defaultRef = (git -C $docs symbolic-ref --short refs/remotes/origin/HEAD | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { throw "Could not resolve origin/HEAD in $docs." } + $defaultBranch = $defaultRef -replace '^origin/', '' + $branch = (git -C $docs branch --show-current | Out-String).Trim() + if ($branch -ne $defaultBranch) { + throw "$docs is on '$branch', not '$defaultBranch'. Switch branches before using this context." + } + if (@(git -C $docs status --porcelain).Count -gt 0) { + throw "$docs has uncommitted changes. Resolve them before using this context." + } + git -C $docs merge --ff-only --quiet $defaultRef + if ($LASTEXITCODE -ne 0) { + throw "MSXOrg/docs cannot fast-forward to $defaultRef. Do not use stale context." + } + if ((git -C $docs rev-parse HEAD) -ne (git -C $docs rev-parse $defaultRef)) { + throw "$docs is not exactly synchronized with $defaultRef. Reconcile local commits before using this context." } } -pwsh (Join-Path $docs 'bootstrap/Initialize-MsxWorkspace.ps1') +$projects = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = $docsUrl + MemoryUrl = $memoryUrl + } + # Add project-specific entries when this template is adopted there: + # @{ + # Name = 'PSModule' + # Path = 'projects/PSModule' + # DocsUrl = 'https://github.com/PSModule/docs.git' + # MemoryUrl = 'https://github.com/PSModule/memory.git' + # } +) +& (Join-Path $docs 'bootstrap/Initialize-MsxWorkspace.ps1') -Root $workspaceRoot -Project $projects +if ($LASTEXITCODE -ne 0) { + throw "Context synchronization failed. Do not read context until every project is current." +} ``` +Keep the MSXOrg entry and add only the additional project coordinates required by repositories that inherit this template. Every project reuses the same synchronization and validation implementation. + This produces: -- `~/.msx/docs` — how work is done: ways of working, coding standards, and agent roles. The same content published at . +- `~/.msx/docs.git` — bare backing repository for central docs. +- `~/.msx/docs` — clean, readable main worktree containing ways of working, standards, and workflow guidance. - `~/.msx/memory` — what has been learned before: durable notes and prior session context. Each clone has repository-local git config only; it never modifies the global git config or the repository being worked in (git still reads them, but only repository-local config is written). @@ -32,10 +138,22 @@ Each clone has repository-local git config only; it never modifies the global gi ## Then — read before acting -1. Read the relevant pages under `~/.msx/docs` for the task at hand. -2. Read `~/.msx/memory` for prior decisions, pitfalls, and context. +1. Start at `~/.msx/docs/src/docs/index.md`. +2. Follow the Ways of Working index to `Workflow.md`. +3. Infer the current stage from the task and its artifacts, then read the linked stage procedure. +4. Read the relevant standards, repository context, and `~/.msx/memory`. + +Clear task language may shortcut the index trail: `Review this PR ` enters Review, `Make this issue ` enters Define, and `Implement ` enters Implement. The linked documentation owns each procedure; this file does not define a separate agent or skill. + +## Work in the selected repository + +1. Read its `README.md` to understand the repository and its build. +2. Read its `CONTRIBUTING.md` for the contribution and review contract. +3. Use a dedicated worktree and the branch naming defined by the canonical Ways of Working. +4. Make small, descriptive micro-commits and push every commit so remote state, CI, and the draft pull request stay current. +5. Capture verified reusable lessons in organization memory, following that repository's own instructions. ## Two write rules -- **Docs change through pull requests.** Branch inside `~/.msx/docs` and open a pull request; never push its `main`. -- **Memory pushes to main.** Commit and push notes directly inside `~/.msx/memory`; no pull request. +- **Docs change through topic worktrees and pull requests.** Create a topic worktree from `~/.msx/docs.git`; never branch or work inside the canonical `~/.msx/docs` main worktree. +- **Memory follows repository policy.** Read the selected memory repository's `AGENTS.md` and `CONTRIBUTING.md` before writing. diff --git a/bootstrap/Initialize-MsxWorkspace.ps1 b/bootstrap/Initialize-MsxWorkspace.ps1 index ad644d8..80ddff9 100644 --- a/bootstrap/Initialize-MsxWorkspace.ps1 +++ b/bootstrap/Initialize-MsxWorkspace.ps1 @@ -3,39 +3,52 @@ <# .SYNOPSIS - Clone or update the MSX central workspace (docs + memory) in a git-isolated location under $HOME. + Clone or update canonical project context repositories in a git-isolated workspace under $HOME. .DESCRIPTION The single starting point for every agent. It ensures the central - documentation and memory repositories exist locally under one dedicated - workspace, so an agent reads the same evergreen docs and the same prior - memory regardless of which repository it is working in. + documentation and memory repositories for each configured project exist + locally under one dedicated workspace, so an agent reads current canonical + context regardless of which repository it is working in. The workspace is deliberately kept separate from the repositories an agent works in: - - Each clone gets repository-local git config only. Nothing here modifies the - global git config or the working repository's config; git still reads global - and system config as usual, but this script writes only repository-local config. - - Documentation (MSXOrg/docs) is context and is changed through pull requests - only; this script never pushes its main branch. - - Memory (MSXOrg/memory) is append-only context; notes are committed and - pushed to main directly, without a pull request. + - Each docs repository uses a bare backing repository plus a canonical clean + default-branch worktree. Topic branches use separate worktrees. + - Each memory repository remains a simple default-branch checkout. + - Every checkout gets repository-local git config only. Nothing here modifies + global git config or the working product repository. + - The script synchronizes context but never writes or pushes repository content. - The script is idempotent: it clones what is missing and attempts to - fast-forward what is already present, leaving a repository unchanged (with a - warning) when it cannot fast-forward. + The script is idempotent: it clones what is missing and synchronizes every + existing context repository to the exact remote default-branch head. It stops + before context is read when a repository is dirty, on another branch, locally + ahead, diverged, or unavailable. .EXAMPLE ./Initialize-MsxWorkspace.ps1 - Clones missing repositories and attempts to fast-forward existing ones under ~/.msx. + Clones missing repositories and exactly synchronizes existing ones under ~/.msx. .EXAMPLE ./Initialize-MsxWorkspace.ps1 -Root /work/.msx -Verbose Uses a custom workspace root and logs each step. +.EXAMPLE + $projects = @( + @{ + Name = 'PSModule' + Path = 'projects/PSModule' + DocsUrl = 'https://github.com/PSModule/docs.git' + MemoryUrl = 'https://github.com/PSModule/memory.git' + } + ) + ./Initialize-MsxWorkspace.ps1 -Project $projects + Installs a project's docs and memory under a project-specific workspace path. + .OUTPUTS - [pscustomobject] with Repository, Path, and Changes for each workspace repository. + [pscustomobject] with Repository, Path, BackingPath, and Changes for each + workspace repository. #> [CmdletBinding(SupportsShouldProcess)] param( @@ -52,62 +65,508 @@ param( # The git author email written to each clone's local config. [Parameter()] [ValidateNotNullOrEmpty()] - [string] $UserEmail = 'MariusStorhaug@users.noreply.github.com' + [string] $UserEmail = 'MariusStorhaug@users.noreply.github.com', + + # Projects whose canonical docs and memory repositories must be synchronized. + [Parameter()] + [ValidateNotNullOrEmpty()] + [hashtable[]] $Project = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = 'https://github.com/MSXOrg/docs.git' + MemoryUrl = 'https://github.com/MSXOrg/memory.git' + } + ) ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ((-not $PSBoundParameters.ContainsKey('UserName')) -or (-not $PSBoundParameters.ContainsKey('UserEmail'))) { - Write-Warning "Using part of the default maintainer identity ($UserName <$UserEmail>). Pass both -UserName and -UserEmail to attribute your own commits (memory pushes to main)." + Write-Warning "Using part of the default maintainer identity ($UserName <$UserEmail>). Pass both -UserName and -UserEmail to set your own repository-local author identity." } -$repositories = @( - [pscustomobject]@{ Name = 'docs'; Url = 'https://github.com/MSXOrg/docs.git'; Changes = 'pull requests' } - [pscustomobject]@{ Name = 'memory'; Url = 'https://github.com/MSXOrg/memory.git'; Changes = 'push to main' } -) +function Assert-ContextOrigin { + param( + [Parameter(Mandatory)] + [string] $GitPath, + + [Parameter(Mandatory)] + [string] $RepositoryUrl, + + [Parameter()] + [switch] $Bare + ) + + [string[]] $gitRoot = if ($Bare) { @("--git-dir=$GitPath") } else { @('-C', $GitPath) } + $originUrl = (& git @gitRoot remote get-url origin | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw "Cannot resolve origin for '$GitPath'. Configure it as '$RepositoryUrl'." + } + if ($originUrl -ne $RepositoryUrl) { + throw "Origin for '$GitPath' is '$originUrl', not canonical '$RepositoryUrl'. Repair it before using this context." + } +} + +function Sync-ContextRemote { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $GitPath, + + [Parameter(Mandatory)] + [string] $RepositoryUrl, + + [Parameter()] + [switch] $Bare + ) + + if (-not $PSCmdlet.ShouldProcess($GitPath, 'Fetch canonical remote state')) { + return + } + + [string[]] $gitRoot = if ($Bare) { @("--git-dir=$GitPath") } else { @('-C', $GitPath) } + Assert-ContextOrigin -GitPath $GitPath -RepositoryUrl $RepositoryUrl -Bare:$Bare + $allBranchesRefspec = '+refs/heads/*:refs/remotes/origin/*' + $fetchRefspecs = @(& git @gitRoot config --get-all remote.origin.fetch) + if ($LASTEXITCODE -notin @(0, 1)) { + throw "git config remote.origin.fetch failed for '$GitPath' (exit $LASTEXITCODE)." + } + if ($allBranchesRefspec -notin $fetchRefspecs) { + & git @gitRoot config --add remote.origin.fetch $allBranchesRefspec + if ($LASTEXITCODE -ne 0) { + throw "Could not configure remote tracking branches for '$GitPath' (exit $LASTEXITCODE)." + } + } + + & git @gitRoot fetch origin --prune --quiet + if ($LASTEXITCODE -ne 0) { + throw "git fetch failed for '$GitPath' (exit $LASTEXITCODE). Check access to $RepositoryUrl." + } + & git @gitRoot remote set-head origin --auto | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Cannot detect the remote default branch for '$GitPath'. Check origin before using this context." + } + + $defaultRef = (& git @gitRoot symbolic-ref --quiet --short refs/remotes/origin/HEAD | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw "Cannot resolve the remote default branch for '$GitPath'. Repair origin/HEAD before using this context." + } + $remoteHead = (& git @gitRoot rev-parse $defaultRef | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or -not $remoteHead) { + throw "Cannot resolve remote head '$defaultRef' for '$GitPath'." + } + return [pscustomobject]@{ + DefaultRef = $defaultRef + DefaultBranch = $defaultRef -replace '^origin/', '' + RemoteHead = $remoteHead + } +} + +function Sync-ContextCheckout { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [string] $RepositoryUrl + ) + + $remote = Sync-ContextRemote -GitPath $Path -RepositoryUrl $RepositoryUrl -Confirm:$false + if (-not $remote -or -not $PSCmdlet.ShouldProcess($Path, "Synchronize $($remote.DefaultBranch)")) { + return $remote + } + + $currentBranch = (git -C $Path branch --show-current | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw "git branch --show-current failed for '$Path' (exit $LASTEXITCODE)." + } + if ($currentBranch -ne $remote.DefaultBranch) { + throw "'$Path' is on '$currentBranch', not the default branch '$($remote.DefaultBranch)'. Switch branches before using this context." + } + + $status = @(git -C $Path status --porcelain) + if ($LASTEXITCODE -ne 0) { + throw "git status failed for '$Path' (exit $LASTEXITCODE)." + } + if ($status.Count -gt 0) { + throw "'$Path' has uncommitted changes. Commit, push, or remove them before using this context." + } + + git -C $Path merge --ff-only --quiet $remote.DefaultRef + if ($LASTEXITCODE -ne 0) { + throw "Could not fast-forward '$Path' to '$($remote.DefaultRef)'. Resolve its diverged history before using this context." + } + $localHead = (git -C $Path rev-parse HEAD | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw "git rev-parse HEAD failed for '$Path' (exit $LASTEXITCODE)." + } + if ($localHead -ne $remote.RemoteHead) { + throw "'$Path' is not exactly synchronized with '$($remote.DefaultRef)'. Push or reconcile local commits before using this context." + } + return $remote +} + +function Sync-BareDefaultBranch { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $BackingPath, + + [Parameter(Mandatory)] + [pscustomobject] $Remote + ) + + $localRef = "refs/heads/$($Remote.DefaultBranch)" + $localHead = (git --git-dir=$BackingPath rev-parse --verify $localRef 2>$null | Out-String).Trim() + if ($LASTEXITCODE -eq 128) { + if ($PSCmdlet.ShouldProcess($localRef, "Create at $($Remote.RemoteHead)")) { + git --git-dir=$BackingPath update-ref $localRef $Remote.RemoteHead + if ($LASTEXITCODE -ne 0) { + throw "Could not create bare docs branch '$localRef' in '$BackingPath'." + } + } + return + } + if ($LASTEXITCODE -ne 0) { + throw "Could not inspect bare docs branch '$localRef' in '$BackingPath'." + } + if ($localHead -eq $Remote.RemoteHead) { + return + } + + git --git-dir=$BackingPath merge-base --is-ancestor $localHead $Remote.RemoteHead + if ($LASTEXITCODE -ne 0) { + throw "Bare docs branch '$localRef' is ahead or diverged. Reconcile '$BackingPath' before creating its canonical worktree." + } + $worktreeState = @(git --git-dir=$BackingPath worktree list --porcelain) + if ($LASTEXITCODE -ne 0) { + throw "Could not inspect worktrees for '$BackingPath'." + } + if ("branch $localRef" -in $worktreeState) { + throw "Bare docs branch '$localRef' is checked out in another worktree. Update that worktree before creating the canonical one." + } + if ($PSCmdlet.ShouldProcess($localRef, "Fast-forward to $($Remote.RemoteHead)")) { + git --git-dir=$BackingPath update-ref $localRef $Remote.RemoteHead $localHead + if ($LASTEXITCODE -ne 0) { + throw "Could not fast-forward bare docs branch '$localRef' in '$BackingPath'." + } + } +} + +function Set-ContextIdentity { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [string] $Email + ) + + if (-not $PSCmdlet.ShouldProcess($Path, 'Set repository-local git identity')) { + return + } + git -C $Path config user.name $Name + if ($LASTEXITCODE -ne 0) { throw "git config user.name failed for '$Path' (exit $LASTEXITCODE)." } + git -C $Path config user.email $Email + if ($LASTEXITCODE -ne 0) { throw "git config user.email failed for '$Path' (exit $LASTEXITCODE)." } +} + +$projectNames = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) +$repositories = foreach ($projectDefinition in $Project) { + foreach ($key in @('Name', 'Path', 'DocsUrl', 'MemoryUrl')) { + if (-not $projectDefinition.ContainsKey($key) -or $null -eq $projectDefinition[$key]) { + throw "Project definitions require Name, Path, DocsUrl, and MemoryUrl. Missing '$key'." + } + } + + $projectName = [string] $projectDefinition.Name + $projectPath = ([string] $projectDefinition.Path).Trim() + if (-not $projectName.Trim()) { + throw 'Project Name must not be empty.' + } + $projectName = $projectName.Trim() + if (-not $projectNames.Add($projectName)) { + throw "Project definitions require unique names. Duplicate: '$projectName'." + } + $pathSegments = @($projectPath -split '[\\/]' | Where-Object { $_ -and $_ -ne '.' }) + if ([IO.Path]::IsPathRooted($projectPath) -or '..' -in $pathSegments) { + throw "Project Path '$projectPath' must be a safe path relative to the workspace root." + } + $projectPath = $pathSegments -join [IO.Path]::DirectorySeparatorChar + + $docsPath = if ($projectPath) { Join-Path $projectPath 'docs' } else { 'docs' } + $memoryPath = if ($projectPath) { Join-Path $projectPath 'memory' } else { 'memory' } + [pscustomobject]@{ + Name = "$projectName/docs" + Project = $projectName + ProjectPath = $projectPath + Kind = 'docs' + RelativePath = $docsPath + Url = [string] $projectDefinition.DocsUrl + Changes = 'pull requests' + } + [pscustomobject]@{ + Name = "$projectName/memory" + Project = $projectName + ProjectPath = $projectPath + Kind = 'memory' + RelativePath = $memoryPath + Url = [string] $projectDefinition.MemoryUrl + Changes = 'repository policy' + } +} + +$occupiedPaths = foreach ($repository in $repositories) { + [pscustomobject]@{ + Project = $repository.Project + Repository = $repository.Name + Path = $repository.RelativePath + } + if ($repository.Kind -eq 'docs') { + if ($repository.ProjectPath) { + [pscustomobject]@{ + Project = $repository.Project + Repository = "$($repository.Project) root" + Path = $repository.ProjectPath + } + } + [pscustomobject]@{ + Project = $repository.Project + Repository = "$($repository.Name) backing" + Path = "$($repository.RelativePath).git" + } + [pscustomobject]@{ + Project = $repository.Project + Repository = "$($repository.Name) migration backup" + Path = "$($repository.RelativePath).simple-clone-backup" + } + } +} +for ($left = 0; $left -lt $occupiedPaths.Count; $left++) { + $leftPath = ($occupiedPaths[$left].Path -replace '\\', '/').Trim('/').ToLowerInvariant() + for ($right = $left + 1; $right -lt $occupiedPaths.Count; $right++) { + if ($occupiedPaths[$left].Project -eq $occupiedPaths[$right].Project) { + continue + } + $rightPath = ($occupiedPaths[$right].Path -replace '\\', '/').Trim('/').ToLowerInvariant() + $collision = ( + $leftPath -eq $rightPath -or + $leftPath.StartsWith("$rightPath/", [StringComparison]::Ordinal) -or + $rightPath.StartsWith("$leftPath/", [StringComparison]::Ordinal) + ) + if ($collision) { + throw "Project workspace paths overlap: '$($occupiedPaths[$left].Path)' and '$($occupiedPaths[$right].Path)'." + } + } +} + +foreach ($repository in $repositories | Where-Object Kind -eq 'memory') { + $memoryPath = Join-Path $Root $repository.RelativePath + $memoryGitEntry = Join-Path $memoryPath '.git' + if (Test-Path $memoryGitEntry -PathType Leaf) { + throw "Memory context '$memoryPath' is a worktree, but memory requires a simple checkout with a .git directory." + } + if ((Test-Path $memoryPath) -and -not (Test-Path $memoryGitEntry -PathType Container)) { + throw "Memory context '$memoryPath' is not a supported simple git checkout." + } +} + +foreach ($repository in $repositories) { + $contextPath = Join-Path $Root $repository.RelativePath + $gitEntry = Join-Path $contextPath '.git' + if ($repository.Kind -eq 'memory' -and (Test-Path $gitEntry -PathType Container)) { + Assert-ContextOrigin -GitPath $contextPath -RepositoryUrl $repository.Url + } elseif ($repository.Kind -eq 'docs') { + if (Test-Path $gitEntry -PathType Container) { + Assert-ContextOrigin -GitPath $contextPath -RepositoryUrl $repository.Url + } elseif (Test-Path $gitEntry -PathType Leaf) { + $commonDir = (git -C $contextPath rev-parse --path-format=absolute --git-common-dir | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw "Cannot resolve docs backing repository for '$contextPath'." + } + Assert-ContextOrigin -GitPath $commonDir -RepositoryUrl $repository.Url -Bare + } elseif (Test-Path "$contextPath.git") { + Assert-ContextOrigin -GitPath "$contextPath.git" -RepositoryUrl $repository.Url -Bare + } + } +} if ($PSCmdlet.ShouldProcess($Root, 'Create workspace root')) { New-Item -ItemType Directory -Force -Path $Root | Out-Null } $results = foreach ($repo in $repositories) { - $path = Join-Path $Root $repo.Name - if (Test-Path (Join-Path $path '.git')) { - if ($PSCmdlet.ShouldProcess($path, 'Fetch and fast-forward')) { - Write-Verbose "Updating $path" - git -C $path fetch origin --quiet - if ($LASTEXITCODE -ne 0) { - throw "git fetch failed for '$path' (exit $LASTEXITCODE). Check network access and credentials for $($repo.Url)." + $path = Join-Path $Root $repo.RelativePath + if ($repo.Kind -eq 'memory') { + $memoryGitEntry = Join-Path $path '.git' + if (Test-Path $memoryGitEntry -PathType Leaf) { + throw "Memory context '$path' is a worktree, but memory requires a simple checkout with a .git directory." + } + if (-not (Test-Path $memoryGitEntry -PathType Container)) { + if (Test-Path $path) { + throw "Cannot clone memory into '$path': it exists but is not a supported simple git checkout." + } + if ($PSCmdlet.ShouldProcess($repo.Url, "Clone memory into '$path'")) { + New-Item -ItemType Directory -Path (Split-Path -Parent $path) -Force | Out-Null + git clone --quiet $repo.Url $path + if ($LASTEXITCODE -ne 0) { + throw "git clone failed for $($repo.Url) (exit $LASTEXITCODE). Check access and credentials." + } } - git -C $path pull --ff-only --quiet + } + Sync-ContextCheckout -Path $path -RepositoryUrl $repo.Url -Confirm:$false | Out-Null + Set-ContextIdentity -Path $path -Name $UserName -Email $UserEmail -Confirm:$false + [pscustomobject]@{ + Repository = $repo.Name + Path = $path + BackingPath = $null + Changes = $repo.Changes + } + continue + } + + $expectedBackingPath = "$path.git" + $backingPath = $null + $gitEntry = Join-Path $path '.git' + if (Test-Path $gitEntry -PathType Container) { + # Safe simple-clone migration: synchronize first, preserve all refs in a + # new bare backing repository, and retain the old clone as a backup. + $remote = Sync-ContextCheckout -Path $path -RepositoryUrl $repo.Url -Confirm:$false + if (Test-Path $expectedBackingPath) { + throw "Cannot migrate '$path': backing path '$expectedBackingPath' already exists." + } + $backupPath = "$path.simple-clone-backup" + if (Test-Path $backupPath) { + throw "Cannot migrate '$path': backup path '$backupPath' already exists. Reconcile it first." + } + if ($PSCmdlet.ShouldProcess($path, "Migrate simple clone to '$expectedBackingPath'")) { + $sourceRefs = @(git -C $path for-each-ref '--format=%(refname) %(objectname)' refs/heads refs/tags) if ($LASTEXITCODE -ne 0) { - Write-Warning "Could not fast-forward '$path' (local changes or diverged history). Left as-is." + throw "Could not inventory branches and tags in '$path' before migration." + } + try { + git clone --bare --quiet $path $expectedBackingPath + if ($LASTEXITCODE -ne 0) { + throw "Could not create bare backing repository '$expectedBackingPath' (exit $LASTEXITCODE)." + } + git --git-dir=$expectedBackingPath remote set-url origin $repo.Url + if ($LASTEXITCODE -ne 0) { + throw "Could not set origin on '$expectedBackingPath' (exit $LASTEXITCODE)." + } + Sync-ContextRemote -GitPath $expectedBackingPath -RepositoryUrl $repo.Url -Bare -Confirm:$false | Out-Null + $backingRefs = @(git --git-dir=$expectedBackingPath for-each-ref '--format=%(refname) %(objectname)' refs/heads refs/tags) + if ($LASTEXITCODE -ne 0 -or (Compare-Object $sourceRefs $backingRefs)) { + throw "Bare backing repository '$expectedBackingPath' did not preserve every local branch and tag." + } + } catch { + if (Test-Path $expectedBackingPath) { + Remove-Item -LiteralPath $expectedBackingPath -Recurse -Force + } + throw "Migration preparation failed for '$path'; the original clone is unchanged. $($_.Exception.Message)" } + + $moved = $false + try { + if ($env:MSX_BOOTSTRAP_TEST_FAIL_DOCS_MOVE -eq '1') { + throw 'Injected migration move failure.' + } + Move-Item -LiteralPath $path -Destination $backupPath -ErrorAction Stop + $moved = $true + if ($env:MSX_BOOTSTRAP_TEST_FAIL_AFTER_DOCS_MOVE -eq '1') { + throw 'Injected post-move migration failure.' + } + git --git-dir=$expectedBackingPath worktree add --quiet $path $remote.DefaultBranch + if ($LASTEXITCODE -ne 0) { + throw "Could not create canonical docs worktree '$path' (exit $LASTEXITCODE)." + } + Sync-ContextCheckout -Path $path -RepositoryUrl $repo.Url -Confirm:$false | Out-Null + } catch { + $activationError = $_ + $rollbackErrors = [Collections.Generic.List[string]]::new() + if ($moved -and (Test-Path $path)) { + git --git-dir=$expectedBackingPath worktree remove --force $path 2>$null + if ($LASTEXITCODE -ne 0) { + $rollbackErrors.Add("git worktree remove failed for '$path'.") + } + try { + Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction Stop + } catch { + $rollbackErrors.Add("Could not remove partial worktree '$path': $($_.Exception.Message)") + } + } + if ($moved -and -not (Test-Path $path) -and (Test-Path $backupPath)) { + try { + Move-Item -LiteralPath $backupPath -Destination $path -ErrorAction Stop + } catch { + $rollbackErrors.Add("Could not restore '$backupPath' to '$path': $($_.Exception.Message)") + } + } + if (Test-Path $expectedBackingPath) { + try { + Remove-Item -LiteralPath $expectedBackingPath -Recurse -Force -ErrorAction Stop + } catch { + $rollbackErrors.Add("Could not remove partial backing '$expectedBackingPath': $($_.Exception.Message)") + } + } + if ($rollbackErrors.Count -gt 0) { + throw "Migration activation and rollback both failed. $($rollbackErrors -join ' ') Original error: $($activationError.Exception.Message)" + } + throw "Migration activation failed for '$path'; the original clone is usable and partial backing removed. $($activationError.Exception.Message)" + } + Write-Warning "Migrated '$path' to bare+worktree layout. Verify it, then remove retained backup '$backupPath'." + } + $backingPath = $expectedBackingPath + } elseif (Test-Path $gitEntry -PathType Leaf) { + $backingPath = (git -C $path rev-parse --path-format=absolute --git-common-dir | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw "Cannot resolve the backing repository for docs worktree '$path'." + } + $isBare = (git --git-dir=$backingPath rev-parse --is-bare-repository | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or $isBare -ne 'true') { + throw "Docs worktree '$path' is not backed by a bare repository. Repair it before using context." } + } elseif (Test-Path $path) { + throw "Cannot install docs at '$path': it exists but is not a supported git checkout." } else { - if (Test-Path $path) { - throw "Cannot clone into '$path': it exists but is not a git repository. Remove it or choose a different -Root." + $backingPath = $expectedBackingPath + if (-not (Test-Path $backingPath)) { + if ($PSCmdlet.ShouldProcess($repo.Url, "Clone bare docs backing into '$backingPath'")) { + New-Item -ItemType Directory -Path (Split-Path -Parent $backingPath) -Force | Out-Null + git clone --bare --quiet $repo.Url $backingPath + if ($LASTEXITCODE -ne 0) { + throw "Bare clone failed for $($repo.Url) (exit $LASTEXITCODE)." + } + } + } + $isBare = (git --git-dir=$backingPath rev-parse --is-bare-repository | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or $isBare -ne 'true') { + throw "Docs backing path '$backingPath' is not a bare repository." } - if ($PSCmdlet.ShouldProcess($repo.Url, "Clone into '$path'")) { - Write-Verbose "Cloning $($repo.Url) into $path" - git clone --quiet $repo.Url $path + $remote = Sync-ContextRemote -GitPath $backingPath -RepositoryUrl $repo.Url -Bare -Confirm:$false + Sync-BareDefaultBranch -BackingPath $backingPath -Remote $remote -Confirm:$false + if ($PSCmdlet.ShouldProcess($path, 'Create canonical docs worktree')) { + git --git-dir=$backingPath worktree add --quiet $path $remote.DefaultBranch if ($LASTEXITCODE -ne 0) { - throw "git clone failed for $($repo.Url) (exit $LASTEXITCODE). Check access and credentials (MSXOrg/memory is private)." + throw "Could not create canonical docs worktree '$path' (exit $LASTEXITCODE)." } } } - # Isolated identity: write repository-local config only. Git still reads - # global and system config; the script never writes to them. - if ($PSCmdlet.ShouldProcess($path, 'Set repository-local git identity')) { - git -C $path config user.name $UserName - if ($LASTEXITCODE -ne 0) { throw "git config user.name failed for '$path' (exit $LASTEXITCODE)." } - git -C $path config user.email $UserEmail - if ($LASTEXITCODE -ne 0) { throw "git config user.email failed for '$path' (exit $LASTEXITCODE)." } + Sync-ContextCheckout -Path $path -RepositoryUrl $repo.Url -Confirm:$false | Out-Null + Set-ContextIdentity -Path $path -Name $UserName -Email $UserEmail -Confirm:$false + [pscustomobject]@{ + Repository = $repo.Name + Path = $path + BackingPath = $backingPath + Changes = $repo.Changes } - - [pscustomobject]@{ Repository = $repo.Name; Path = $path; Changes = $repo.Changes } } $results diff --git a/bootstrap/README.md b/bootstrap/README.md index ad93387..469d7a8 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -4,37 +4,163 @@ The single starting point for agents: a git-isolated local clone of the MSX cent ## Contents -- `Initialize-MsxWorkspace.ps1` — idempotent setup. Clones `MSXOrg/docs` and `MSXOrg/memory` under `~/.msx`, attempts to fast-forward them if present, and writes a repository-local git identity so the workspace never modifies the global git config. +- `Initialize-MsxWorkspace.ps1` — idempotent setup. Clones `MSXOrg/docs` and `MSXOrg/memory` under `~/.msx`, requires existing clones to exactly match their remote default branches, and writes a repository-local git identity so the workspace never modifies the global git config. - `AGENTS.template.md` — the user-global entry instruction. It bootstraps the workspace, then points the agent at the docs and memory. Install it once per machine (below). ## The model -- `~/.msx/docs` is **read context** — the ways of working, coding standards, and agent roles. Changes to it go through **pull requests**. -- `~/.msx/memory` is **append-only context** — durable notes and session history. Changes to it are **pushed to main**. +- `~/.msx/docs` is **read context** — the ways of working, coding standards, and agent workflow. Changes to it go through **pull requests**. +- `~/.msx/docs.git` is the bare backing repository for the readable, clean `~/.msx/docs` main worktree. +- `~/.msx/memory` is **durable context** — notes and session history governed by that repository's contribution policy. +- `~/.msx/projects//docs.git` and `docs/` provide the same bare+main-worktree model for optional project docs; `memory/` remains a simple checkout. > **Prerequisite:** `MSXOrg/memory` is a private repository — the bootstrap needs access to it (and working github.com credentials) to clone or update memory. +Before either repository is used, bootstrap fetches it and requires a clean checkout on the remote default branch at the exact remote head. A dirty, locally ahead, diverged, wrong-branch, or unreachable context repository stops bootstrap; stale context is never treated as a successful fallback. + Keeping the workspace separate and git-isolated means an agent reads the same docs and memory in every repository, and its commits there use the workspace identity rather than whatever the working repository or the global config happens to be set to. +The loaded `AGENTS.md` points to the roots; discovery happens in documentation. Start at `~/.msx/docs/src/docs/index.md`, follow Ways of Working to Workflow, infer the current stage, and read the linked procedure. Clear task language can shortcut stage selection, but no skill or instruction file owns a separate copy of the process. + ## Install (once per machine) Run the bootstrap: ```powershell -$docs = Join-Path $HOME '.msx/docs' +$workspaceRoot = if ($env:MSX_WORKSPACE_ROOT) { $env:MSX_WORKSPACE_ROOT } else { Join-Path $HOME '.msx' } +$docsUrl = if ($env:MSX_DOCS_URL) { $env:MSX_DOCS_URL } else { 'https://github.com/MSXOrg/docs.git' } +$memoryUrl = if ($env:MSX_MEMORY_URL) { $env:MSX_MEMORY_URL } else { 'https://github.com/MSXOrg/memory.git' } +$docs = Join-Path $workspaceRoot 'docs' +$docsBacking = "$docs.git" if ((Test-Path $docs) -and -not (Test-Path (Join-Path $docs '.git'))) { throw "$docs exists but is not a git repository. Remove it and re-run." } if (-not (Test-Path (Join-Path $docs '.git'))) { - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $docs) | Out-Null - git clone https://github.com/MSXOrg/docs.git $docs + if (-not (Test-Path $docsBacking)) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $docs) | Out-Null + git clone --bare $docsUrl $docsBacking + if ($LASTEXITCODE -ne 0) { + throw "Bare clone of MSXOrg/docs failed (exit $LASTEXITCODE). Check network access and credentials." + } + } + if ((git --git-dir=$docsBacking rev-parse --is-bare-repository) -ne 'true') { + throw "$docsBacking exists but is not a bare repository." + } + if ((git --git-dir=$docsBacking remote get-url origin) -ne $docsUrl) { + throw "$docsBacking origin does not match canonical $docsUrl." + } + $refspec = '+refs/heads/*:refs/remotes/origin/*' + if ($refspec -notin @(git --git-dir=$docsBacking config --get-all remote.origin.fetch)) { + git --git-dir=$docsBacking config --add remote.origin.fetch $refspec + if ($LASTEXITCODE -ne 0) { throw "Could not configure $docsBacking." } + } + git --git-dir=$docsBacking fetch origin --prune --quiet + if ($LASTEXITCODE -ne 0) { throw "Could not refresh $docsBacking. Do not use stale context." } + git --git-dir=$docsBacking remote set-head origin --auto | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Could not detect the MSXOrg/docs default branch." } + $defaultRef = (git --git-dir=$docsBacking symbolic-ref --short refs/remotes/origin/HEAD | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { throw "Could not resolve origin/HEAD in $docsBacking." } + $defaultBranch = $defaultRef -replace '^origin/', '' + $remoteHead = (git --git-dir=$docsBacking rev-parse $defaultRef | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { throw "Could not resolve $defaultRef in $docsBacking." } + $localRef = "refs/heads/$defaultBranch" + $localHead = (git --git-dir=$docsBacking rev-parse --verify $localRef 2>$null | Out-String).Trim() + if ($LASTEXITCODE -eq 128) { + git --git-dir=$docsBacking update-ref $localRef $remoteHead + } elseif ($LASTEXITCODE -ne 0) { + throw "Could not inspect $localRef in $docsBacking." + } elseif ($localHead -ne $remoteHead) { + git --git-dir=$docsBacking merge-base --is-ancestor $localHead $remoteHead + if ($LASTEXITCODE -ne 0) { throw "$localRef is ahead or diverged in $docsBacking." } + if ("branch $localRef" -in @(git --git-dir=$docsBacking worktree list --porcelain)) { + throw "$localRef is checked out elsewhere. Update that worktree first." + } + git --git-dir=$docsBacking update-ref $localRef $remoteHead $localHead + } + if ($LASTEXITCODE -ne 0 -or (git --git-dir=$docsBacking rev-parse $localRef) -ne $remoteHead) { + throw "$localRef is not exactly synchronized with $defaultRef." + } + git --git-dir=$docsBacking worktree add $docs $defaultBranch if ($LASTEXITCODE -ne 0) { - throw "git clone of MSXOrg/docs failed (exit $LASTEXITCODE). Check network access and github.com credentials, then re-run." + throw "Could not create the canonical MSXOrg/docs worktree at $docs." } +} else { + if ((git -C $docs remote get-url origin) -ne $docsUrl) { + throw "$docs origin does not match canonical $docsUrl." + } + $refspec = '+refs/heads/*:refs/remotes/origin/*' + if ($refspec -notin @(git -C $docs config --get-all remote.origin.fetch)) { + git -C $docs config --add remote.origin.fetch $refspec + if ($LASTEXITCODE -ne 0) { + throw "Could not configure remote tracking branches for MSXOrg/docs (exit $LASTEXITCODE)." + } + } + git -C $docs fetch origin --prune --quiet + if ($LASTEXITCODE -ne 0) { + throw "git fetch of MSXOrg/docs failed (exit $LASTEXITCODE). Do not use stale context." + } + git -C $docs remote set-head origin --auto | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Could not detect the MSXOrg/docs default branch." } + $defaultRef = (git -C $docs symbolic-ref --short refs/remotes/origin/HEAD | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { throw "Could not resolve origin/HEAD in $docs." } + $defaultBranch = $defaultRef -replace '^origin/', '' + $branch = (git -C $docs branch --show-current | Out-String).Trim() + if ($branch -ne $defaultBranch) { + throw "$docs is on '$branch', not '$defaultBranch'. Switch branches before using this context." + } + if (@(git -C $docs status --porcelain).Count -gt 0) { + throw "$docs has uncommitted changes. Resolve them before using this context." + } + git -C $docs merge --ff-only --quiet $defaultRef + if ($LASTEXITCODE -ne 0) { + throw "MSXOrg/docs cannot fast-forward to $defaultRef. Do not use stale context." + } + if ((git -C $docs rev-parse HEAD) -ne (git -C $docs rev-parse $defaultRef)) { + throw "$docs is not exactly synchronized with $defaultRef. Reconcile local commits before using this context." + } +} +$projects = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = $docsUrl + MemoryUrl = $memoryUrl + } +) +& (Join-Path $docs 'bootstrap/Initialize-MsxWorkspace.ps1') -Root $workspaceRoot -Project $projects +if ($LASTEXITCODE -ne 0) { + throw "MSX workspace synchronization failed. Do not read context until every repository is current." } -pwsh (Join-Path $docs 'bootstrap/Initialize-MsxWorkspace.ps1') ``` +## Add project context + +The default project is MSXOrg. A repository in another project declares additional docs and memory coordinates in its agent installation chapter and passes them to the same bootstrap: + +```powershell +$projects = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = 'https://github.com/MSXOrg/docs.git' + MemoryUrl = 'https://github.com/MSXOrg/memory.git' + } + @{ + Name = 'PSModule' + Path = 'projects/PSModule' + DocsUrl = 'https://github.com/PSModule/docs.git' + MemoryUrl = 'https://github.com/PSModule/memory.git' + } +) +& (Join-Path $docs 'bootstrap/Initialize-MsxWorkspace.ps1') -Project $projects +``` + +Each plug-in uses the same fail-closed freshness validation. `Path` is relative to `~/.msx`, so projects can choose a collision-free location without forking the bootstrap. + +Existing clean simple docs clones are migrated automatically. The original clone is retained beside the new layout as `docs.simple-clone-backup` for manual verification and removal. Existing docs worktrees backed by another bare path are reused in place. Dirty, ahead, diverged, wrong-branch, conflicting-path, or otherwise unsafe layouts stop with actionable guidance before conversion. + +Docs changes use topic worktrees created from `~/.msx/docs.git`; never branch or work inside the canonical `~/.msx/docs` main worktree. + Wire it into the tools so it runs as the first instruction: - **Claude Code** reads `CLAUDE.md`. Add an import to `~/.claude/CLAUDE.md`: diff --git a/src/docs/Agents/agent-author.md b/src/docs/Agents/agent-author.md deleted file mode 100644 index 8d3db9e..0000000 --- a/src/docs/Agents/agent-author.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Agent Author -description: Create and maintain the agent role descriptions and the per-repository pointer files that reference them. ---- - -# Agent Author - -Create and maintain the agent roles in this section, and the per-repository pointer files that reference them. Every role description is grounded in the [Ways of Working](../Ways-of-Working/index.md); every pointer file stays thin. Agent Author keeps descriptions and pointers honest — it does not encode standards into either. - -## When to use - -Create a new agent role, update an existing one, review agent quality, or refactor a bloated agent file back into a thin pointer over the docs. - -## Flow - -### 1. Gather requirements - -1. Identify the role — the single job it owns, and its boundary. -2. Identify the docs pages that govern that role; confirm they exist. -3. Identify what the role must **not** do — boundaries prevent scope creep. - -### 2. Author the description - -Write the role as a page in this section, following the shape of its siblings: front matter (`title`, `description`), a one-paragraph role and boundary, when to use, a numbered flow, operating rules, and a "Where this connects" list. - -- **Link, don't inline.** If a standard exists in the docs, link to it — never paste it in. -- **Procedural, not conversational.** Numbered imperatives, no filler. -- **Keyword-rich description.** The front-matter `description` is the discovery surface. - -### 3. Keep pointers thin - -A repository never carries a copy of a role. Its `AGENTS.md` — and the `CLAUDE.md` that imports it — point to these pages and add only repo-specific nuance and the genuinely tool-specific settings (permission scopes, model choice) that cannot be expressed as a pointer. When a new runtime is adopted, add a thin pointer; do not move process knowledge into it. See [Agentic Development](../Ways-of-Working/Agentic-Development.md). - -### 4. Validate - -1. Front-matter YAML parses cleanly. -2. Every link resolves, and the body duplicates no doc content. -3. The role is added to the navigation so its index row generates. - -## Operating rules - -1. Docs are the source of truth. If a standard is missing, propose adding it to the docs — do not embed it in an agent. -2. One agent, one job. Multiple roles mean multiple pages. -3. Update the navigation when adding or removing a role. - -## Where this connects - -- [Agentic Development](../Ways-of-Working/Agentic-Development.md) — the pointer model this maintains. -- [Documentation Model](../Ways-of-Working/Documentation-Model.md) — how these pages stay evergreen. diff --git a/src/docs/Agents/define.md b/src/docs/Agents/define.md deleted file mode 100644 index 54088b8..0000000 --- a/src/docs/Agents/define.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Define -description: Capture, refine, and plan a change into an actionable issue ready for implementation. ---- - -# Define - -Take something someone wants — a feature, a bug, an improvement, external feedback, or a production signal — and turn it into a planned, actionable issue. The output is either a single Task with its three sections populated, or a decomposed initiative with structured sub-issues. Define plans work; it does not build it. - -## When to use - -Capture a desire for change, write an issue, plan work, decompose an epic, refine a bug report, create sub-issues, structure a feature request, or turn feedback into a task. - -## Input - -A description of a desired change, a feedback issue from a non-contributor (treated as input, never modified), a platform signal (error, failed run, alert), or an existing issue to refine. - -## Flow - -### 1. Capture - -Turn the input into an issue with Section 1 (context and request). - -1. Search for duplicates first — propose consolidation rather than creating a new issue. -2. Frame from the user's perspective per [Issue Format](../Ways-of-Working/Issues/Process/Format.md). -3. Acceptance criteria must be user-observable and testable. - -### 2. Refine - -Ground the issue so anyone reading it agrees on what "done" means, up to the [Definition of Ready](../Ways-of-Working/Definition-of-Ready-and-Done.md). - -1. Pain before solution — push back on implementation-framed requests. -2. Make assumptions explicit. -3. Acceptance criteria answerable yes or no by a non-author. -4. Ask one question at a time when interactive. - -### 3. Plan - -Decide how the work will happen and record the decisions. - -1. **Task** — one deliverable, one pull request. Populate the technical decisions and the implementation plan per [Issue Format](../Ways-of-Working/Issues/Process/Format.md). -2. **Larger work** — decompose into child issues per [Issue Hierarchy](../Ways-of-Working/Issues/Types/Hierarchy.md). -3. Find the minimum viable path — spike, then proof of concept, then minimum viable product, then improve. -4. Record decisions with their rationale and the alternatives considered. -5. Resolve open questions before finishing; defer anything that does not block this slice to a follow-up issue. - -## Operating rules - -1. Tone is impersonal. The issue description is the source of truth; comments record what changed. -2. External references are hyperlinks. -3. Do not modify a feedback issue from a non-contributor — create an internal issue and cross-link. -4. Stop when the issue is plannable. Do not build, branch, or open pull requests — that is [Implement](implement.md). - -## Where this connects - -- [Workflow](../Ways-of-Working/Workflow.md) — the loop this opens. -- [Issue Format](../Ways-of-Working/Issues/Process/Format.md) and [Issue Hierarchy](../Ways-of-Working/Issues/Types/Hierarchy.md) — issue structure and levels. -- [Definition of Ready and Done](../Ways-of-Working/Definition-of-Ready-and-Done.md) — the readiness bar this aims for. diff --git a/src/docs/Agents/implement.md b/src/docs/Agents/implement.md deleted file mode 100644 index 32c1381..0000000 --- a/src/docs/Agents/implement.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Implement -description: Take a planned issue and deliver it as a review-ready pull request — branch, build, self-review, and finalize. ---- - -# Implement - -Take a planned issue and deliver working software in a review-ready pull request. Owns the full delivery loop: branching, coding, committing, opening the pull request, tracking progress, running the automated review loop, responding to feedback, and finalizing the release note. Implement builds; it does not plan from scratch or review others' work. - -## When to use - -Implement an issue, build a feature, fix a bug, create a branch, open a pull request, respond to review feedback, or finalize a pull request. Given an initiative rather than a task, pick the next unfinished sub-issue. - -## Input - -A Task issue number or URL with its three sections populated. - -## Flow - -### 1. Orient - -1. Read the issue fully — all three sections per [Issue Format](../Ways-of-Working/Issues/Process/Format.md). -2. Read the repository README first per [README-Driven Context](../Ways-of-Working/Readme-Driven-Context.md). -3. Identify the stack and load the relevant [Coding Standards](../Coding-Standards/index.md). Repo-local linter config wins where it disagrees with a published standard. - -### 2. Branch and draft pull request - -Use [git worktrees](../Ways-of-Working/Git-Worktrees.md) for every issue. - -1. Create a worktree from the default branch per [Branching and Merging](../Ways-of-Working/Branching-and-Merging.md). -2. Push an initial commit and immediately open a **draft** pull request so CI attaches from the first push. -3. Link the issue with a closing keyword, and assign the pull request. - -### 3. Build - -For each task in the plan: - -1. Implement the change and self-review the staged diff. -2. Commit per [Commit Conventions](../Ways-of-Working/Commit-Conventions.md) — one logical change per commit. -3. Update the issue as each task completes — do not batch. -4. Push regularly so CI runs against current work. - -When the plan is wrong, stop and document the conflict in a comment, then update the plan before resuming. Out-of-scope problems go to [Define](define.md). - -### 4. Self-review and respond - -1. Run the [Copilot review loop](../Ways-of-Working/Contribution-Workflow.md#the-copilot-review-loop) until it reports a clean round. -2. Triage each thread and CI failure per [Review Etiquette](../Ways-of-Working/Review-Etiquette.md): fix in scope and propagate the same fix elsewhere; file a follow-up for out-of-scope; reply, then resolve. - -### 5. Finalize and hand off - -When the change meets the [Definition of Ready for Review](../Ways-of-Working/Definition-of-Ready-and-Done.md): - -1. Finalize the title, release-note description, and label per [PR Format](../Ways-of-Working/PR-Format.md). -2. Mark the pull request ready and enable auto-merge per [Branching and Merging](../Ways-of-Working/Branching-and-Merging.md). - -## Operating rules - -1. Micro-commits, one logical change each, with descriptive messages. -2. Progress is visible — issues updated as tasks complete, not in bulk. -3. Draft pull request from the start; stay in the issue's scope. -4. Mark ready only when the change meets the Definition of Ready for Review — never with open tasks. -5. No planning from scratch (that is [Define](define.md)); no reviewing others' pull requests (that is [Reviewer](reviewer.md)). - -## Where this connects - -- [Contribution Workflow](../Ways-of-Working/Contribution-Workflow.md) — the draft-first loop this runs. -- [Definition of Ready and Done](../Ways-of-Working/Definition-of-Ready-and-Done.md) — the gate this hands off at. -- [PR Format](../Ways-of-Working/PR-Format.md) and [Branching and Merging](../Ways-of-Working/Branching-and-Merging.md) — packaging and landing. diff --git a/src/docs/Agents/index.md b/src/docs/Agents/index.md deleted file mode 100644 index 0d0efa6..0000000 --- a/src/docs/Agents/index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Agents -description: The roles agents play across the ecosystem — authored once as documentation and pointed to from each repository. ---- - -# Agents - -The roles agents play across the MSX ecosystem, authored once as documentation. Each page describes one role — its job, when to use it, and the steps it follows — grounded in the [Ways of Working](../Ways-of-Working/index.md) rather than restating them. - -These descriptions are the **single source for agent behaviour**. A repository does not carry its own copy; its `AGENTS.md` and `CLAUDE.md` are thin pointers to these pages ([Agentic Development](../Ways-of-Working/Agentic-Development.md)). Humans read the same pages a new teammate would. - -The lifecycle runs **Define → Implement → Review**: capture and plan the work, build it in a pull request, then review it. Two supporting roles — Security Reviewer and Agent Author — run alongside. - -## Contents - - - -| Page | Description | -| --- | --- | -| [Define](define.md) | Capture, refine, and plan a change into an actionable issue ready for implementation. | -| [Implement](implement.md) | Take a planned issue and deliver it as a review-ready pull request — branch, build, self-review, and finalize. | -| [Reviewer](reviewer.md) | Review someone else's pull request for delivery, taste, security, and undiscussed decisions. | -| [Security Reviewer](security-reviewer.md) | A structured, defensive security review that reports vulnerabilities as an actionable responsible-disclosure issue. | -| [Agent Author](agent-author.md) | Create and maintain the agent role descriptions and the per-repository pointer files that reference them. | - - diff --git a/src/docs/Agents/reviewer.md b/src/docs/Agents/reviewer.md deleted file mode 100644 index 7090a58..0000000 --- a/src/docs/Agents/reviewer.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Reviewer -description: Review someone else's pull request for delivery, taste, security, and undiscussed decisions. ---- - -# Reviewer - -Look at a pull request as the person on the other side of the contribution. Verify the work delivers the linked issue, applies good taste, respects security, and does not quietly introduce decisions that were not discussed. The Reviewer comments and approves; it does not change code, fix CI, or merge. - -## When to use - -Review a pull request, check a change for delivery, verify acceptance criteria, or assess taste and standards. For a security-focused pass, use [Security Reviewer](security-reviewer.md). - -## Input - -A pull request number or URL. If the pull request was authored by the reviewing identity, switch to self-review: write findings to the terminal, and do not post public comments (an author cannot review their own pull request). - -## Flow - -### 1. Read the issue - -The three sections are the contract. The pull request should deliver the acceptance criteria using the recorded approach. Note any gap. - -### 2. Read the README - -Some "missing" things are by design per the documented scope ([README-Driven Context](../Ways-of-Working/Readme-Driven-Context.md)). - -### 3. Assess the diff - -Check each dimension per [Review Etiquette](../Ways-of-Working/Review-Etiquette.md): - -- **Delivery** — does the diff meet the acceptance criteria, without scope it did not ask for? -- **Taste** — readability, naming, structure, tests that exercise behaviour. -- **Security** — input validation, no secrets in logs, SHA-pinned actions, least privilege. Escalate a deep pass to [Security Reviewer](security-reviewer.md). -- **Documentation** — updated where user-facing behaviour changed. -- **Tests** — new behaviour has tests; bugs get regression tests. - -### 4. Post the review - -Use severity prefixes per [Review Etiquette](../Ways-of-Working/Review-Etiquette.md): `Blocking:`, `Question:`, `Suggestion:`, `Nit:`. Out-of-scope suggestions become new issues, not blocking comments. - -### 5. Approve - -An approval co-signs the change, so approve once the blocking concerns are resolved. The approving identity must not be the author and must not be the built-in Actions identity — approvals come from a separate reviewer identity per [Branching and Merging](../Ways-of-Working/Branching-and-Merging.md). With auto-merge enabled, the approval is what lands the change. - -## Operating rules - -1. Read the issue first; review against it. -2. Stay in the pull request's scope — beyond-issue suggestions are filed as new issues. -3. One concern per comment. -4. Apply repo standards; linter rules win where a standard is silent. -5. Security is non-negotiable. -6. No code changes, no CI fixing, and no merging from the Reviewer. - -## Where this connects - -- [Review Etiquette](../Ways-of-Working/Review-Etiquette.md) — tone, severity, and how to disagree well. -- [Branching and Merging](../Ways-of-Working/Branching-and-Merging.md) — who approves and how a change lands. -- [Security Reviewer](security-reviewer.md) — the dedicated security pass. diff --git a/src/docs/Capabilities/downstream-release-propagation/design.md b/src/docs/Capabilities/downstream-release-propagation/design.md index 6e8af4f..2245f48 100644 --- a/src/docs/Capabilities/downstream-release-propagation/design.md +++ b/src/docs/Capabilities/downstream-release-propagation/design.md @@ -9,7 +9,9 @@ The notification runs in the **producer** when a release is cut. It resolves the release coordinates, builds a self-contained prompt per dependent, and delegates the change to a cloud agent **in the dependent** via the [Agent Tasks API](https://docs.github.com/rest/agent-tasks/agent-tasks). The -brief travels entirely in the prompt; the agent opens the pull request directly. +brief travels entirely in the prompt. The agent first creates or reuses the +dependent's Task delivery issue, then opens the pull request with that Task as +its one closing reference. ```mermaid flowchart TD @@ -17,7 +19,8 @@ flowchart TD notify --> resolve["Resolve version + immutable ref (SHA / digest) + notes"] resolve --> fan{"For each dependent"} fan --> delegate["Create agent task in dependent
self-contained prompt with full context"] - delegate --> pr["Agent opens PR: bump + related fixes + impact"] + delegate --> issue["Create or reuse Task delivery issue"] + issue --> pr["Agent opens closing PR: bump + related fixes + impact"] pr --> review["Human review + merge"] ``` @@ -71,11 +74,12 @@ a single configured `notify_repo` (published-artifact shape), with ## Agent instructions -The agent is told to: **apply the bump** (every matching reference, bringing any +The agent is told to: **create or reuse one Task delivery issue** for the +dependent's slice; **apply the bump** (every matching reference, bringing any mutable-tag pins into SHA-pinned compliance); **apply the related changes it can make safely**; **call out** larger or riskier work under a follow-up section rather than forcing it into the bump; **summarise impact** in the PR body; and -**open the pull request**. +**open the pull request** with exactly that Task as its closing issue. ## Permissions and credentials @@ -107,3 +111,4 @@ and a release it publishes cannot trigger a `release:` workflow. So the job: - [Release Management](../release-management/design.md) — produces the release and note this consumes. - [GitHub Actions](../../Coding-Standards/GitHub-Actions.md) — SHA pinning, least-privilege permissions, explicit secret passing. - [Security](../../Coding-Standards/Security.md#supply-chain) — the supply-chain rationale for immutable references. +- [PR Format](../../Ways-of-Working/PR-Format.md) — the delivery-leaf closure contract used by the agent. diff --git a/src/docs/Capabilities/downstream-release-propagation/spec.md b/src/docs/Capabilities/downstream-release-propagation/spec.md index ca1d351..dee44d8 100644 --- a/src/docs/Capabilities/downstream-release-propagation/spec.md +++ b/src/docs/Capabilities/downstream-release-propagation/spec.md @@ -37,13 +37,13 @@ Two shapes occur; both are the same mechanism with a different artifact: - **Automatic on stable release.** A stable producer release MUST trigger propagation to every declared dependent. Prereleases MUST NOT propagate. - **Full context, not just a number.** Each dependent receives the new version, the immutable reference (commit SHA or image digest), the release notes, and any related-change context the update implies. -- **A PR per dependent, opened by an agent.** The mechanical work — the bump plus the fixes that make it work — is delegated to a cloud agent *in the dependent*, which opens the pull request directly. No tracking issue is created. +- **A Task and a PR per dependent, opened by an agent.** The mechanical work — the bump plus the fixes that make it work — is delegated to a cloud agent *in the dependent*. The agent creates or reuses one Task delivery leaf and opens a pull request that closes exactly that Task. - **Humans decide.** A human reviews and merges each PR; the agent applies what it can safely do now and calls out larger or riskier work as follow-up. - **Backfill on demand.** Propagation MUST be re-runnable for a specific release — for a missed event, or a dependent added after the release. ## Success criteria -- A stable release yields one PR in each declared dependent, carrying the immutable reference and an impact summary, with no manual tracking. +- A stable release yields one Task and one closing PR in each declared dependent, carrying the immutable reference and an impact summary without manual coordination. - A prerelease yields none. - A dependent added after a release can be back-filled without cutting a new release. @@ -52,3 +52,4 @@ Two shapes occur; both are the same mechanism with a different artifact: - [Design](design.md) — how these requirements are delivered. - [Release Management](../release-management/spec.md) — the release this propagates. - [Dependency Updates](../dependency-updates/spec.md) — the inbound counterpart, for external dependencies. +- [Issue Hierarchy](../../Ways-of-Working/Issues/Types/Hierarchy.md) and [PR Format](../../Ways-of-Working/PR-Format.md) — the delivery leaf and closure rules this automation follows. diff --git a/src/docs/Frameworks/Agentic-Development/design.md b/src/docs/Frameworks/Agentic-Development/design.md index c1551ed..25e8568 100644 --- a/src/docs/Frameworks/Agentic-Development/design.md +++ b/src/docs/Frameworks/Agentic-Development/design.md @@ -37,7 +37,7 @@ The `docs` repository is the canonical knowledge base. It owns: - coding standards and documentation standards; - framework and capability specs and designs; - project glossary and onboarding; -- agent role descriptions and integration guidance. +- the canonical Workflow and its linked stage procedures. Changes to `docs` happen through pull requests because this repository defines durable project intent. @@ -47,7 +47,7 @@ The `memory` repository is the durable working-memory store. It owns: - recurring gotchas and lessons learned; - active project context that should survive a single chat session; -- agent role working knowledge; +- workflow-stage working knowledge; - issue, PR, and incident notes worth reusing; - project-specific preferences that are factual rather than private user preference. @@ -69,7 +69,7 @@ Product repositories carry local context and thin pointers: docs/ ``` -The repository owns only repository-specific nuance: build commands, architecture notes, local exceptions, and path-scoped rules. Cross-cutting standards remain in `docs`; reusable lessons remain in `memory`. +The repository owns only repository-specific nuance: bootstrap entry points, build commands, contribution mechanics, architecture notes, local exceptions, and path-scoped rules. Cross-cutting standards remain in `docs`; reusable lessons remain in `memory`. Thin means "no duplicated reusable process," not "discard the local operating contract." ## OKF page model @@ -108,6 +108,8 @@ Indexes are the navigation layer. An agent starts at the root index, reads descr docs/ index.md Ways-of-Working/index.md + Ways-of-Working/Workflow.md + Ways-of-Working/Workflow-Stages/index.md Coding-Standards/index.md Frameworks/index.md Frameworks/Agentic-Development/index.md @@ -127,29 +129,26 @@ Every index describes what sits below it. Generated indexes are preferred where flowchart TD start["Agent receives task"] --> policy["System and client policy"] policy --> user["User-global preferences"] - user --> locate["Detect host, org, and repo"] + user --> pointer["Read AGENTS.md pointer"] + pointer --> locate["Resolve host, org, docs, and memory roots"] locate --> host{"Which project scope?"} host -->|"dnb.ghe.com / AI-Platform"| aip["AI-Platform context"] - host -->|"github.com / MSXOrg"| msx["MSXOrg context"] - host -->|"github.com / PSModule"| psmodule["PSModule context"] - - aip --> aipdocs["Read AI-Platform/docs index"] - aipdocs --> aipmemory["Read AI-Platform/memory index"] - - msx --> msxdocs["Read MSXOrg/docs index"] - msxdocs --> msxmemory["Read MSXOrg/memory index"] - - psmodule --> psdocs["Read PSModule/docs index"] - psdocs --> psmemory["Read PSModule/memory index"] - - aipmemory --> repo["Read repository pointer files"] - msxmemory --> repo - psmemory --> repo - - repo --> path["Apply path-specific instructions"] + host -->|"github.com/MSXOrg"| msx["MSXOrg context"] + host -->|"github.com/PSModule"| psmodule["PSModule context"] + + aip --> refresh["Refresh selected docs + memory
stop unless exactly synchronized"] + msx --> refresh + psmodule --> refresh + refresh --> docs["Read selected docs index"] + docs --> workflow["Follow indexes to Workflow"] + + workflow --> stage["Infer current stage
read canonical procedure"] + stage --> memory["Read organization memory index"] + memory --> repo["Read README and repository docs"] + repo --> path["Apply path-specific local rules"] path --> task["Read issue, PR, branch, diff, diagnostics, and open files"] - task --> act["Plan and act with the selected project personality"] + task --> act["Act and follow stage handoffs"] act --> newpath{"New file path touched?"} newpath -->|"Yes"| path @@ -160,7 +159,7 @@ Resolution is deterministic. If the active repository remote is `github.com/PSMo ## Pointer files -`AGENTS.md` is the cross-runtime pointer file. It identifies the project, names the canonical docs and memory roots, and lists local nuance. +`AGENTS.md` is the cross-runtime pointer file. It identifies the project, names the canonical docs and memory root indexes, and lists local nuance. It points to the discovery trail, not to a stage-specific tool file. ```markdown # Agent Instructions @@ -175,15 +174,18 @@ Canonical project context: Before changing files: 1. Segment the work by host, organization, repository, path, and task. -2. Read this repository's README and local docs (front door first). -3. Read the relevant index in the resolved project docs repository. -4. Read relevant project memory for the resolved organization. -5. Apply path-specific instructions for the files being changed. +2. Refresh every canonical context repository and stop unless each exactly matches its remote default branch. +3. Start at the resolved project `docs/index.md`. +4. Follow the Ways of Working index to Workflow, infer the current stage, and read that stage procedure. +5. Read the relevant project standards, repository README and local docs, and organization memory. +6. Apply path-specific local rules for the files being changed. This file points; it does not define process knowledge. ``` -> **Discovery order vs. conflict precedence** — agents read local repository context first so they understand what a repository is and does before consulting cross-cutting standards. That discovery order does not invert authority: organization standards remain authoritative for cross-org practices. Local pointer files and `docs/` add repo-specific nuance and narrow exceptions; they never silently override an organization standard unless the standard explicitly permits a local exception. +The index trail is the default. A clear prompt can shortcut stage discovery: `Review this PR ` enters Review, `Make this issue ` enters Define, and `Implement ` enters Implement. These phrases are routing hints interpreted by [Workflow](../../Ways-of-Working/Workflow.md#find-the-current-stage), not commands with independent procedures. + +> **Discovery order vs. conflict precedence** — the pointer resolves organization context first, then the agent uses the stage procedure to select relevant organization and repository pages. Local pointer files and repository docs add nuance and narrow exceptions; they never silently override an organization standard unless the standard explicitly permits a local exception. `CLAUDE.md` stays a thin import: @@ -196,10 +198,10 @@ This file points; it does not define process knowledge. ```markdown Follow `AGENTS.md`. -Segment the work by host, organization, repository, path, and task before loading project standards or memory. Resolve organization docs and memory before editing. Use path-specific instruction files when their `applyTo` pattern matches a file being read, generated, reviewed, or edited. +Segment the work by host, organization, repository, path, and task. Refresh the resolved canonical context repositories and stop on any synchronization failure. Only then start at the organization docs root index and follow its Workflow. Use path-specific instruction files only for local path rules when their `applyTo` pattern matches a file being read, generated, reviewed, or edited. ``` -Path-specific instruction files are reserved for local rules that cannot live centrally because they apply only to a repository path. +Path-specific instruction files are reserved for local rules that cannot live centrally because they apply only to a repository path. They never define workflow stages. ## Local workspace @@ -207,18 +209,19 @@ A local bootstrap makes central context predictable: ```text ~/.msx/ - AI-Platform/ - docs/ - memory/ - MSXOrg/ - docs/ - memory/ - PSModule/ - docs/ - memory/ + docs.git/ # MSXOrg/docs bare backing repository + docs/ # clean MSXOrg/docs main worktree + memory/ # simple MSXOrg/memory checkout + projects/ + PSModule/ + docs.git/ # optional project docs backing repository + docs/ # optional project docs main worktree + memory/ # optional project memory checkout ``` -The bootstrap clones missing repositories and fast-forwards existing clones when possible. It writes repository-local git configuration only. If a context repository cannot update, the agent uses the local copy and reports that it may be stale. +The bootstrap clones missing repositories and fetches every existing context repository before use. Each clone must be clean, checked out on the remote default branch, and exactly equal to the fetched remote head. A dirty, locally ahead, diverged, wrong-branch, or unreachable clone stops context resolution; the agent does not use a possibly stale local copy. Bootstrap writes repository-local git configuration only. + +MSXOrg is the default project. Additional projects plug in a name, relative workspace path, docs URL, and memory URL. For example, PSModule can use `projects/PSModule/{docs,memory}` beneath the same workspace while reusing the identical synchronization and validation path. Repository agent files retain this small coordinate block because it is required before project documentation can be reached; the reusable bootstrap behavior remains central. ## Memory writing rules @@ -238,10 +241,10 @@ Different clients load different files, but the framework keeps the same depende | Client | Adapter | Behavior | | --- | --- | --- | -| Cross-client agents | `AGENTS.md` | Read the shared project pointer and local nuance. | +| Cross-client agents | `AGENTS.md` | Resolve and synchronize the shared docs and memory roots, then traverse indexes to Workflow and the current stage. | | Claude Code | `CLAUDE.md` | Import `AGENTS.md`; add no duplicated process knowledge. | -| GitHub Copilot in VS Code | `.github/copilot-instructions.md` and `.github/instructions/*.instructions.md` | Read project pointers, then apply path-specific instructions when files match. | -| Copilot coding agent | `AGENTS.md`, `.github/copilot-instructions.md`, setup workflow | Prepare the local context before implementation and follow the same project roots. | +| GitHub Copilot in VS Code | `.github/copilot-instructions.md` and `.github/instructions/*.instructions.md` | Follow `AGENTS.md`, including its freshness gate, then apply path-specific local rules when files match. | +| Copilot coding agent | `AGENTS.md`, `.github/copilot-instructions.md`, setup workflow | Synchronize the local roots, traverse the canonical indexes, and follow the resolved stage procedure. | | Copilot code review | Base-branch instructions | Review using trusted base-branch instructions rather than instructions changed by the PR under review. | ## Failure modes @@ -249,8 +252,9 @@ Different clients load different files, but the framework keeps the same depende | Failure | Design response | | --- | --- | | Repository does not identify its organization context | Infer from remote URL; ask when ambiguous. | -| Docs or memory clone is missing | Bootstrap it before work; if unavailable, continue only with explicit warning. | +| A docs or memory clone is missing or cannot synchronize | Bootstrap or repair it, then retry. Stop context resolution until every canonical context repository passes the freshness gate. | | Pointer file duplicates central standards | Replace duplicated content with links during review. | +| A skill, command, named agent, or instruction file defines a workflow stage | Delete the duplicate procedure and link to Workflow or its stage page. | | Memory conflicts with docs | Docs win; memory is corrected or removed. | | Two organizations are open in one workspace | Select by active repository; ask before cross-project changes. | | A client ignores one pointer format | Add a thin adapter for that client that points to the same canonical roots. | @@ -260,10 +264,10 @@ Different clients load different files, but the framework keeps the same depende 1. Create or identify the organization `docs` repository. 2. Create or identify the organization `memory` repository, using the [Memory Repository Template](memory-template.md) as the starting scaffold. 3. Add `docs/index.md` and `memory/index.md` as the two root maps. -4. Add framework docs, standards, and agent role descriptions to `docs`. +4. Add the canonical Workflow and linked stage procedures to `docs`. 5. Add starter memory sections to `memory`. 6. Add thin pointer files to each product repository. -7. Add a bootstrap that keeps local docs and memory clones present. +7. Add a bootstrap that keeps local docs and memory clones present and exactly synchronized before use. 8. Review new work for pointer discipline: facts live once, links point to them. ## Where this connects diff --git a/src/docs/Frameworks/Agentic-Development/index.md b/src/docs/Frameworks/Agentic-Development/index.md index 2a98c2b..20849ac 100644 --- a/src/docs/Frameworks/Agentic-Development/index.md +++ b/src/docs/Frameworks/Agentic-Development/index.md @@ -13,7 +13,7 @@ A repository adopts the framework by carrying thin agent pointer files and by le | Page | Description | | --- | --- | -| [Spec](spec.md) | Requirements for the agentic development framework — org-scoped documentation, memory, and pointer files that make agents behave correctly per project. | +| [Spec](spec.md) | Requirements for refresh-first, index-first agentic development through canonical documentation, memory, and thin pointers. | | [Design](design.md) | How the agentic development framework is built — OKF documentation, org memory, thin repo pointers, and deterministic context resolution. | | [Memory Repository Template](memory-template.md) | The concrete, copy-pasteable scaffold every organization's memory repository instantiates, and why it deliberately breaks from the Repository Standard. | diff --git a/src/docs/Frameworks/Agentic-Development/memory-template.md b/src/docs/Frameworks/Agentic-Development/memory-template.md index ec45094..c3cc042 100644 --- a/src/docs/Frameworks/Agentic-Development/memory-template.md +++ b/src/docs/Frameworks/Agentic-Development/memory-template.md @@ -27,7 +27,7 @@ memory/ ├── knowledge/ # durable facts about the ecosystem, tools, cross-repo relationships │ ├── index.md │ └── repos/ # one file per repo worth remembering repo-specific facts about (created lazily as needed) -└── agents/ # per-agent-role working knowledge; empty stub until agent roles are formally defined +└── agents/ # per-workflow-stage knowledge; empty stub until stage-specific lessons exist └── index.md ``` @@ -43,7 +43,7 @@ top-level folder is one of those responsibilities made concrete: | --- | --- | | `gotchas/` | Recurring gotchas and lessons learned. | | `knowledge/` | Active project context that should survive a single chat session, project-specific preferences that are factual rather than private user preference, and issue/PR/incident notes worth reusing. | -| `agents/` | Agent role working knowledge. | +| `agents/` | Agent workflow-stage working knowledge. | `index.md` is the root map described in [Design's indexes section](design.md#indexes-as-the-mindmap): it links to `gotchas/index.md`, `knowledge/index.md`, and `agents/index.md` so a human or diff --git a/src/docs/Frameworks/Agentic-Development/spec.md b/src/docs/Frameworks/Agentic-Development/spec.md index 1c2eb47..d79c1f4 100644 --- a/src/docs/Frameworks/Agentic-Development/spec.md +++ b/src/docs/Frameworks/Agentic-Development/spec.md @@ -1,6 +1,6 @@ --- title: Spec -description: Requirements for the agentic development framework — org-scoped documentation, memory, and pointer files that make agents behave correctly per project. +description: Requirements for refresh-first, index-first agentic development through canonical documentation, memory, and thin pointers. --- # Agentic Development — Spec @@ -12,7 +12,7 @@ An agent does useful work only when it knows which project it is serving, which Each organization owns two canonical repositories: - `docs` — the reviewed knowledge base: vision, standards, workflows, specs, designs, glossary, onboarding, and project-wide rules. -- `memory` — the durable agent working memory: lessons learned, recurring gotchas, active context, agent role knowledge, and project-specific operating notes. +- `memory` — the durable agent working memory: lessons learned, recurring gotchas, active context, workflow-stage knowledge, and project-specific operating notes. Product repositories do not copy that knowledge. They carry thin pointer files that identify the organization context and direct agents to the relevant `docs` and `memory` roots before acting. @@ -33,7 +33,8 @@ Applies to any organization that wants a shared project knowledge base and memor - Organization-level `docs` and `memory` repositories. - Markdown documents with YAML frontmatter, following the [Open Knowledge Format](../../Dictionary/index.md#open-knowledge-format) model. -- Thin repository pointer files such as `AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, and path-scoped instruction files. +- Thin repository pointer files such as `AGENTS.md` and client adapters that import or follow it. +- Refresh-first, index-first discovery from canonical context repositories to the Workflow and its stage procedures. - Deterministic context resolution by host, organization, repository, path, and task. - Human-reviewed changes to canonical knowledge through pull requests. - Durable agent memory that can be shared by every person and agent working in the organization. @@ -50,13 +51,18 @@ Applies to any organization that wants a shared project knowledge base and memor - **Organization is the project boundary.** The framework MUST resolve project context from the Git host and organization before resolving repository-specific context. - **Canonical docs repository.** Each adopting organization MUST have a `docs` repository that owns the reviewed knowledge base. - **Canonical memory repository.** Each adopting organization MUST have a `memory` repository that owns durable project memory and agent working knowledge. +- **Pluggable project context.** The bootstrap MUST accept project-specific docs and memory coordinates and collision-free relative workspace paths without requiring a fork of its synchronization logic. - **OKF-style documents.** Knowledge and memory documents MUST be Markdown files with YAML frontmatter, one primary concept per page, and stable paths that act as identity. - **Small pages and indexes.** Documentation and memory SHOULD prefer small pages, each folder SHOULD have an `index.md`, and indexes MUST let a human or agent navigate inward from the root. -- **Thin pointer files.** Product repositories MUST carry pointer files that identify the organization context and link to the canonical docs and memory roots. Pointer files MUST NOT duplicate standards or process knowledge. +- **Thin pointer files.** Product repositories MUST carry pointer files that identify the organization context and link to the canonical docs and memory root indexes. They MAY retain agent-only bootstrap steps and repository-specific operating instructions needed to reach or safely change that context. They MUST NOT duplicate standards, workflow stages, or reusable process knowledge. +- **Refresh-first, index-first workflow discovery.** After every canonical context repository passes the freshness gate, a human or agent MUST be able to follow the docs root index to Ways of Working, the canonical Workflow, and the procedure for the current stage. +- **Stage resolution from work.** Agents MUST infer the current stage from the prompt and current artifacts. Explicit task language MAY shortcut to the matching stage, but the shortcut MUST resolve to the canonical documentation. +- **One process source.** Skills, commands, named agents, and tool-specific instruction files MUST NOT redefine Workflow stages. A client convenience MAY link to a stage procedure and add only runtime mechanics. - **Segmentation before loading.** Local agent files MUST instruct agents to segment work by host, organization, repository, path, and task before loading project standards or memory. -- **Client adapters.** Tool-specific files such as `CLAUDE.md`, `.github/copilot-instructions.md`, and `.github/instructions/*.instructions.md` MAY add runtime-specific loading rules, but MUST point back to the same canonical docs and memory. -- **Deterministic context resolution.** Agents MUST resolve context in layers: system and client policy, user preferences, organization docs, organization memory, repository pointers, path-specific instructions, then current task context. +- **Client adapters.** Tool-specific files such as `CLAUDE.md`, `.github/copilot-instructions.md`, and `.github/instructions/*.instructions.md` MAY add runtime-specific loading or path rules, but MUST point back to the same canonical roots and MUST NOT define workflow behavior. +- **Deterministic context resolution.** Agents MUST resolve context in layers: system and client policy, user preferences, repository pointer, context-repository freshness gate, organization docs, organization memory, repository context, path-specific rules, then current task context. - **Local-first availability.** The docs and memory repositories SHOULD be available locally in a predictable workspace so agents can read them without relying on search or web access. +- **Fresh context before use.** Every canonical context repository MUST be fetched and exactly synchronized with its remote default branch before its contents are read. Dirty, locally ahead, diverged, wrong-branch, or unreachable repositories MUST stop context resolution rather than fall back to stale content. - **Reviewed knowledge changes.** Changes to the `docs` repository MUST happen through pull requests. Changes to memory MAY be lighter-weight, but MUST remain versioned in git. - **No cross-project bleed.** An agent working in one organization MUST NOT apply another organization's standards or memory unless the current task explicitly asks for cross-organization work. - **Traceable memory.** Memory entries SHOULD identify the context they came from and SHOULD be short, factual, and linked to the relevant issue, pull request, document, or repository when one exists. @@ -68,6 +74,9 @@ Applies to any organization that wants a shared project knowledge base and memor - An agent working in `dnb.ghe.com/AI-Platform/` resolves `dnb.ghe.com/AI-Platform/docs` and `dnb.ghe.com/AI-Platform/memory` as the canonical project context. - A new product repository can adopt the framework by adding pointer files without copying standards or memory pages. - A human can start at `docs/index.md` or `memory/index.md` and navigate to the same context an agent uses. +- A human or agent can follow `docs/index.md` → Ways of Working → Workflow → the current stage procedure without knowing a file path in advance. +- A prompt such as `Review this PR ` reaches the Review procedure directly, while `Make this issue ` reaches Define, without a parallel process definition. +- A missing, dirty, locally ahead, diverged, wrong-branch, or unreachable canonical context repository stops discovery before any context index is read. - Updating a standard in `docs` changes the canonical guidance without editing every repository. - Capturing a recurring lesson in `memory` makes it available to later agents working in the same organization. @@ -77,12 +86,13 @@ The framework uses this normative resolution order: 1. **System and client policy** — non-project instructions imposed by the agent runtime. 2. **User-global preferences** — the human operator's baseline style and risk posture. -3. **Host and organization** — the project identity, derived from the current repository remote or explicit task. -4. **Organization docs** — the organization `docs` repository root index, then relevant standards, workflows, specs, and designs. -5. **Organization memory** — the organization `memory` repository root index, then relevant lessons, gotchas, and active context. -6. **Repository pointer files** — `AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, and related local instructions. -7. **Path-specific instructions** — scoped rules that apply to the files being read, generated, reviewed, or edited. -8. **Current task context** — issue, pull request, prompt, branch, diff, diagnostics, terminal output, and open files. +3. **Repository pointer** — `AGENTS.md` identifies the host, organization, canonical docs root, memory root, and repository-local nuance. +4. **Freshness gate** — fetch every canonical context repository and stop unless each clean default-branch checkout exactly matches its remote head. +5. **Organization docs** — start at `docs/index.md`, traverse to Ways of Working and Workflow, resolve the current stage, then load the relevant standards, specs, and designs. +6. **Organization memory** — start at `memory/index.md`, then load relevant lessons, gotchas, and active context. +7. **Repository context** — README, CONTRIBUTING, local docs, and narrow repository exceptions. +8. **Path-specific rules** — scoped local rules that apply to the files being read, generated, reviewed, or edited. +9. **Current task context** — issue, pull request, prompt, branch, diff, diagnostics, terminal output, and open files; use these artifacts to re-evaluate the stage after each handoff. A lower layer MAY refine a higher layer, but MUST NOT contradict it unless the higher layer explicitly allows a local exception. diff --git a/src/docs/Vision/index.md b/src/docs/Vision/index.md index f798d57..10ca19f 100644 --- a/src/docs/Vision/index.md +++ b/src/docs/Vision/index.md @@ -59,7 +59,7 @@ Vision (this site) the why — stable, evergreen └── Agents read the same docs as context before acting ``` -Each layer references the one above instead of restating it. A repository's README does not re-explain the principles — it links to them. An agent definition does not embed a style guide — it points to one. This keeps a single source of truth and lets the whole system evolve without drifting out of sync. +Each layer references the one above instead of restating it. A repository's README does not re-explain the principles — it links to them. An agent pointer does not embed a style guide or workflow stage — it leads into the documentation indexes. This keeps a single source of truth and lets the whole system evolve without drifting out of sync. ## Where it comes to life diff --git a/src/docs/Ways-of-Working/Agentic-Development.md b/src/docs/Ways-of-Working/Agentic-Development.md index 184f70d..54d74c8 100644 --- a/src/docs/Ways-of-Working/Agentic-Development.md +++ b/src/docs/Ways-of-Working/Agentic-Development.md @@ -5,7 +5,7 @@ description: How ways of working, standards, and documentation are authored once # Agentic Development -How the ecosystem's ways of working, coding standards, and documentation are authored — and how both humans and agents consume them. The documentation defines how work is done; agent configuration *references* that documentation. It never the other way around. +How the ecosystem's ways of working, coding standards, and documentation are authored — and how both humans and agents consume them. The documentation defines how work is done; agent configuration *references* that documentation, never the other way around. ## Premise @@ -24,20 +24,23 @@ This spec rests on the [Principles](Principles/index.md). Four apply directly: ## Architecture -Agent configuration files are **pointers, not containers**. They tell the agent which human-readable files to read; they hold no process knowledge of their own. Documentation lives where it belongs — repo-specific docs in each repository's `README.md`, `CONTRIBUTING.md`, and `docs/`; cross-cutting standards in the org-level documentation site, referenced by canonical URL. +Agent configuration files are **pointers, not process containers**. They identify canonical context and may retain the bootstrap steps and repository-specific operating instructions an agent needs before it can reach that context. They do not select a persona, copy workflow stages, or restate standards. Documentation lives where it belongs — repo-specific context in each repository's `README.md`, `CONTRIBUTING.md`, and `docs/`; cross-cutting guidance in the org-level documentation site. -When an agent starts work in a repository, it discovers context in layers — local first, then central, then local nuance on top: +When an agent receives work, it follows the same documentation trail a human can follow: ```mermaid flowchart TD - agent["Agent starts in a repository"] --> local["1 Read local context
README.md · CONTRIBUTING.md · docs/"] - local --> index["2 Read the central indexes
Ways of Working · Coding Standards · Capabilities"] - index --> pick["3 Pick up the relevant central docs
e.g. the Terraform coding standard"] - pick --> nuance["4 Layer local overrides
repo-specific instruction files"] - nuance --> work["Start work"] + task["Agent receives work"] --> pointer["1 Read AGENTS.md
resolve project docs + memory roots"] + pointer --> refresh["2 Refresh every context repository
stop unless exactly synchronized"] + refresh --> root["3 Read docs/index.md"] + root --> ways["4 Follow Ways of Working index"] + ways --> workflow["5 Read Workflow"] + workflow --> stage["6 Infer the current stage
read its procedure"] + stage --> context["7 Read relevant standards,
repository context, and memory"] + context --> work["Act and follow stage handoffs"] ``` -The flow is sequential, not a decision. The agent reads local docs to understand the repository, reads the central indexes to see which shared standards exist, picks up the relevant ones, and finally checks for local overrides that add repo-specific nuance. **Local files never replace central standards — they layer specifics on top.** The docs are the stable core; every integration is a thin adapter that references them. +Refresh is a gate before traversal, not a best-effort background step. After it passes, the indexes are the default discovery mechanism. [Workflow](Workflow.md) owns the process and routes the work to a [stage procedure](Workflow-Stages/index.md); the stage page then points to the standards and artifacts it consumes. A clear prompt such as `Review this PR ` may shortcut directly through the Workflow routing table, but it does not create a second process definition. **Local files never replace central standards — they layer specifics on top.** ## Where documentation lives @@ -52,39 +55,35 @@ This split follows [Repository Segmentation](Repository-Segmentation.md) and [RE ## How an agent runtime plugs in -Agent context is delivered through three layers, in priority order — the same three layers the [Principles](Principles/AI-First-Development.md#human-agent-coexistence) describe: - -1. **Documentation.** The primary source. The published docs, READMEs, and issue bodies are written for humans and read natively by agents. -2. **Central agent descriptions.** The roles agents play — Define, Implement, Reviewer, and the rest — are authored once as documentation in the [Agents](../Agents/index.md) section. They describe roles, boundaries, and procedural steps, and they reference the ways of working; they never restate a standard or convention. -3. **Local pointer files.** Each repository carries an `AGENTS.md` — read natively by most agent runtimes — and a `CLAUDE.md` that imports it, pointing to the central descriptions and adding only repo-specific nuance and the small amount of genuinely tool-specific configuration (permission scopes, path-scoped rules) that cannot be expressed as a pointer. +Each repository carries an `AGENTS.md` that points to the organization documentation and memory root indexes. A `CLAUDE.md` or other client adapter may import that pointer and add only the small amount of genuinely tool-specific configuration, such as permission scopes or path matching, that cannot live in ordinary documentation. Any new runtime follows the same pattern, regardless of vendor: -- A **context file** that links to the ways-of-working docs and the repository's own context. -- **Workflow entry points** — named commands or agents — that reference those same docs and add the operational steps (branch creation, tool invocations, API calls). +- A **context pointer** that identifies the canonical docs and memory root indexes. +- Optional **keyword shortcuts** that route a clearly stated task to the matching [Workflow stage](Workflow.md#find-the-current-stage) without copying its procedure. - **Tool-specific settings** — permissions, model selection, and the like. -The context file and the entry points are pointers; the settings are the only genuinely tool-specific surface. When a new runtime is adopted, only this integration layer is added — the documentation it points to is untouched. +There is no separate process surface for Define, Implement, or Review. If a client exposes a skill, command, named agent, or other convenience, it links to the canonical stage page and adds no process knowledge. When a new runtime is adopted, only this thin integration layer is added. ## Distribution The two non-documentation layers have different distribution models: -- **Central agent descriptions** live in the [Agents](../Agents/index.md) section of this site and are referenced by canonical URL — one definition, available to every repository and runtime with no per-repo copy to maintain. -- **Per-repository pointer files** — `AGENTS.md`, the `CLAUDE.md` that imports it, and any path-scoped instruction files — are seeded from a template repository and kept current across existing repositories by a sync mechanism. +- **The canonical process** lives in [Workflow](Workflow.md), which links to ordinary documentation pages for each [stage procedure](Workflow-Stages/index.md). +- **Per-repository pointer files** — `AGENTS.md`, the `CLAUDE.md` that imports it, and any path-scoped local-rule adapters — are seeded from a template repository and kept current across existing repositories by a sync mechanism. Process knowledge is never added to a distributed config file. If an agent needs the branch strategy, it goes in [Branching and Merging](Branching-and-Merging.md) or the repo's `CONTRIBUTING.md`; if it needs a coding convention, it goes in the relevant [coding standard](../Coding-Standards/index.md). The config file only points — it never defines. ## The workspace bootstrap -The **user-global** entry file is a thin **bootstrap**, not a copy of the docs. Each runtime auto-loads its own user-level file — Copilot from its user instructions, Claude Code from `~/.claude/CLAUDE.md` (which imports the same instructions) — and its first instruction is to make the central workspace present locally, then read from it. This is distinct from the per-repository `AGENTS.md` and `CLAUDE.md`, which remain thin pointers to the central descriptions. +The **user-global** entry file is a thin **bootstrap**, not a copy of the docs. Each runtime auto-loads its own user-level file — Copilot from its user instructions, Claude Code from `~/.claude/CLAUDE.md` (which imports the same instructions) — and its first instruction is to make the central workspace present locally, then start at the root indexes. This is distinct from the per-repository `AGENTS.md` and `CLAUDE.md`, which remain thin pointers to the same roots. The workspace is a git-isolated clone of the central repositories under `~/.msx`: - `~/.msx/docs` — this documentation, read as local files. Changes to it go through pull requests. - `~/.msx/memory` — durable notes and prior session context. Changes to it are pushed to main. -Each clone carries repository-local git config only, so the workspace never modifies the global git config or the repository the agent is working in — git still reads them, but only repository-local config is written. The setup is one idempotent script — [`bootstrap/Initialize-MsxWorkspace.ps1`](https://github.com/MSXOrg/docs/blob/main/bootstrap/Initialize-MsxWorkspace.ps1) — that clones what is missing and attempts to fast-forward the rest, leaving a repository as-is if it cannot. This keeps "start at the same point" literal: every agent, in every repository, begins from the same local docs and memory. +Each clone carries repository-local git config only, so the workspace never modifies the global git config or the repository the agent is working in — git still reads them, but only repository-local config is written. Before context is read, [`bootstrap/Initialize-MsxWorkspace.ps1`](https://github.com/MSXOrg/docs/blob/main/bootstrap/Initialize-MsxWorkspace.ps1) clones missing repositories and requires every existing context repository to be clean, on its remote default branch, and exactly synchronized with the remote head. Any update failure stops context resolution rather than allowing stale guidance or memory. The workspace makes the *central* context present locally; the same local-first stance shapes how each working repository is laid out. Repositories are cloned as [git worktrees](Git-Worktrees.md) — one working directory per branch — so a person and an agent, or several agents, can work on multiple issues in the same repository at once without stashing or switching branches. diff --git a/src/docs/Ways-of-Working/Branching-and-Merging.md b/src/docs/Ways-of-Working/Branching-and-Merging.md index 6dffd95..28eeae2 100644 --- a/src/docs/Ways-of-Working/Branching-and-Merging.md +++ b/src/docs/Ways-of-Working/Branching-and-Merging.md @@ -1,6 +1,6 @@ --- title: Branching and Merging -description: Topic branches, pull-request-only integration, and merge models. +description: Delivery-leaf topic branches, pull-request-only integration, and merge models. --- # Branching and Merging @@ -9,7 +9,7 @@ How changes move from a working branch into a protected branch. The model is sma ## Topic branches -- Work happens on short-lived branches — one per issue, each in its own [worktree](Git-Worktrees.md). A branch is cut from the default branch unless a different base is explicitly given; an agent follows the same default. +- Repository delivery happens on short-lived branches — one per Task or Bug, each in its own [worktree](Git-Worktrees.md). Epic and PBI aggregates do not own branches, and an operational Task has no branch. A branch is cut from the default branch unless a different base is explicitly given; an agent follows the same default. - Name branches `/-`, e.g. `feat/42-pagination` or `fix/99-null-context`. The type matches the change type. - Branches stay short-lived. The longer a branch lives, the further it diverges and the harder it is to merge. @@ -22,7 +22,7 @@ How changes move from a working branch into a protected branch. The model is sma ## Stacked pull requests -Use a stack when several reviewable changes depend on one another and cannot land independently. Each layer remains one Task issue, one branch, and one pull request; the stack only records their dependency and order. Independent changes use separate branches from the default branch instead. +Use a stack when several reviewable changes depend on one another and cannot land independently. Each layer remains one Task or Bug, one branch, and one pull request; the stack only records their dependency and order. Independent changes use separate branches from the default branch instead. Build the stack from its destination upward: @@ -33,9 +33,9 @@ Build the stack from its destination upward: Every layer follows the ordinary [Contribution Workflow](Contribution-Workflow.md): -1. **Plan the dependency.** Give each layer its own Task issue. Record `Blocked by: #N` on dependent issues as described in [Issue Hierarchy](Issues/Types/Hierarchy.md#how-to-express-the-hierarchy), and order the implementation so the shared foundation is the first layer. +1. **Plan the dependency.** Give each layer its own Task or Bug issue. Add a native blocked-by edge from every dependent leaf to its prerequisite as described in [Issue Relationships](Issues/Process/Relationships.md#execution-order); prose, sub-issue order, and stack position do not create the gate. 2. **Open every layer as a draft.** After the initial commit, push the branch and open its pull request immediately. Use the standard user-facing title and description from [PR Format](PR-Format.md); do not add stack position or an internal branch name to the title. -3. **Link the stack in Technical Details.** Add fully qualified pull request links for the immediate dependency and dependent, using `Depends on Owner/Repo#N` and `Followed by Owner/Repo#N`. Each pull request closes only its own Task issue. +3. **Link the stack in Technical Details.** Add fully qualified pull request links for the immediate dependency and dependent, using `Depends on Owner/Repo#N` and `Followed by Owner/Repo#N`. Each pull request closes only its own Task or Bug. 4. **Keep each delta isolated.** The pull request diff against its current base contains only that layer's change. Run its tests, checks, and automated review even when an earlier layer already exercised the combined code. 5. **Make one layer ready at a time.** Only the lowest unmerged layer is marked ready and given auto-merge. Every later layer stays draft, even when its current checks are green. 6. **Advance after merge.** When the lowest layer lands, refresh the next branch against the landed target, retarget its pull request to that target, and verify that the diff still contains only its intended change. Run CI and the automated review loop again before marking it ready. diff --git a/src/docs/Ways-of-Working/Contribution-Workflow.md b/src/docs/Ways-of-Working/Contribution-Workflow.md index 4581076..9809968 100644 --- a/src/docs/Ways-of-Working/Contribution-Workflow.md +++ b/src/docs/Ways-of-Working/Contribution-Workflow.md @@ -10,10 +10,7 @@ puts an automated Copilot review *before* human review: the author iterates with Copilot on a **draft** pull request until it has no more feedback, then opens the pull request for people. -This is the operational "how". The conventions it builds on live in the ways of -working — [Issue Format](Issues/Process/Format.md), [PR Format](PR-Format.md), -[Branching and Merging](Branching-and-Merging.md), and -[Review Etiquette](Review-Etiquette.md). +This is the operational "how" for a repository-delivery Task or Bug. The conventions it builds on live in the ways of working — [Issue Lifecycle](Issues/Process/Lifecycle.md), [PR Format](PR-Format.md), [Branching and Merging](Branching-and-Merging.md), and [Review Etiquette](Review-Etiquette.md). Epic and PBI aggregates stay in planning and coordination rather than entering this branch-and-pull-request flow. ## The flow @@ -32,9 +29,10 @@ flowchart TD H --> M[Auto-merge lands it on approval + green checks] ``` -1. **Branch and implement.** Work on a `/` branch and keep the - per-change implementation detail in the issue and the pull request, not in the - spec or design. +1. **Branch and implement.** Pull one ready, unblocked Task or Bug into work on a + `/-` branch. Keep per-change implementation detail in + that delivery issue and the pull request, not in the spec, design, or parent + aggregate. 2. **Open the pull request as a draft.** A draft attaches CI and keeps the change out of people's review queues while you iterate. 3. **Run the Copilot review loop** (below) until Copilot reports a clean round — @@ -87,8 +85,8 @@ For each piece of feedback, decide: - **In scope** — it concerns the change under review. Address it in this pull request and push; the next round re-checks it. - **Out of scope** — it points at a pre-existing gap or an adjacent improvement. - File an issue ([Issue Format](Issues/Process/Format.md)) capturing the gap and reference - it; do not grow the pull request to cover it. + Route a follow-up through the [Issue Hierarchy](Issues/Types/Hierarchy.md) and + reference it; do not grow the pull request to cover it. - **Not actionable** — a false positive, or a matter of taste you disagree with. Reply with the reason and resolve the thread; a documented dismissal counts as handled ([Review Etiquette](Review-Etiquette.md)). @@ -122,6 +120,7 @@ the approval identity that satisfies the gate. ## Where this connects - [Workflow](Workflow.md) — the spec-led loop this operates within. +- [Issue Lifecycle](Issues/Process/Lifecycle.md) — why only ready Task and Bug leaves enter this flow. - [PR Format](PR-Format.md) — the pull request title, description, and labels. - [Definition of Ready and Done](Definition-of-Ready-and-Done.md#definition-of-ready-for-review) — the readiness gate this hands off at. - [Branching and Merging](Branching-and-Merging.md) — the branch model a pull request builds on. diff --git a/src/docs/Ways-of-Working/Definition-of-Ready-and-Done.md b/src/docs/Ways-of-Working/Definition-of-Ready-and-Done.md index 285152c..a502dfa 100644 --- a/src/docs/Ways-of-Working/Definition-of-Ready-and-Done.md +++ b/src/docs/Ways-of-Working/Definition-of-Ready-and-Done.md @@ -5,20 +5,21 @@ description: The three gates that bracket every piece of work. # Definition of Ready and Done -Three gates bracket every piece of work: one gates when it can start, one gates when it is ready for other people to review, and one gates when it can be called complete. All are shared contracts, not personal preferences. +Three gates bracket work: one gates when an issue can move at its own altitude, one gates when a delivery pull request is ready for other people to review, and one gates when an issue can be called complete. All are shared contracts, not personal preferences. ## Definition of Ready -An item is ready to be pulled into work when: +Readiness is type-specific. The canonical type pages own the detailed criteria; this gate determines what may move next. -- It is at the right level — a single deliverable, not a bundle. See [Issue Hierarchy](Issues/Types/Hierarchy.md). -- The intent is clear — the problem and the desired outcome are understood, written from the user's perspective. -- It has acceptance criteria — "done" is described and testable. -- It is sized to fit comfortably within one cycle of work. -- Dependencies and blockers are known. -- Open questions are resolved. +### Aggregate readiness -Ready gates *starting*, not scope. An item that isn't ready stays in the backlog and gets refined — it is not pulled in and figured out on the fly. +An [Epic](Issues/Types/Epic.md#ready) or [PBI](Issues/Types/PBI.md#ready) is ready to decompose or coordinate when its aggregate outcome and acceptance criteria are testable, its native containment and dependency relationships are current, and no open decision prevents the first required children from becoming ready. Readiness does not send an aggregate into Build. + +### Delivery-leaf readiness + +A [Task](Issues/Types/Task.md#ready) or [Bug](Issues/Types/Bug.md#ready) is ready for Build when it owns one independently verifiable deliverable, its local acceptance criteria and implementation plan are executable, its native blockers are clear, and it fits one reviewable pull request. An operational Task is ready only when the action, durable evidence, and independent verifier are identified before execution. + +Ready gates starting, not scope. An issue that is not ready stays in refinement or planning; it is not pulled into Build and figured out on the fly. See [Issue Hierarchy](Issues/Types/Hierarchy.md) for routing and [Issue Relationships](Issues/Process/Relationships.md) for authoritative blockers. ## Definition of Ready for Review @@ -26,12 +27,11 @@ A pull request stays a **draft** until it is genuinely ready for other people to A pull request is ready for review when: -- Every task in the linked issue's checklist is complete — or explicitly moved to a follow-up issue. No half-done work is left implicit. -- Dependencies are current — any dependency update the change relies on is already merged or included; nothing waits on a bump that has not landed yet. +- It closes exactly one Task or Bug, and every item in that delivery leaf's implementation plan is complete or explicitly moved to a follow-up issue. +- Native dependencies are current and every prerequisite the change relies on has landed; the pull request is not used to bypass a blocked-by edge. - All required checks are green — not just tests that pass locally. CI is complete, not in progress. - The automated review loop has converged — a clean [Copilot round](Contribution-Workflow.md#the-copilot-review-loop) with no unresolved review threads. - The title, release-note description, and exactly one change-type label are finalized. See [PR Format](PR-Format.md). -- A linked issue exists. If any item is open, the pull request stays a draft. Marking it ready with known-open work shifts the author's unfinished job onto reviewers — the opposite of what the signal means. @@ -39,15 +39,18 @@ Once every item holds, hand the change off: mark it ready for review and enable ## Definition of Done -An item is done when: +Completion follows the issue's delivery or aggregate path. Across all paths, the issue's acceptance criteria are verified, required documentation is current, and no known regression is left implicit. + +### Repository delivery leaf + +A repository-delivery Task or Bug is done when its reviewed pull request is merged and closes that one leaf, required checks and tests pass, applicable coding standards hold, and the affected evergreen specification and documentation describe the delivered behavior. Release or deploy it where that applies. + +### Operational Task + +An operational Task is done without a pull request only after the action has durable audit evidence, an independent verifier confirms every acceptance criterion, and the Task is closed. The canonical [operational delivery path](Issues/Types/Task.md#operational-delivery) owns the required record. + +### Aggregate issue -- The acceptance criteria are met. -- The change is merged through a reviewed pull request. -- It conforms to the [Coding Standards](../Coding-Standards/index.md); linters and checks are green. -- Tests cover the behavior and pass. -- The [evergreen specification](Documentation-Model.md#evergreen-and-evolutionary) for the affected capability describes the new behaviour — the spec leads, the code matches it. -- Documentation is updated in the same change — both owning-team docs (README, in-code help, and this site) and user-facing documentation. -- It is released or deployed, where that applies. -- No known regressions remain, and the issue is closed. +A PBI or Epic is done only when every required native child is complete and its own aggregate acceptance criteria and evidence are verified. Close the aggregate directly after that verification; a delivery pull request must not close it. -Done is binary. Skip a criterion only when it genuinely does not apply — a docs-only change has no deploy step. If "done" repeatedly needs exceptions, fix the definition rather than quietly lowering the bar. +Done is binary. Skip a criterion only when it genuinely does not apply. If "done" repeatedly needs exceptions, fix the definition rather than quietly lowering the bar. diff --git a/src/docs/Ways-of-Working/Documentation-Model.md b/src/docs/Ways-of-Working/Documentation-Model.md index 8eeff33..217ec03 100644 --- a/src/docs/Ways-of-Working/Documentation-Model.md +++ b/src/docs/Ways-of-Working/Documentation-Model.md @@ -27,8 +27,8 @@ mechanism, the moving parts, the configuration. Both are durable, and both evolve — neither is a one-time plan. Only the detail of a *single change* — the paths touched, the trade-off taken -this once — stays out of both, living in the [issue](Issues/Process/Format.md) and the -pull request where it belongs. +this once — stays out of both, living at the delivery leaf's canonical +[issue planning altitude](Issues/Process/Planning.md) and in its pull request. ## Capabilities live in folders @@ -57,7 +57,7 @@ two documents live with the code. | **How / what** we build to deliver it | the capability's **design** | | **How we work** — process, principles, conventions | [Ways of Working](index.md) | | **How code looks** — style applied to code | [Coding Standards](../Coding-Standards/index.md) | -| **How this one change is implemented** — paths, trade-offs | the [issue](Issues/Process/Format.md) and the PR | +| **How this one change is implemented** — paths, trade-offs | the Task or Bug delivery leaf and its PR; see [Issue Planning](Issues/Process/Planning.md) | Keeping implementation out of the spec is what makes the spec durable: implementation detail rots fastest, so the spec leaves it to the design, and the @@ -72,8 +72,8 @@ then code — and loops: 2. **Spec** — agree the next version's requirements: why it matters and what it must do. Nothing is committed to building yet. 3. **Design** — once committed to deliver, describe how and what we will build. -4. **Build** — implement, evolving the design *and* the spec as development - teaches you things. +4. **Build** — ready Task and Bug leaves implement the gap, evolving the design + *and* the spec as development teaches you things. 5. **Operate** — running the system surfaces new needs, and the loop returns. ```mermaid diff --git a/src/docs/Ways-of-Working/Fleet-Orchestration.md b/src/docs/Ways-of-Working/Fleet-Orchestration.md index a7bff83..c6c0f37 100644 --- a/src/docs/Ways-of-Working/Fleet-Orchestration.md +++ b/src/docs/Ways-of-Working/Fleet-Orchestration.md @@ -1,13 +1,14 @@ --- title: Fleet Orchestration -description: How one change is rolled out across many repositories — a campaign of branches, pull requests, and review loops tracked entirely on GitHub. +description: How one change is rolled out across many repositories through Task or Bug delivery leaves tracked entirely on GitHub. --- # Fleet Orchestration How a single change is applied across many repositories at once — a *campaign*. -Each repository gets its own branch, pull request, and review loop; the campaign -is the coordination layer that keeps them moving and visible. +Each repository gets its own Task or Bug delivery leaf, branch, pull request, +and review loop; the campaign is the coordination layer that keeps them moving +and visible. This is the multi-repository "how". Inside each repository the change follows the ordinary [Contribution Workflow](Contribution-Workflow.md) — draft first, the @@ -30,18 +31,16 @@ tool, can read and drive a campaign with the GitHub CLI alone. ## The campaign A campaign is one change rolled out across a set of repositories. Each -repository's slice of the change is a **work item**: a tracking issue, a pull -request, or both. A campaign has a short, stable **slug** (for example -`process-psmodule-v6`) that names it everywhere. - -A work item is usually created for the campaign, but an **existing open pull -request can be adopted** as one. When a repository already has a pull request -that does part of the change, add the remaining change to that branch and prefix -its title with the same bracketed slug (`[]`) instead of opening a duplicate — the existing pull request -*is* the work item. A separate tracking issue is optional in this case (a work -item may be a pull request alone); if one already exists, link it with -`Fixes #n` so merging still closes it. Reusing what is already open avoids two -competing pull requests touching the same files. +repository's slice is one Task or Bug delivery leaf and its pull request. A +campaign has a short, stable **slug** (for example `process-psmodule-v6`) that +names both artifacts everywhere. + +An **existing open pull request can be adopted** when its scope matches the +campaign slice. Ensure it closes exactly one correctly typed Task or Bug, +creating that delivery issue first when it is missing, then add the remaining +change to the existing branch instead of opening a duplicate. Reusing what is +already open avoids competing pull requests without bypassing the canonical +[Issue Hierarchy](Issues/Types/Hierarchy.md) or [PR Format](PR-Format.md). ## State lives on GitHub @@ -55,15 +54,15 @@ copied into another state-bearing field, because duplicated state drifts. | Fact | GitHub property | | --- | --- | -| Work item exists | the issue or pull request itself | +| Delivery exists | its Task or Bug issue | | Who holds it | `assignees` | | Work in progress | pull request is a **draft** | | Ready for a human | pull request is **not** a draft | | CI health | the status-check rollup | | Review outcome | `reviewDecision` and unresolved review threads | | Mergeability | `mergeable` / merge-state status | -| Done | pull request **merged**; tracking issue **closed** | -| Issue ↔ PR link | the pull request's closing references (`Fixes #n`) | +| Done | pull request **merged**; Task or Bug **closed** | +| Issue ↔ PR link | the pull request's one closing reference | "Ready for review" is the draft flag flipping off; "done" is the merge. The two signals people care about most are native, and are set by the same act that does @@ -71,9 +70,10 @@ the work — [marking ready](Contribution-Workflow.md) and merging. ### Campaign identity lives in the title -Campaign membership is carried in a stable square-bracket prefix on every work -item title, for example `[process-psmodule-v6]`. The prefix is the cross- -repository join key; labels stay reserved for mutable workflow state. +Campaign membership is carried in a stable square-bracket prefix on every +delivery issue and pull request title, for example `[process-psmodule-v6]`. The +prefix is the cross-repository join key; labels stay reserved for mutable +workflow state. ### Process labels fill the gap @@ -84,11 +84,11 @@ same way. | Label | Purpose | | --- | --- | -| `stage:queued` | Identified, not started (typically a tracking issue with no pull request yet). | +| `stage:queued` | Delivery issue exists; no pull request has started. | | `stage:in-progress` | Actively being changed; the assignee owns it. | | `stage:blocked` | Needs a human decision or a manual, off-platform action before it can proceed. | -Rules: the campaign prefix is mandatory on every work item; at most one +Rules: the campaign prefix is mandatory on every delivery issue and pull request; at most one `stage:*` label applies at a time; and `stage:*` may be dropped once a pull request carries the signal itself (a ready pull request needs no `stage` label, but `stage:blocked` stays explicit because "a human must act" has no built-in @@ -96,7 +96,7 @@ equivalent). The slug inside the prefix is lowercase and hyphenated. ## Effective status -A campaign view shows one **effective status** per work item, derived purely from +A campaign view shows one **effective status** per repository delivery, derived purely from the two layers above — no guessing. The first matching rule wins. | # | Effective status | Condition | @@ -108,7 +108,7 @@ the two layers above — no guessing. The first matching rule wins. | 5 | CI failing | checks are failing on a draft | | 6 | In review | draft with at least one review and CI not failing | | 7 | In progress | draft with no review yet, or `stage:in-progress` | -| 8 | Queued | tracking issue open, no pull request yet | +| 8 | Queued | Task or Bug open, no pull request yet | Terminal and attention states (merged, blocked, changes requested, ready) rank above transient progress states, because an explicit act — marking ready, or @@ -121,49 +121,46 @@ GitHub action, so the resulting state is always re-derivable. ```mermaid flowchart TD - Q[Queued: tracking issue] --> P[Open PR as draft] + Q[Queued: Task or Bug] --> P[Open PR as draft] A[Adopt existing PR: return to draft] --> R P --> R[Contribution Workflow: Copilot review loop] R -->|needs a human decision| B[Blocked] B -->|unblocked| R R -->|loop clean| Y[Mark ready for review] Y --> M[Human review and merge] - M --> D[Close tracking issue if linked] + M --> D[Close Task or Bug] ``` -1. **Queue the work.** Create a tracking issue per repository, with the campaign - prefix in the title and `stage:queued`, following the [Issue Format](Issues/Process/Format.md). - The whole fleet starts as *Queued*. Skip this for any repository whose work - item will be an **adopted pull request** (see step 2): that pull request is - the work item and needs no tracking issue. +1. **Queue the work.** Create one Task or Bug delivery issue per repository, with + the campaign prefix in the title and `stage:queued`. Route it through the + [Issue Hierarchy](Issues/Types/Hierarchy.md) and follow its canonical type + page. The whole fleet starts as *Queued*. 2. **Branch and open a draft.** Create a worktree and branch ([Git Worktrees](Git-Worktrees.md)), then open a **draft** pull request that - closes the tracking issue, per [PR Format](PR-Format.md). Use the same + closes exactly that delivery issue, per [PR Format](PR-Format.md). Use the same campaign prefix in the pull request title and move the stage to - `stage:in-progress`, clearing the tracking issue's `stage:queued` so the work - item never carries two stages at once. If the repository already has an open + `stage:in-progress`, clearing the issue's `stage:queued` so the repository + delivery never carries two stages at once. If the repository already has an open pull request that covers part of the change, adopt it instead of opening a new one: add the remaining change to its branch, return it to **draft** while work is in progress, and give it the same campaign prefix and - `stage:in-progress` label (clearing `stage:*` from any linked issue). The - adopted pull request is the work item, so a separate tracking issue is - optional — link one with `Fixes #n` if it exists; when there is none, the - tracking-issue steps (1 and the close-on-merge in 6) simply do not apply, and - the pull request's own draft and merge state carry the signal. + `stage:in-progress` label (clearing `stage:*` from its closing issue). Add the + one closing Task or Bug reference before continuing if the adopted pull + request did not already have it. 3. **Apply the change and run the loop.** Make the change and take the pull request through the [Contribution Workflow](Contribution-Workflow.md) — the Copilot review loop — exactly as any single-repository change. The - [Implement](../Agents/implement.md) and [Reviewer](../Agents/reviewer.md) - agent roles apply unchanged. -4. **Flag blockers, don't stall the fleet.** If an item needs a human decision or + [Implement](Workflow-Stages/Implement.md) and [Review](Workflow-Stages/Review.md) + workflow stages apply unchanged. +4. **Flag blockers, don't stall the fleet.** If a delivery leaf needs a human decision or an off-platform action, set `stage:blocked` with a note and move on to the next repository. 5. **Mark ready only when the loop is clean.** When Copilot has no more feedback and CI is green, mark the pull request ready for review per the [Definition of Ready and Done](Definition-of-Ready-and-Done.md). The draft - flag flips and the item becomes *Ready for review*. -6. **Human review and merge land it.** Merging lands the change and, where the - pull request links a tracking issue, closes it via the `Fixes #n` reference. + flag flips and the repository delivery becomes *Ready for review*. +6. **Human review and merge land it.** Merging lands the change and closes its + Task or Bug through the pull request's one closing reference. The campaign's job is to get every pull request to *Ready*; [Branching and Merging](Branching-and-Merging.md) governs how it merges. @@ -193,19 +190,20 @@ concern that is invalid on one repository is usually invalid on the rest, barring a repository where the change genuinely differs. Discovery can also surface a repository where the change applies differently, or -not at all. Adapt that work item — its change and its pull request description — -to what the repository actually needs, rather than forcing an identical diff. +not at all. Adapt that delivery leaf — its scope, change, and pull request +description — to what the repository actually needs, rather than forcing an +identical diff. ## The dashboard A campaign is watched through a **dashboard** — a deterministic projection of -GitHub state, not a store of its own. A script enumerates the campaign's work -items by title prefix (`[]` across the owners), reads each pull request's +GitHub state, not a store of its own. A script enumerates the campaign's +repository deliveries by title prefix (`[]` across the owners), reads each pull request's built-in properties, computes the effective status, and renders a page. It can regenerate on an interval so the view refreshes as GitHub changes; deleting it loses nothing, because GitHub is the source of truth. -Typical columns: effective status, repository, work item (linked), draft/ready, +Typical columns: effective status, repository, delivery issue and pull request, draft/ready, CI, review decision and open-thread count, assignee, the latest progress note, and last-updated. Rows sort by status then repository name, so the items needing attention group together. *Ready for review* appears the instant a pull request @@ -219,8 +217,8 @@ source of truth that drifts. ## Progress notes To record *what* is being done — narration, not a state change — post a comment -on the work item and, when the pipeline stage changes, move the `stage:*` label. -The dashboard surfaces the latest comment as the item's note. Setting a status is +on the delivery issue and, when the pipeline stage changes, move the `stage:*` label. +The dashboard surfaces the latest comment as the delivery's note. Setting a status is a deterministic label-and-comment write, so it is scripted; the judgement of *which* status applies is the only human or agent decision. @@ -233,7 +231,7 @@ Because all state is on GitHub and the workflow labels are generic: `stage:*` labels, and merge. - **An automated system** — a scheduled workflow, a different agent framework, or a teammate's tooling — can enumerate the work with one query and pick up any - item. The state is portable and self-describing. + delivery. The state is portable and self-describing. If an automated run stops midway, nothing is lost: the board is complete, every in-flight pull request shows its true state, and anyone can finish the job. This @@ -271,7 +269,7 @@ the Pester version requirement to the test files, and migrate the tests. - **Slug:** `process-psmodule-v6`. - **Discover the fleet:** find consumers of the reusable workflow with a code - search for its `uses:` reference, then queue a tracking issue in each. + search for its `uses:` reference, then queue a Task delivery issue in each. - **Per repository:** bump the workflow pin, add the `#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' }` requirement to each `*.Tests.ps1`, migrate the tests, and take the pull request diff --git a/src/docs/Ways-of-Working/Git-Worktrees.md b/src/docs/Ways-of-Working/Git-Worktrees.md index 53aa28e..0387a9c 100644 --- a/src/docs/Ways-of-Working/Git-Worktrees.md +++ b/src/docs/Ways-of-Working/Git-Worktrees.md @@ -5,17 +5,17 @@ description: How agentic development is implemented locally — a bare-clone and # Git Worktrees -Git worktrees are how [agentic development](Agentic-Development.md) is implemented on a local machine. They are purely a **local development** convenience: a way for one person — or a person and an agent, or several agents — to work on multiple issues in the same repository at the same time, without stashing, committing half-finished work, or switching branches. They change nothing about how a repository is built, reviewed, or shipped — that still happens through branches and pull requests, exactly as it would with an ordinary clone. +Git worktrees are how [agentic development](Agentic-Development.md) is implemented on a local machine. They are purely a **local development** convenience: a way for one person — or a person and an agent, or several agents — to work on multiple repository-delivery leaves at the same time, without stashing, committing half-finished work, or switching branches. They change nothing about how a repository is built, reviewed, or shipped — that still happens through branches and pull requests, exactly as it would with an ordinary clone. -All repositories are set up as **bare clones with worktrees**. Each piece of work gets its own worktree — an independent working directory for one branch — so parallel work never collides. +All repositories are set up as **bare clones with worktrees**. Each repository-delivery Task or Bug gets its own worktree — an independent working directory for one branch — so parallel work never collides. Epic and PBI aggregates, and operational Tasks without repository artifacts, do not get worktrees. ## Why this matters: working agentically in parallel -The reason this layout is the default — not the occasional convenience it is in most projects — is **parallelism**. [Agentic development](Agentic-Development.md) does not proceed one issue at a time. A single developer can have several agents working at once, each on a different issue, alongside their own hands-on changes. Worktrees are what make running many streams at once safe instead of chaotic: +The reason this layout is the default — not the occasional convenience it is in most projects — is **parallelism**. [Agentic development](Agentic-Development.md) does not proceed one delivery leaf at a time. A single developer can have several agents working at once, each on a different Task or Bug, alongside their own hands-on changes. Worktrees are what make running many streams at once safe instead of chaotic: -- **One worktree per issue, one agent per worktree.** Each agent gets its own working directory, its own branch, and its own uncommitted state. Two agents never write to the same checkout, so their edits cannot corrupt one another. -- **No stashing, no branch-switching, no waiting.** Because the worktrees are independent, the agent finishing issue #42 never disturbs the one still working on issue #99 — and neither touches the clean `/` reference you read from. Nobody has to reach a clean tree before anyone else can move. -- **Fan out, then integrate.** A batch of independent issues can be started together — one worktree each — worked concurrently, and merged back one at a time through the normal [branch-and-PR flow](Branching-and-Merging.md) as each finishes. +- **One worktree per repository-delivery Task or Bug, one agent per worktree.** Each agent gets its own working directory, its own branch, and its own uncommitted state. Two agents never write to the same checkout, so their edits cannot corrupt one another. +- **No stashing, no branch-switching, no waiting.** Because the worktrees are independent, the agent finishing Task #42 never disturbs the one still working on Bug #99 — and neither touches the clean canonical `/` worktree you read from. Nobody has to reach a clean tree before anyone else can move. +- **Fan out, then integrate.** A batch of independent delivery leaves can be started together — one worktree each — worked concurrently, and merged back one at a time through the normal [branch-and-PR flow](Branching-and-Merging.md) as each finishes. In a single ordinary clone the opposite is forced: one branch checked out at a time, every human and agent contending for the same files, constant stashing and switching. Worktrees turn "several issues at once" from a hazard into the default working mode — which is what makes local agentic development practical at all. @@ -23,26 +23,26 @@ In a single ordinary clone the opposite is forced: one branch checked out at a t | Problem with traditional clones | How worktrees solve it | | -------------------------------------------- | --------------------------------------------------------- | -| Only one branch checked out at a time | Each issue gets its own worktree — parallel by default | +| Only one branch checked out at a time | Each delivery leaf gets a worktree — parallel by default | | Switching branches requires clean state | Worktrees are independent — no stashing or committing WIP | | Agent work blocks human work on same repo | Different worktrees, no interference | -| Default branch gets dirty during development | `/` worktree is always a clean reference | +| Default branch gets dirty during development | Canonical `/` worktree is always a clean reference | ## Repository layout ```text -/ -├── .bare/ # bare git data (the actual repository) -├── .git # file containing: gitdir: ./.bare -├── / # worktree: default branch (always clean, never worked in directly) +/ +├── .git/ # bare backing repository +├── / # canonical default-branch worktree (always clean) ├── 42-add-pagination/ # worktree folder; branch: feat/42-add-pagination └── 99-null-ref/ # worktree folder; branch: fix/99-null-ref ``` -- **`.bare/`** — the shared git object store. All worktrees share this. -- **`.git`** — a file (not a directory) that points git tooling to `.bare/`. -- **`/`** — the default branch worktree (e.g. `main` or `master`). Kept as a clean reference. Used for diffing, reading docs, running comparisons. Never directly committed to. -- **`-/`** — one worktree folder per issue in flight, named by issue number and a short slug. The folder is a concise local path; its branch uses the required `/-` name, so the two names do not need to match. +- **`.git/`** — the shared bare object store. All worktrees use this backing repository. +- **`/`** — the canonical default-branch worktree. Kept clean and exactly synchronized for reading, diffing, and comparisons. Never directly committed to. +- **`-/`** — one worktree folder per repository-delivery Task or Bug in flight, named by issue number and a short slug. The folder is a concise local path; its branch uses the required `/-` name, so the two names do not need to match. + +For the central MSX context, this becomes `~/.msx/docs.git` plus the readable `~/.msx/docs` main worktree. Memory remains a simple checkout at `~/.msx/memory`. ## Remotes @@ -75,43 +75,44 @@ Both remotes are configured with full refspecs so `git fetch --all --prune` keep ## Setup (one-time per repository) ```powershell -# Clone as bare into .bare/ -git clone --bare https://github.com//.git .bare - -# Create the .git pointer file -Set-Content .git "gitdir: ./.bare" -NoNewline +# From the workspace root, clone the bare backing repository. +$repo = '' +git clone --bare "https://github.com//$repo.git" "$repo.git" # Configure fetch refspec (bare clones don't set this automatically) -git -C .bare config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' +git --git-dir="$repo.git" config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' # Fetch remote branches -git -C .bare fetch origin +git --git-dir="$repo.git" fetch origin # Determine the default branch -$defaultBranch = git -C .bare symbolic-ref HEAD | ForEach-Object { $_ -replace 'refs/heads/', '' } +$defaultBranch = git --git-dir="$repo.git" symbolic-ref HEAD | + ForEach-Object { $_ -replace 'refs/heads/', '' } -# Create the default branch worktree -git -C .bare worktree add "../$defaultBranch" $defaultBranch +# Create the canonical default-branch worktree at the repository name. +git --git-dir="$repo.git" worktree add $repo $defaultBranch # Set upstream tracking (prevents "Publish Branch" prompt in VS Code) -git -C .bare config "branch.$defaultBranch.remote" origin -git -C .bare config "branch.$defaultBranch.merge" "refs/heads/$defaultBranch" +git --git-dir="$repo.git" config "branch.$defaultBranch.remote" origin +git --git-dir="$repo.git" config "branch.$defaultBranch.merge" "refs/heads/$defaultBranch" ``` > The [Checkout-GitHubRepo](https://github.com/MariusStorhaug/.dev/blob/main/.github/Checkout-GitHubRepo.ps1) script automates this for all repositories. -## Working on an issue +## Working on a delivery leaf ```powershell -# From the repo root (where .bare/ lives) -$defaultBranch = git -C .bare symbolic-ref HEAD | ForEach-Object { $_ -replace 'refs/heads/', '' } +# From the workspace root (where .git lives) +$repo = '' +$defaultBranch = git --git-dir="$repo.git" symbolic-ref HEAD | + ForEach-Object { $_ -replace 'refs/heads/', '' } $worktreeName = '42-add-pagination' $branchName = 'feat/42-add-pagination' -git -C .bare worktree add "../$worktreeName" -b $branchName $defaultBranch +git --git-dir="$repo.git" worktree add $worktreeName -b $branchName $defaultBranch # Set upstream tracking (prevents "Publish Branch" prompt in VS Code) -git -C .bare config "branch.$branchName.remote" origin -git -C .bare config "branch.$branchName.merge" "refs/heads/$branchName" +git --git-dir="$repo.git" config "branch.$branchName.remote" origin +git --git-dir="$repo.git" config "branch.$branchName.merge" "refs/heads/$branchName" # Open in VS Code code $worktreeName @@ -123,17 +124,17 @@ Then follow the normal Implement flow: initial commit → push → draft PR → ```powershell # Remove the worktree -git -C .bare worktree remove 42-add-pagination +git --git-dir="${repo}.git" worktree remove 42-add-pagination # Delete the local branch ref -git -C .bare branch -D feat/42-add-pagination +git --git-dir="${repo}.git" branch -D feat/42-add-pagination # Prune if needed (removes stale worktree references) -git -C .bare worktree prune +git --git-dir="${repo}.git" worktree prune ``` ## Where this connects - [Agentic Development](Agentic-Development.md) — the framework these worktrees implement locally, so several pieces of work run in parallel. -- [Branching and Merging](Branching-and-Merging.md) — the branch-per-issue model each worktree holds. +- [Branching and Merging](Branching-and-Merging.md) — the branch-per-delivery-leaf model each worktree holds. - [Workflow](Workflow.md) — where creating a worktree fits in the flow from issue to delivery. diff --git a/src/docs/Ways-of-Working/Goal-Setting.md b/src/docs/Ways-of-Working/Goal-Setting.md index 764d69f..d5cdbc0 100644 --- a/src/docs/Ways-of-Working/Goal-Setting.md +++ b/src/docs/Ways-of-Working/Goal-Setting.md @@ -1,6 +1,6 @@ --- title: Goal-Setting Framework -description: Mission, OKRs, and initiatives — strategy connected to delivery. +description: Mission, OKRs, and Initiatives — the strategy above repository Epics. --- # Goal-Setting Framework @@ -12,8 +12,8 @@ MSX uses a lightweight OKR-based framework to connect strategic direction to day | Layer | Lives in | Purpose | | -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------- | | **Mission** | Pinned issue in the org `.github` repository | The org's reason to exist — *make great software delivery the default: easy, fast, and safe* | -| **OKR** | Sub-issues of the Mission | Qualitative objectives + measurable key results | -| **Initiative** | Sub-issues of an OKR | A concrete bet to move a Key Result — becomes an Epic in a repo | +| **OKR** | Sub-issues of the Mission | Qualitative objectives + measurable key results | +| **Initiative** | Sub-issues of an OKR | A concrete organization-level bet to move a Key Result | ## Why OKRs and not KPIs @@ -32,11 +32,14 @@ OKRs are tracked as sub-issues of the Mission issue in the org `.github` reposit ## From strategy to delivery -Initiatives are the bridge between strategy and execution. An Initiative is a sub-issue of an OKR and maps directly to an **Epic** in the relevant repository. From there it decomposes into PBIs and Tasks through the [Issue Hierarchy](Issues/Types/Hierarchy.md). +Mission, OKR, and Initiative are the strategy layers. The strategy boundary sits **above Epic**: an Initiative links to the repository Epics that deliver its outcome, and each participating repository owns its own Epic. The canonical [Issue Hierarchy](Issues/Types/Hierarchy.md) owns all decomposition and delivery semantics from Epic downward. ```text Mission (org-level, evergreen) └── OKR (qualitative objective + key results) └── Initiative (concrete bet to move a KR) - └── Epic (in a repository) → PBIs → Tasks + ├── Epic (repository A) + └── Epic (repository B) ``` + +The arrow from Initiative to Epic is a strategy-to-delivery link, not a replacement for the native relationships inside a repository. Epic and lower issue types follow the [Issue Planning](Issues/Process/Planning.md) and [Issue Relationships](Issues/Process/Relationships.md) guidance. diff --git a/src/docs/Ways-of-Working/PR-Format.md b/src/docs/Ways-of-Working/PR-Format.md index 7f9d6af..f30148f 100644 --- a/src/docs/Ways-of-Working/PR-Format.md +++ b/src/docs/Ways-of-Working/PR-Format.md @@ -119,19 +119,19 @@ The **Technical details** block is for reviewers and maintainers. Include intern - Which internal functions, classes, or files were changed. - Implementation approach and design decisions. - Backward compatibility notes for developers. -- **Implementation plan progress** — cross-reference Section 3 of the linked issue. Which tasks does this PR complete? Which remain? +- **Implementation plan progress** — cross-reference the closing Task or Bug's plan. Which plan steps does this PR complete? Which were moved to follow-up delivery issues? The **Relevant issues (or links)** block is required and uses fully qualified references (`Owner/Repo#N`) so links work across repositories. -Use one bullet per linked issue. Use a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue) (`Fixes`, `Closes`, `Resolves`) when the PR fully addresses the issue. Otherwise, list the reference without a keyword to indicate a relationship without auto-closing. +Use one bullet per linked issue. Exactly one bullet uses a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue) (`Fixes`, `Closes`, or `Resolves`), and it closes the one Task or Bug delivered by the pull request. A parent PBI or Epic may appear as context without a closing keyword; never close an aggregate through a delivery pull request. Other supporting links are also non-closing. -If no issue is linked: **stop**. PRs without issues break the workflow. Route back to the Ideator role or proceed only on explicit user confirmation. The Shipper enforces this. +If there is not exactly one closing Task or Bug: **stop**. Route back to [Define](Workflow-Stages/Define.md) to create or correctly route the delivery leaf. The [Issue Hierarchy](Issues/Types/Hierarchy.md) and [Issue Lifecycle](Issues/Process/Lifecycle.md) own type and closure semantics. ## Formatting - Paragraphs are written as a **single unbroken line**. GitHub renders mid-paragraph newlines as spaces. - The PR description is **the release note**. Write it for users, not reviewers. -- If a linked issue exists, the PR title and description should align with the issue's user-facing framing and the Technical Decisions section (Section 2). +- The PR title and description align with the closing Task or Bug's user-facing framing and recorded technical decisions. ## Example diff --git a/src/docs/Ways-of-Working/Principles/AI-First-Development.md b/src/docs/Ways-of-Working/Principles/AI-First-Development.md index 5f9f10d..d8414d2 100644 --- a/src/docs/Ways-of-Working/Principles/AI-First-Development.md +++ b/src/docs/Ways-of-Working/Principles/AI-First-Development.md @@ -32,20 +32,20 @@ Agents are trained to read documentation. That is their natural skill. By keepin Agent context is delivered through three layers, in priority order: 1. **Documentation** — the primary source. Published docs at , READMEs, and issue bodies are written for humans and naturally consumable by agents. -2. **Central agent descriptions** — the roles agents play, authored once as documentation in the [Agents](../../Agents/index.md) section. They reference the ways of working and define roles, boundaries, and procedural steps — never standards or conventions. -3. **Local pointer files** — each repository's `AGENTS.md` (and the `CLAUDE.md` that imports it), pointing to the central descriptions and adding only repo-specific nuance and genuinely tool-specific settings. +2. **Canonical workflow** — [Workflow](../Workflow.md) owns the process and links to ordinary documentation for each [stage procedure](../Workflow-Stages/index.md). Indexes provide the default discovery path; clear task language may shortcut stage selection without creating separate instructions. +3. **Local pointer files** — each repository's `AGENTS.md` (and the `CLAUDE.md` that imports it), pointing to the canonical docs and memory root indexes and adding only repo-specific nuance and genuinely tool-specific settings. ## Augmentation, not replacement Agents amplify the team. They make us faster, more consistent, and free us from work that is mechanical. **Human in the loop** remains the default for decisions that matter. -## Persona, not swarm +## One workflow, not a swarm -Treat the agent ecosystem as one team mate. Many specialized roles, one cohesive bank of knowledge, one consistent voice. +Treat the agent ecosystem as one teammate following one shared workflow. Stages and specialized review paths divide responsibility while preserving one cohesive bank of knowledge and one consistent voice. ## Self-improving agents -Agents need feedback and a way to process it. Every agent definition should evolve as we learn. Capture lessons in the agent definitions and in this docs section — don't let them live only in someone's head. +Agents need feedback and a way to process it. Every workflow stage should evolve as we learn. Capture lessons in the stage descriptions and in this docs section — don't let them live only in someone's head. ## Integration and sensoring @@ -78,7 +78,7 @@ See [README-Driven Context](../Readme-Driven-Context.md). The work of keeping context **right, evergreen, and declarative** runs alongside software delivery: - **Software delivery** produces code, tests, and releases using source control, CI/CD, and DevOps practices. -- **Context maintenance** produces issues, decisions, READMEs, agent definitions, and documentation — and treats them as products that must be kept current. +- **Context maintenance** produces issues, decisions, READMEs, pointer files, and documentation — and treats them as products that must be kept current. Both run continuously. Each iteration of software delivery produces context that needs maintenance; each iteration of context maintenance unblocks the next round of software work. diff --git a/src/docs/Ways-of-Working/Principles/Engineering-Practices.md b/src/docs/Ways-of-Working/Principles/Engineering-Practices.md index 75603a9..618d2d5 100644 --- a/src/docs/Ways-of-Working/Principles/Engineering-Practices.md +++ b/src/docs/Ways-of-Working/Principles/Engineering-Practices.md @@ -17,7 +17,7 @@ If something is only in your head: ## Everything as code -Everything — workflows, configuration, infrastructure, processes, agent definitions, even the docs you are reading — lives in source control. This is the bedrock of human-agent interaction. An agent can read code, change code, propose code. It cannot read what's only in someone's memory. +Everything — workflows, configuration, infrastructure, processes, pointer files, even the docs you are reading — lives in source control. This is the bedrock of human-agent interaction. An agent can read code, change code, propose code. It cannot read what's only in someone's memory. ## Code in code files diff --git a/src/docs/Ways-of-Working/Principles/Purpose-and-Direction.md b/src/docs/Ways-of-Working/Principles/Purpose-and-Direction.md index 686ad0a..bf38398 100644 --- a/src/docs/Ways-of-Working/Principles/Purpose-and-Direction.md +++ b/src/docs/Ways-of-Working/Principles/Purpose-and-Direction.md @@ -11,9 +11,9 @@ Every piece of work — at every level — should be groundable in three concent - **Why** — what change in the world are we trying to make? Vision. - **How** — what approach do we take to make that change happen? Mission. -- **What** — what concrete thing are we delivering right now? OKRs → initiatives → tasks. +- **What** — what concrete thing are we delivering right now? -When an issue is being written, the **Why** belongs in the Context part of Section 1. The **How** belongs in Section 2 (Technical Decisions). The **What** belongs in Section 3 (Implementation Plan). +[Goal Setting](../Goal-Setting.md) owns the strategy path from Mission through OKR and Initiative. [Issue Planning](../Issues/Process/Planning.md) applies Why, How, and What progressively from Epic and PBI aggregates to Task and Bug delivery leaves; this principle does not assign them to copied body sections. ## Product / service mindset diff --git a/src/docs/Ways-of-Working/Readme-Driven-Context.md b/src/docs/Ways-of-Working/Readme-Driven-Context.md index 837f44d..bd38f9a 100644 --- a/src/docs/Ways-of-Working/Readme-Driven-Context.md +++ b/src/docs/Ways-of-Working/Readme-Driven-Context.md @@ -53,9 +53,9 @@ The README is updated **in the same pull request** that introduces the change. D - CI/CD changes that do not affect usage. - Dependency updates that do not change behavior. -## Agent workflow +## Using repository context -When an agent works on a repository, the README sequence is: +When a human or agent works on a repository, the README sequence is: 1. **Read the README first** — before reading source code, understand the project context. 2. **Use the README as a guide** — let documented scope and architecture inform where and how to make changes. diff --git a/src/docs/Ways-of-Working/Repository-Standard.md b/src/docs/Ways-of-Working/Repository-Standard.md index fa4f105..88ef370 100644 --- a/src/docs/Ways-of-Working/Repository-Standard.md +++ b/src/docs/Ways-of-Working/Repository-Standard.md @@ -110,7 +110,7 @@ Default title pattern: []: ``` -The description should lead with user-facing impact, continue with user-facing change sections, include optional technical details after those sections, and end with the related-issues block. +The description should lead with user-facing impact, continue with user-facing change sections, include optional technical details after those sections, and end with the related-issues block. It closes exactly one Task or Bug as required by [PR Format](PR-Format.md). Repository templates may be simpler than the full PR Manager body, but they must gather enough information to reconstruct it. diff --git a/src/docs/Ways-of-Working/Review-Etiquette.md b/src/docs/Ways-of-Working/Review-Etiquette.md index 6fbd14a..937cf9a 100644 --- a/src/docs/Ways-of-Working/Review-Etiquette.md +++ b/src/docs/Ways-of-Working/Review-Etiquette.md @@ -23,7 +23,7 @@ Consider each of the following dimensions when reviewing a PR: ### Stay in scope -The PR delivers a specific issue. Suggestions that go beyond that issue are **new issues**, not blocking review comments. File the follow-up and link it from the comment. +The PR delivers one Task or Bug. Suggestions that go beyond that delivery leaf are **new issues**, not blocking review comments. File the follow-up and link it from the comment. ### One concern per comment @@ -40,7 +40,7 @@ Use explicit prefixes so the author knows what is blocking: ### Reason from evidence -Cite the docs, the linter rule, the security advisory, the linked issue's Section 2. "It feels off" is rarely actionable. "This contradicts the decision in #N's Section 2" is. +Cite the docs, the linter rule, the security advisory, or a recorded decision in the closing delivery issue. "It feels off" is rarely actionable. "This contradicts the decision recorded in Owner/Repo#N" is. ### Don't review your own PR publicly diff --git a/src/docs/Ways-of-Working/Spec-Driven-Development.md b/src/docs/Ways-of-Working/Spec-Driven-Development.md index ec0a7fd..ba77e1d 100644 --- a/src/docs/Ways-of-Working/Spec-Driven-Development.md +++ b/src/docs/Ways-of-Working/Spec-Driven-Development.md @@ -5,7 +5,7 @@ description: The specification is the source of truth — the spec (why and what # Spec-Driven Development -Spec-driven development treats the specification as the source of truth: the spec captures *why* a change matters and *what* it must do, and everything downstream — design, tasks, code, tests — serves it. When intent changes, the spec changes first and the rest follows. +Spec-driven development treats the specification as the source of truth: the spec captures *why* a change matters and *what* it must do, and everything downstream — design, delivery issues, code, tests — serves it. When intent changes, the spec changes first and the rest follows. This standard is the shape of a spec. It defines the artifacts of the method — the **specification** and its **design** — what each contains, at what level of detail, and how they move through the life of a change. It builds on the [evergreen documentation](Principles/Engineering-Practices.md#evergreen-documentation) principle (how a spec is written) and the [engineering practices](Principles/Engineering-Practices.md) (how we plan, build, ship, and measure). @@ -18,15 +18,15 @@ A change moves down a ladder of artifacts. Each sits at a fixed altitude, is ver | **Need** | Is this worth doing? | one line | the request or issue | — | | **Spec** | Why, what, for whom, and what "done" means | implementation-agnostic | the capability folder, in the owning repo | intent changes | | **Design** | How it is built | technical, still readable | beside its spec (`design.md`) | the approach changes | -| **Tasks** | In what steps | actionable | issues, at the levels of the [issue hierarchy](Issues/Types/Hierarchy.md) | the plan changes | +| **Delivery issues** | In what tracked outcomes and deliverables | progressively actionable | Epic and PBI aggregates, then Task and Bug leaves | the delivery plan changes | | **Code & tests** | The working expression | concrete | the codebase | continuously | ```mermaid flowchart LR need["Need\nrequest · incident · gap"] --> spec["Spec\nwhy · what · impact"] spec --> design["Design\nhow"] - design --> tasks["Tasks\nepic → item → task"] - tasks --> code["Code & tests\nthe expression"] + design --> issues["Delivery issues\nEpic → PBI → Task / Bug"] + issues --> code["Code & tests\nthe expression"] code -. "production feedback" .-> spec ``` @@ -56,7 +56,7 @@ A spec **excludes** — this is the design's job: - The task breakdown and rollout sequence. - Links to the code that fulfils it — implementations come and go. -The altitude test: push detail *down* into the design, and push scope *up* into the epic. If a sentence would change when the team picks a different library, it belongs in the design, not the spec. This is the same rule the [Issue Format](Issues/Process/Format.md) applies to issues — describe the *what* and *why*, never the *how*. +The altitude test: push implementation detail *down* into the design, and keep delivery scope at the appropriate [issue planning altitude](Issues/Process/Planning.md). If a sentence would change when the team picks a different library, it belongs in the design, not the spec. ## Specify the minimum @@ -111,9 +111,9 @@ The method is requirements-first. Work does not start from a solution; it starts 1. **A need surfaces.** A stakeholder request, an incident, or a platform gap — or the team authors one when it sees the opportunity. 2. **Draft the spec.** Capture the why, the outcome, and the requirements collaboratively. Agents draft, research context, and check the spec for ambiguity and gaps; humans supply the intent and make the calls ([AI-first development](Principles/AI-First-Development.md)). Unknowns are marked, not guessed (see [Authoring conventions](#authoring-conventions)). 3. **Review the spec as a pull request.** The spec is versioned and reviewed like any change, following [PR Format](PR-Format.md) and [Review Etiquette](Review-Etiquette.md). Review argues about intent while it is still cheap to change. -4. **Pass the readiness bar.** The spec is ready when it meets the [Definition of Ready](Definition-of-Ready-and-Done.md#definition-of-ready) — clear intent, testable acceptance criteria, no open questions that would change the approach. -5. **Design and decompose.** Write the design, then break the work into the levels of the [issue hierarchy](Issues/Types/Hierarchy.md) — an epic into smaller, independently deliverable items. -6. **Build against the spec.** Implement in thin vertical slices, test-first where it pays, to the [Definition of Done](Definition-of-Ready-and-Done.md#definition-of-done) ([engineering practices](Principles/Engineering-Practices.md)). [Test-driven development](Principles/Engineering-Practices.md#test-driven-development) is an implementation practice governed by the coding standards and the Definition of Done, not something each spec re-specifies. +4. **Accept the spec.** Resolve clarification markers, confirm testable requirements and acceptance criteria, and review the intent before committing to delivery. +5. **Design and decompose.** Write the design, then use the canonical [Issue Planning](Issues/Process/Planning.md) and [Issue Hierarchy](Issues/Types/Hierarchy.md) guidance to create Epic or PBI aggregates and ready Task or Bug delivery leaves. +6. **Build against the spec.** Pull only a ready Task or Bug into Build, implement in thin vertical slices, and finish through its applicable [Definition of Done](Definition-of-Ready-and-Done.md#definition-of-done) ([engineering practices](Principles/Engineering-Practices.md)). [Test-driven development](Principles/Engineering-Practices.md#test-driven-development) is an implementation practice governed by the coding standards and the Definition of Done, not something each spec re-specifies. 7. **Feed reality back.** Metrics and incidents update the spec, and the cycle repeats. ## Where specs and designs live diff --git a/src/docs/Ways-of-Working/Workflow-Stages/Define.md b/src/docs/Ways-of-Working/Workflow-Stages/Define.md new file mode 100644 index 0000000..446e3ee --- /dev/null +++ b/src/docs/Ways-of-Working/Workflow-Stages/Define.md @@ -0,0 +1,59 @@ +--- +title: Define +description: Procedure for the Workflow stage that captures, routes, refines, and plans work at the correct issue altitude. +--- + +# Define + +Define is the first stage of the canonical [Workflow](../Workflow.md). It takes something someone wants — a feature, a bug, an improvement, external feedback, or a production signal — and routes it to the correct native issue type. The stage hands off an Epic or PBI ready to coordinate and decompose, or a Task or Bug ready for delivery. Define plans work; it does not build it. + +## Enter this stage when + +Capture a desire for change, write an issue, plan work, decompose an epic, refine a bug report, create sub-issues, structure a feature request, or turn feedback into a task. + +## Input + +A description of a desired change, a feedback issue from a non-contributor (treated as input, never modified), a platform signal (error, failed run, alert), or an existing issue to refine. + +## Flow + +### 1. Capture + +Turn the input into an issue with a provisional native type and enough local context to refine it. + +1. Search for duplicates first — propose consolidation rather than creating a new issue. +2. Follow the universal [Issue Format](../Issues/Process/Format.md) and the selected type page for its body. +3. Acceptance criteria must be user-observable and testable. + +### 2. Refine + +Ground the issue so anyone reading it agrees on what "done" means, up to the [Definition of Ready](../Definition-of-Ready-and-Done.md). + +1. Pain before solution — push back on implementation-framed requests. +2. Make assumptions explicit. +3. Acceptance criteria answerable yes or no by a non-author. +4. Ask one question at a time when interactive. + +### 3. Plan + +Decide how the work will happen and record the decisions. + +1. Route the work through the [Issue Hierarchy](../Issues/Types/Hierarchy.md); do not infer altitude from effort, priority, or a legacy label. +2. For an Epic or PBI, establish native containment and dependency relationships and refine the first required children to their own readiness gates. +3. For a Task or Bug, produce one executable delivery plan sized for one pull request. Use the audited operational Task path only when no repository artifact exists. +4. Find the minimum viable path — spike, then proof of concept, then minimum viable product, then improve. +5. Record decisions at the issue's [planning altitude](../Issues/Process/Planning.md), with rationale and alternatives where they matter. +6. Resolve open questions before finishing; defer anything that does not block this slice to a follow-up issue. + +## Operating rules + +1. Tone is impersonal. The issue description is the source of truth; comments record what changed. +2. External references are hyperlinks. +3. Do not modify a feedback issue from a non-contributor — create an internal issue and cross-link. +4. Stop when the issue meets the readiness gate for its altitude. Hand a ready delivery leaf to the [Implement stage](Implement.md); do not build, branch, or open pull requests here. + +## Where this connects + +- [Workflow](../Workflow.md) — the loop this opens. +- [Issue Format](../Issues/Process/Format.md), [Issue Planning](../Issues/Process/Planning.md), [Issue Relationships](../Issues/Process/Relationships.md), and [Issue Hierarchy](../Issues/Types/Hierarchy.md) — canonical issue ownership. +- [Definition of Ready and Done](../Definition-of-Ready-and-Done.md) — the readiness bar this aims for. diff --git a/src/docs/Ways-of-Working/Workflow-Stages/Implement.md b/src/docs/Ways-of-Working/Workflow-Stages/Implement.md new file mode 100644 index 0000000..6ad0950 --- /dev/null +++ b/src/docs/Ways-of-Working/Workflow-Stages/Implement.md @@ -0,0 +1,70 @@ +--- +title: Implement +description: Procedure for the Workflow stage that delivers one ready Task or Bug as a review-ready pull request. +--- + +# Implement + +Implement is the delivery stage of the canonical [Workflow](../Workflow.md). It takes one ready, unblocked Task or Bug and produces working software in a review-ready pull request. The stage owns branching, coding, committing, opening the pull request, tracking progress, running the automated review loop, responding to feedback, and finalizing the release note. Implement builds delivery leaves; it does not implement an Epic or PBI aggregate, plan from scratch, or supply the independent review. + +## Enter this stage when + +Implement a Task, fix a Bug, create a branch, open a pull request, respond to review feedback, or finalize a pull request. Given an Initiative, Epic, or PBI, follow native containment and blocked-by relationships to a ready delivery leaf; do not treat list order as execution order. + +## Input + +A ready repository-delivery Task or Bug number or URL. An operational Task follows its canonical [operational delivery path](../Issues/Types/Task.md#operational-delivery) instead of this pull-request flow. + +## Flow + +### 1. Orient + +1. Read the delivery issue fully, including its type-specific body, native parent, and dependency edges. +2. Read the repository README first per [README-Driven Context](../Readme-Driven-Context.md). +3. Identify the stack and load the relevant [Coding Standards](../../Coding-Standards/index.md). Repo-local linter config wins where it disagrees with a published standard. + +### 2. Branch and draft pull request + +Use [git worktrees](../Git-Worktrees.md) for every repository-delivery Task or Bug. + +1. Create a worktree from the default branch per [Branching and Merging](../Branching-and-Merging.md). +2. Push an initial commit and immediately open a **draft** pull request so CI attaches from the first push. +3. Close exactly that Task or Bug in the pull request. Reference a parent PBI or Epic only as non-closing context, then assign the pull request. + +### 3. Build + +For each step in the delivery plan: + +1. Implement the change and self-review the staged diff. +2. Commit per [Commit Conventions](../Commit-Conventions.md) — one logical change per commit. +3. Update the issue as each plan step completes — do not batch. +4. Push regularly so CI runs against current work. + +When the plan is wrong, stop and document the conflict in a comment, then update the plan before resuming. Out-of-scope problems go to [Define](Define.md). + +### 4. Self-review and respond + +1. Run the [Copilot review loop](../Contribution-Workflow.md#the-copilot-review-loop) until it reports a clean round. +2. Triage each thread and CI failure per [Review Etiquette](../Review-Etiquette.md): fix in scope and propagate the same fix elsewhere; file a follow-up for out-of-scope; reply, then resolve. + +### 5. Finalize and hand off + +When the change meets the [Definition of Ready for Review](../Definition-of-Ready-and-Done.md): + +1. Finalize the title, release-note description, and label per [PR Format](../PR-Format.md). +2. Mark the pull request ready and enable auto-merge per [Branching and Merging](../Branching-and-Merging.md). + +## Operating rules + +1. Micro-commits, one logical change each, with descriptive messages. +2. Progress is visible — the delivery issue is updated as plan steps complete, not in bulk. +3. Draft pull request from the start; stay in the issue's scope. +4. Mark ready only when the change meets the Definition of Ready for Review — never with open plan steps. +5. Return unplanned work to [Define](Define.md) and hand review-ready work to [Review](Review.md). + +## Where this connects + +- [Contribution Workflow](../Contribution-Workflow.md) — the draft-first loop this runs. +- [Issue Lifecycle](../Issues/Process/Lifecycle.md) and [Issue Relationships](../Issues/Process/Relationships.md) — delivery-leaf eligibility and blockers. +- [Definition of Ready and Done](../Definition-of-Ready-and-Done.md) — the gate this hands off at. +- [PR Format](../PR-Format.md) and [Branching and Merging](../Branching-and-Merging.md) — packaging and landing. diff --git a/src/docs/Ways-of-Working/Workflow-Stages/Maintain-Guidance.md b/src/docs/Ways-of-Working/Workflow-Stages/Maintain-Guidance.md new file mode 100644 index 0000000..17b5a34 --- /dev/null +++ b/src/docs/Ways-of-Working/Workflow-Stages/Maintain-Guidance.md @@ -0,0 +1,49 @@ +--- +title: Maintain Workflow Guidance +description: Maintain the canonical Workflow stage procedures and the thin repository pointers that lead to them. +--- + +# Maintain Workflow Guidance + +Maintain the canonical [Workflow](../Workflow.md), the stage procedures in this section, and the per-repository pointer files that lead to the documentation root. Every pointer file stays thin. This maintenance path keeps the indexes, workflow, procedures, and pointers coherent; it does not encode process knowledge in configuration. + +## Enter this maintenance path when + +Create a new workflow stage, update an existing stage, review workflow discoverability, or refactor a bloated agent file back into a thin pointer to the documentation root. + +## Flow + +### 1. Gather requirements + +1. Identify the stage — its entry condition, job, handoff, and boundary. +2. Identify the docs pages that govern the stage; confirm they exist. +3. Identify what the stage must **not** do — boundaries prevent scope creep. + +### 2. Author the description + +Write the stage as a page in this section, following the shape of its siblings: front matter (`title`, `description`), a one-paragraph purpose and boundary, entry condition, input, numbered flow, operating rules, and a "Where this connects" list. + +- **Link, don't inline.** If a standard exists in the docs, link to it — never paste it in. +- **Procedural, not conversational.** Numbered imperatives, no filler. +- **Keyword-rich description.** The front-matter `description` is the discovery surface. + +### 3. Keep pointers thin + +A repository never carries a copy of the workflow. Its `AGENTS.md` — and the `CLAUDE.md` that imports it — point to these pages and add only repo-specific nuance and the genuinely tool-specific settings (permission scopes, model choice) that cannot be expressed as a pointer. When a new runtime is adopted, add a thin pointer; do not move process knowledge into it. See [Agentic Development](../Agentic-Development.md). + +### 4. Validate + +1. Front-matter YAML parses cleanly. +2. Every link resolves, and the body duplicates no doc content. +3. The stage is added to the navigation so its index row generates. + +## Operating rules + +1. Docs are the source of truth. If a standard is missing, propose adding it to the docs — do not embed it in an agent. +2. One stage, one job and handoff. Distinct stages use distinct pages. +3. Update the navigation when adding or removing a stage. + +## Where this connects + +- [Agentic Development](../Agentic-Development.md) — the pointer model this maintains. +- [Documentation Model](../Documentation-Model.md) — how these pages stay evergreen. diff --git a/src/docs/Ways-of-Working/Workflow-Stages/Review.md b/src/docs/Ways-of-Working/Workflow-Stages/Review.md new file mode 100644 index 0000000..80a6e8e --- /dev/null +++ b/src/docs/Ways-of-Working/Workflow-Stages/Review.md @@ -0,0 +1,60 @@ +--- +title: Review +description: Procedure for the Workflow stage that independently reviews a pull request for delivery, taste, security, and decisions. +--- + +# Review + +Review is the independent-assessment stage of the canonical [Workflow](../Workflow.md). It verifies that a pull request delivers its closing Task or Bug, applies good taste, respects security, and does not quietly introduce decisions that were not discussed. This stage comments and approves; it does not change code, fix CI, or merge. + +## Enter this stage when + +Review a pull request, check a change for delivery, verify acceptance criteria, or assess taste and standards. For a security-focused pass, enter [Security Review](Security-Review.md). + +## Input + +A pull request number or URL. If the pull request was authored by the reviewing identity, switch to self-review: write findings to the terminal, and do not post public comments (an author cannot review their own pull request). + +## Flow + +### 1. Read the delivery issue + +Confirm the pull request closes exactly one Task or Bug, then review against that delivery leaf's acceptance criteria, decisions, and plan. Parent PBI or Epic links provide context but do not expand the pull request's scope or close the aggregate. + +### 2. Read the README + +Some "missing" things are by design per the documented scope ([README-Driven Context](../Readme-Driven-Context.md)). + +### 3. Assess the diff + +Check each dimension per [Review Etiquette](../Review-Etiquette.md): + +- **Delivery** — does the diff meet the acceptance criteria, without scope it did not ask for? +- **Taste** — readability, naming, structure, tests that exercise behaviour. +- **Security** — input validation, no secrets in logs, SHA-pinned actions, least privilege. Escalate a deep pass to [Security Review](Security-Review.md). +- **Documentation** — updated where user-facing behaviour changed. +- **Tests** — new behaviour has tests; bugs get regression tests. + +### 4. Post the review + +Use severity prefixes per [Review Etiquette](../Review-Etiquette.md): `Blocking:`, `Question:`, `Suggestion:`, `Nit:`. Out-of-scope suggestions become new issues, not blocking comments. + +### 5. Approve + +An approval co-signs the change, so approve once the blocking concerns are resolved. The approving identity must not be the author and must not be the built-in Actions identity — approvals come from a separate reviewer identity per [Branching and Merging](../Branching-and-Merging.md). With auto-merge enabled, the approval is what lands the change. + +## Operating rules + +1. Read the issue first; review against it. +2. Stay in the pull request's scope — beyond-issue suggestions are filed as new issues. +3. One concern per comment. +4. Apply repo standards; linter rules win where a standard is silent. +5. Security is non-negotiable. +6. No code changes, CI fixes, or merging in the Review stage; required changes return to [Implement](Implement.md). + +## Where this connects + +- [Review Etiquette](../Review-Etiquette.md) — tone, severity, and how to disagree well. +- [PR Format](../PR-Format.md) — delivery-leaf closure and contextual aggregate links. +- [Branching and Merging](../Branching-and-Merging.md) — who approves and how a change lands. +- [Security Review](Security-Review.md) — the specialized security path. diff --git a/src/docs/Agents/security-reviewer.md b/src/docs/Ways-of-Working/Workflow-Stages/Security-Review.md similarity index 72% rename from src/docs/Agents/security-reviewer.md rename to src/docs/Ways-of-Working/Workflow-Stages/Security-Review.md index a1e491a..c62afe2 100644 --- a/src/docs/Agents/security-reviewer.md +++ b/src/docs/Ways-of-Working/Workflow-Stages/Security-Review.md @@ -1,13 +1,13 @@ --- -title: Security Reviewer -description: A structured, defensive security review that reports vulnerabilities as an actionable responsible-disclosure issue. +title: Security Review +description: Procedure for the specialized Workflow stage for defensive security review and responsible disclosure. --- -# Security Reviewer +# Security Review -A defensive security review on behalf of the code owner: identify vulnerabilities and attack vectors in source code and documentation, and produce a clear, actionable responsible-disclosure issue that remediates each finding. A defender's mindset, not an attacker's — and no working exploit code. +Security Review is a specialized path in the canonical [Workflow](../Workflow.md). On behalf of the code owner, it identifies vulnerabilities and attack vectors in source code and documentation and produces a clear, actionable responsible-disclosure issue for each finding. It uses a defender's mindset, not an attacker's, and produces no working exploit code. -## When to use +## Enter this stage when Perform a security review, audit code for vulnerabilities, threat-model a change, or review OWASP Top 10 risks — injection, secrets exposure, path traversal, privilege escalation, supply-chain risk, information disclosure. @@ -43,5 +43,5 @@ After the review, open a security issue on the target repository titled `[Securi ## Where this connects -- [Review Etiquette](../Ways-of-Working/Review-Etiquette.md) — how findings are communicated. -- [Security](../Coding-Standards/Security.md) and [GitHub Actions](../Coding-Standards/GitHub-Actions.md) — the standards a finding cites. +- [Review Etiquette](../Review-Etiquette.md) — how findings are communicated. +- [Security](../../Coding-Standards/Security.md) and [GitHub Actions](../../Coding-Standards/GitHub-Actions.md) — the standards a finding cites. diff --git a/src/docs/Ways-of-Working/Workflow-Stages/index.md b/src/docs/Ways-of-Working/Workflow-Stages/index.md new file mode 100644 index 0000000..9615069 --- /dev/null +++ b/src/docs/Ways-of-Working/Workflow-Stages/index.md @@ -0,0 +1,26 @@ +--- +title: Workflow Stages +description: Procedures for each stage of the canonical Workflow, reached through indexes or direct task-language shortcuts. +--- + +# Workflow Stages + +The canonical [Workflow](../Workflow.md) owns the process, stage routing, and transitions. This section contains only the procedure for each stage: its entry condition, boundary, actions, and handoff. Humans and agents follow the same pages. + +The default discovery trail is [Home](../../index.md) → [Ways of Working](../index.md) → [Workflow](../Workflow.md) → this stage index → the resolved procedure. A task that clearly names its action may use the [Workflow routing table](../Workflow.md#find-the-current-stage) as a shortcut. + +No stage needs its own skill, command, named agent, or tool-specific process file. An integration may link directly to a stage for convenience, but the documentation remains the only process definition. [Maintain Workflow Guidance](Maintain-Guidance.md) keeps these procedures and thin repository pointers aligned. + +## Procedures + + + +| Page | Description | +| --- | --- | +| [Define](Define.md) | Procedure for the Workflow stage that captures, routes, refines, and plans work at the correct issue altitude. | +| [Implement](Implement.md) | Procedure for the Workflow stage that delivers one ready Task or Bug as a review-ready pull request. | +| [Review](Review.md) | Procedure for the Workflow stage that independently reviews a pull request for delivery, taste, security, and decisions. | +| [Security Review](Security-Review.md) | Procedure for the specialized Workflow stage for defensive security review and responsible disclosure. | +| [Maintain Workflow Guidance](Maintain-Guidance.md) | Maintain the canonical Workflow stage procedures and the thin repository pointers that lead to them. | + + diff --git a/src/docs/Ways-of-Working/Workflow.md b/src/docs/Ways-of-Working/Workflow.md index eb7f0d3..d80a38e 100644 --- a/src/docs/Ways-of-Working/Workflow.md +++ b/src/docs/Ways-of-Working/Workflow.md @@ -1,6 +1,6 @@ --- title: Workflow -description: How work flows from idea to delivery and back again. +description: The canonical process from idea to delivery, including how to resolve and enter each workflow stage. --- # Workflow @@ -26,9 +26,9 @@ flowchart TD subgraph CM["Context maintenance"] Cap[Capture] --> Ref[Refine] --> Pl[Plan] - Pl --> T[simple task] - Pl --> S[sub-issues] - Pl --> CL[checklist] + Pl --> Agg["Epic / PBI
decompose and aggregate"] + Agg --> Pl + Pl --> Leaf["Ready Task / Bug
delivery leaf"] end subgraph SD["Software delivery"] @@ -38,15 +38,27 @@ flowchart TD Rsp --> Rev end - T --> Bld - S --> Bld - CL --> Bld + Leaf --> Bld Rev --> Ops["Run and operate (DevOps + SRE loop)"] Ops --> Sig["Signals, errors, feedback"] Sig --> Cap Ops -->|new release observed| Ext ``` +## Find the current stage + +Start from this page after following the documentation indexes: [Home](../index.md) → [Ways of Working](index.md) → Workflow. Infer the current stage from the work and its artifacts, then use the [stage procedure index](Workflow-Stages/index.md) or the direct links below. A stage is a part of this workflow, not a separate agent, skill, or instruction set. + +| Current work or prompt | Enter | Continue until | +| --- | --- | --- | +| A request, signal, or issue needs to be captured, routed, refined, or planned. Prompts such as `Make this issue ` route here. | [Define](Workflow-Stages/Define.md) | The issue meets the readiness gate for its altitude. | +| A ready Task or Bug needs implementation, or a pull request needs author-side changes. Prompts such as `Implement ` or `Fix ` route here. | [Implement](Workflow-Stages/Implement.md) | The pull request meets the ready-for-review gate. | +| A pull request needs an independent assessment. Prompts such as `Review this PR ` route here. | [Review](Workflow-Stages/Review.md) | The change is approved or actionable feedback returns it to Implement. | +| The requested assessment is explicitly security-focused. Prompts such as `Security review ` route here. | [Security Review](Workflow-Stages/Security-Review.md) | Findings are reported through the agreed channel and the review returns to its caller. | +| The workflow pages or repository pointers themselves need to change. | [Maintain Workflow Guidance](Workflow-Stages/Maintain-Guidance.md) | The canonical docs and thin pointers agree. | + +Keywords are shortcuts, not a second routing system. When a prompt is ambiguous, use the current issue, pull request, branch, and completed work to identify the stage. When work crosses a stage boundary, follow the handoff and load the next procedure rather than carrying separate process instructions through the whole task. + ## Phases ### Capture @@ -61,7 +73,7 @@ A desire for change enters the system. It can come from anywhere: The goal is to **write it down** — quickly, in a GitHub issue — so it exists for the world to see and "remember". At this stage, precision is less important than existence. The issue captures the current state, the pain or opportunity, and the desired outcome. -See [Issue Format § Section 1](Issues/Process/Format.md) for structure. +See the [Issue Lifecycle](Issues/Process/Lifecycle.md) for how the issue evolves during Capture. ### Refine @@ -82,32 +94,34 @@ The goal is **shared understanding** — everyone (humans and agents) agrees on Turn the refined understanding into actionable work: - **Gap analysis** — diff the [evergreen specification](Documentation-Model.md#evergreen-and-evolutionary) for the affected capability against the current implementation. The gap is the work. -- **Decisions** — what approach will we take? What trade-offs are we making? Document them in the issue. -- **Decomposition** — if the work is large, break it into sub-issues. Each sub-issue should be deliverable in a single pull request. -- **Checklist** — for a single task, list the concrete steps in the issue body. +- **Route and decide** — assign the native type and add decisions at its [planning altitude](Issues/Process/Planning.md). +- **Decompose aggregates** — use native sub-issues for Epic and PBI children, and native dependency edges only where execution is genuinely gated. +- **Prepare delivery leaves** — refine Task and Bug children until they satisfy their type-specific readiness gate. -The plan is the contract. It drives implementation. +The issue graph is the delivery plan. Only a ready Task or Bug is eligible for Build; Epic and PBI remain in Plan while their children deliver and their aggregate criteria stay current. -See [Documentation Model](Documentation-Model.md), [Issue Format § Sections 2–3](Issues/Process/Format.md), [Issue Hierarchy](Issues/Types/Hierarchy.md). +See [Documentation Model](Documentation-Model.md), [Issue Planning](Issues/Process/Planning.md), [Issue Relationships](Issues/Process/Relationships.md), and [Issue Hierarchy](Issues/Types/Hierarchy.md). ### Build -Execute the plan: +Execute one ready, unblocked Task or Bug. For repository delivery: -1. **Branch** — create a branch (and [worktree](Git-Worktrees.md)) for the issue. -2. **Draft PR** — push early and open a draft pull request. Link it to the issue. This makes progress visible and attaches CI from the start. -3. **Implement** — work through the checklist. One logical change per commit. Update the issue as each task completes. +1. **Branch** — create a branch (and [worktree](Git-Worktrees.md)) for the delivery leaf. +2. **Draft PR** — push early and open a draft pull request that closes exactly that Task or Bug. This makes progress visible and attaches CI from the start. +3. **Implement** — work through the implementation plan. One logical change per commit. Update the issue as each plan step completes. 4. **Test locally** — don't push known failures to CI. Push work as far inward as it can go. 5. **Self-review with automation** — run the [Copilot review loop](Contribution-Workflow.md#the-copilot-review-loop) until it reports a clean round, fixing in-scope feedback and filing follow-up issues for the rest. 6. **Ready and auto-merge** — when the change meets the [Definition of Ready for Review](Definition-of-Ready-and-Done.md#definition-of-ready-for-review), finalize the pull request per [PR Format](PR-Format.md), mark it ready, and enable auto-merge. +An audited operational Task follows its [operational completion path](Issues/Types/Task.md#operational-delivery) instead of creating a branch or pull request. + See [Commit Conventions](Commit-Conventions.md), [PR Format](PR-Format.md), [Contribution Workflow](Contribution-Workflow.md). ### Review Every change gets a second perspective: -- Does the PR deliver what the linked issue asks for? +- Does the PR deliver what its closing Task or Bug asks for? - Does it follow the project's standards and conventions? - Are there security concerns or undiscussed decisions? - Is the code clear and maintainable? @@ -118,11 +132,11 @@ See [Review Etiquette](Review-Etiquette.md). ### Ship -Human review approves the ready pull request and the required checks stay green, so auto-merge lands the change — squash-merged into the protected branch, its branch deleted. Where the project releases from the trunk, the merge cuts the release. +Human review approves the ready pull request and the required checks stay green, so auto-merge lands the change — squash-merged into the protected branch, its branch deleted — and closes its Task or Bug. Where the project releases from the trunk, the merge cuts the release. The pull request description becomes the release note. Write it for end users, not reviewers. -See [PR Format](PR-Format.md), [Branching and Merging](Branching-and-Merging.md#required-checks-and-auto-merge). +Parent PBI and Epic issues close separately when their native children and aggregate acceptance criteria are complete. See [PR Format](PR-Format.md), [Issue Lifecycle](Issues/Process/Lifecycle.md), and [Branching and Merging](Branching-and-Merging.md#required-checks-and-auto-merge). ### Operate @@ -146,7 +160,7 @@ Planning happens at different time horizons and levels of detail: Detail increases as work moves from Later toward Now. - A single task lives in **Now / Detailed**. -- A product backlog item lives between **Now / Logical** and **Next / Detailed**. -- An epic spans **Now → Next → Later** at **Conceptual / Logical** fidelity. +- A PBI lives between **Now / Logical** and **Next / Detailed**. +- An Epic spans **Now → Next → Later** at **Conceptual / Logical** fidelity. This workflow follows the [Human–agent coexistence](Principles/AI-First-Development.md#human-agent-coexistence) principle — it is designed for humans first, with agents joining the same process rather than running a parallel one. diff --git a/src/docs/Ways-of-Working/index.md b/src/docs/Ways-of-Working/index.md index c6cfff5..2165221 100644 --- a/src/docs/Ways-of-Working/index.md +++ b/src/docs/Ways-of-Working/index.md @@ -15,9 +15,10 @@ This section documents the principles, processes, and norms that every contribut | Page | Description | | --- | --- | -| [Workflow](Workflow.md) | How work flows from idea to delivery and back again. | +| [Workflow](Workflow.md) | The canonical process from idea to delivery, including how to resolve and enter each workflow stage. | +| [Workflow Stages](Workflow-Stages/index.md) | Procedures for each stage of the canonical Workflow, reached through indexes or direct task-language shortcuts. | | [Contribution Workflow](Contribution-Workflow.md) | How a change travels from a branch to a review-ready pull request — draft first, the Copilot review loop, then people. | -| [Fleet Orchestration](Fleet-Orchestration.md) | How one change is rolled out across many repositories — a campaign of branches, pull requests, and review loops tracked entirely on GitHub. | +| [Fleet Orchestration](Fleet-Orchestration.md) | How one change is rolled out across many repositories through Task or Bug delivery leaves tracked entirely on GitHub. | | [Documentation Model](Documentation-Model.md) | How every capability is documented — a spec for the why and a design for the how, colocated, concise, and kept evergreen for humans and agents alike. | | [Spec-Driven Development](Spec-Driven-Development.md) | The specification is the source of truth — the spec (why and what), its design (how), and how a change moves from need to shipped. | | [Evolutionary Development](Evolutionary-Development.md) | Grow software as bets under selection — variation, feedback, and survival of the fittest, run as one tight loop. | @@ -28,12 +29,12 @@ This section documents the principles, processes, and norms that every contribut | [Repository Type Property](Repository-Type-Property.md) | How a single "Type" custom property classifies every repository in an initiative organization and drives which org-wide controls apply to it. | | [Principles](Principles/index.md) | The foundational beliefs and product mindset behind every decision. | | [Engineering Taste](Engineering-Taste.md) | The judgment that takes over when the standards run out. | -| [Goal-Setting Framework](Goal-Setting.md) | Mission, OKRs, and initiatives — strategy connected to delivery. | +| [Goal-Setting Framework](Goal-Setting.md) | Mission, OKRs, and Initiatives — the strategy above repository Epics. | | [Definition of Ready and Done](Definition-of-Ready-and-Done.md) | The three gates that bracket every piece of work. | | [Issues](Issues/index.md) | How issues are formatted and organized across the MSX ecosystem. | | [PR Format](PR-Format.md) | Pull request title, description, change types, and labels. | | [Commit Conventions](Commit-Conventions.md) | How commit messages are written. | -| [Branching and Merging](Branching-and-Merging.md) | Topic branches, pull-request-only integration, and merge models. | +| [Branching and Merging](Branching-and-Merging.md) | Delivery-leaf topic branches, pull-request-only integration, and merge models. | | [Review Etiquette](Review-Etiquette.md) | Tone, scope, severity, and how to disagree well. | | [Repository Segmentation](Repository-Segmentation.md) | What belongs in a repository, and when to split or combine. | | [README-Driven Context](Readme-Driven-Context.md) | Why the README is the front door and the source of truth. | diff --git a/src/docs/index.md b/src/docs/index.md index c5259c7..04266a1 100644 --- a/src/docs/index.md +++ b/src/docs/index.md @@ -47,7 +47,6 @@ Each area below carries its own index. Start here, read the descriptions, and fo | [Vision](Vision/index.md) | The mission and the philosophy of easy, fast, and safe. | | [Initiatives](Initiatives/index.md) | The products that make the vision real — PSModule, AzActions, TFActions, and the tools built along the way. | | [Ways of Working](Ways-of-Working/index.md) | How work happens across the ecosystem — for humans and agents alike. | -| [Agents](Agents/index.md) | The roles agents play across the ecosystem — authored once as documentation and pointed to from each repository. | | [Coding Standards](Coding-Standards/index.md) | The shared baseline every repository inherits, and the per-language standards that build on it. | | [Capabilities](Capabilities/index.md) | The capabilities the ecosystem builds, each documented by a spec (why and what) and a design (how and what). | | [Frameworks](Frameworks/index.md) | The complete, end-to-end frameworks the ecosystem ships — opinionated automation a project adopts wholesale to go from source to shipped. | diff --git a/src/zensical.toml b/src/zensical.toml index fdc6ac3..74b66c3 100644 --- a/src/zensical.toml +++ b/src/zensical.toml @@ -25,7 +25,15 @@ nav = [ ]}, {"Ways of Working" = [ "Ways-of-Working/index.md", - {"Workflow" = "Ways-of-Working/Workflow.md"}, + {"Workflow" = [ + "Ways-of-Working/Workflow.md", + "Ways-of-Working/Workflow-Stages/index.md", + {"Define" = "Ways-of-Working/Workflow-Stages/Define.md"}, + {"Implement" = "Ways-of-Working/Workflow-Stages/Implement.md"}, + {"Review" = "Ways-of-Working/Workflow-Stages/Review.md"}, + {"Security Review" = "Ways-of-Working/Workflow-Stages/Security-Review.md"}, + {"Maintain Workflow Guidance" = "Ways-of-Working/Workflow-Stages/Maintain-Guidance.md"}, + ]}, {"Contribution Workflow" = "Ways-of-Working/Contribution-Workflow.md"}, {"Fleet Orchestration" = "Ways-of-Working/Fleet-Orchestration.md"}, {"Documentation Model" = "Ways-of-Working/Documentation-Model.md"}, @@ -77,14 +85,6 @@ nav = [ {"Continuous Practices" = "Ways-of-Working/Continuous-Practices.md"}, {"DevOps Reference" = "Ways-of-Working/DevOps-Reference.md"}, ]}, - {"Agents" = [ - "Agents/index.md", - {"Define" = "Agents/define.md"}, - {"Implement" = "Agents/implement.md"}, - {"Reviewer" = "Agents/reviewer.md"}, - {"Security Reviewer" = "Agents/security-reviewer.md"}, - {"Agent Author" = "Agents/agent-author.md"}, - ]}, {"Coding Standards" = [ "Coding-Standards/index.md", {"Naming" = "Coding-Standards/Naming.md"}, diff --git a/tests/Initialize-MsxWorkspace.Tests.ps1 b/tests/Initialize-MsxWorkspace.Tests.ps1 new file mode 100644 index 0000000..65393f1 --- /dev/null +++ b/tests/Initialize-MsxWorkspace.Tests.ps1 @@ -0,0 +1,785 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +Describe 'Initialize-MsxWorkspace context freshness' { + BeforeAll { + $script:bootstrap = Join-Path $PSScriptRoot '../bootstrap/Initialize-MsxWorkspace.ps1' + $script:agentTemplate = Join-Path $PSScriptRoot '../bootstrap/AGENTS.template.md' + $script:bootstrapReadme = Join-Path $PSScriptRoot '../bootstrap/README.md' + $script:pwsh = (Get-Process -Id $PID).Path + + function Invoke-Git { + param( + [Parameter()] + [string] $WorkingDirectory, + + [Parameter(Mandatory)] + [string[]] $Arguments + ) + + $output = if ($WorkingDirectory) { + & git -C $WorkingDirectory @Arguments 2>&1 + } else { + & git @Arguments 2>&1 + } + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed in '$WorkingDirectory': $($output | Out-String)" + } + return $output + } + + function Add-TestCommit { + param( + [Parameter(Mandatory)] + [string] $Repository, + + [Parameter(Mandatory)] + [string] $Name + ) + + Add-Content -LiteralPath (Join-Path $Repository 'context.txt') -Value $Name + Invoke-Git -WorkingDirectory $Repository -Arguments @('add', 'context.txt') | Out-Null + Invoke-Git -WorkingDirectory $Repository -Arguments @('commit', '--quiet', '-m', $Name) | Out-Null + } + + function New-ContextFixture { + [CmdletBinding(SupportsShouldProcess)] + param() + + $root = Join-Path ([IO.Path]::GetTempPath()) "msx-bootstrap-$([guid]::NewGuid().ToString('N'))" + if (-not $PSCmdlet.ShouldProcess($root, 'Create context fixture')) { + return + } + $workspace = Join-Path $root 'workspace' + $remotes = Join-Path $root 'remotes' + $writers = Join-Path $root 'writers' + New-Item -ItemType Directory -Path $workspace, $remotes, $writers -Force | Out-Null + + $writerMap = @{} + $remoteMap = @{} + foreach ($name in @('docs', 'memory')) { + $remote = Join-Path $remotes "$name.git" + $writer = Join-Path $writers $name + $checkout = Join-Path $workspace $name + + Invoke-Git -Arguments @('init', '--bare', '--quiet', '--initial-branch=main', $remote) | Out-Null + Invoke-Git -Arguments @('clone', '--quiet', $remote, $writer) | Out-Null + Invoke-Git -WorkingDirectory $writer -Arguments @('config', 'user.name', 'Fixture Writer') | Out-Null + Invoke-Git -WorkingDirectory $writer -Arguments @('config', 'user.email', 'fixture@example.invalid') | Out-Null + Set-Content -LiteralPath (Join-Path $writer 'context.txt') -Value "$name context" + if ($name -eq 'docs') { + $bootstrapDirectory = Join-Path $writer 'bootstrap' + New-Item -ItemType Directory -Path $bootstrapDirectory | Out-Null + Copy-Item -LiteralPath $script:bootstrap -Destination $bootstrapDirectory + } + Invoke-Git -WorkingDirectory $writer -Arguments @('add', 'context.txt') | Out-Null + if ($name -eq 'docs') { + Invoke-Git -WorkingDirectory $writer -Arguments @('add', 'bootstrap/Initialize-MsxWorkspace.ps1') | Out-Null + } + Invoke-Git -WorkingDirectory $writer -Arguments @('commit', '--quiet', '-m', "Initialize $name") | Out-Null + Invoke-Git -WorkingDirectory $writer -Arguments @('push', '--quiet', '--set-upstream', 'origin', 'main') | Out-Null + Invoke-Git -Arguments @('clone', '--quiet', $remote, $checkout) | Out-Null + Invoke-Git -WorkingDirectory $checkout -Arguments @('config', 'user.name', 'Fixture Local') | Out-Null + Invoke-Git -WorkingDirectory $checkout -Arguments @('config', 'user.email', 'fixture-local@example.invalid') | Out-Null + $writerMap[$name] = $writer + $remoteMap[$name] = $remote + } + + return [pscustomobject]@{ + Root = $root + Workspace = $workspace + Writers = $writerMap + Remotes = $remoteMap + Docs = Join-Path $workspace 'docs' + Memory = Join-Path $workspace 'memory' + } + } + + function Invoke-BootstrapFixture { + param([Parameter(Mandatory)] $Fixture) + + $runner = Join-Path $Fixture.Root 'invoke-bootstrap.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $Fixture.Workspace.Replace("'", "''") + $docsRemote = $Fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $Fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'Fixture' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + return [pscustomobject]@{ + ExitCode = $LASTEXITCODE + Output = $output + } + } + + function Invoke-BootstrapSeed { + param( + [Parameter(Mandatory)] + $Fixture, + + [Parameter(Mandatory)] + [string] $MarkdownPath, + + [Parameter(Mandatory)] + [string] $Workspace + ) + + $markdown = Get-Content -LiteralPath $MarkdownPath -Raw + $match = [regex]::Match($markdown, '(?s)```powershell\r?\n(.*?)\r?\n```') + if (-not $match.Success) { + throw "No PowerShell seed block found in '$MarkdownPath'." + } + $runner = Join-Path $Fixture.Root "seed-$([IO.Path]::GetFileNameWithoutExtension($MarkdownPath)).ps1" + Set-Content -LiteralPath $runner -Value $match.Groups[1].Value + $previousRoot = $env:MSX_WORKSPACE_ROOT + $previousDocs = $env:MSX_DOCS_URL + $previousMemory = $env:MSX_MEMORY_URL + try { + $env:MSX_WORKSPACE_ROOT = $Workspace + $env:MSX_DOCS_URL = $Fixture.Remotes.docs + $env:MSX_MEMORY_URL = $Fixture.Remotes.memory + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + return [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $output } + } finally { + $env:MSX_WORKSPACE_ROOT = $previousRoot + $env:MSX_DOCS_URL = $previousDocs + $env:MSX_MEMORY_URL = $previousMemory + } + } + } + + BeforeEach { + $fixture = New-ContextFixture + $fixture | Should -Not -BeNullOrEmpty + } + + AfterEach { + if ($fixture -and (Test-Path -LiteralPath $fixture.Root)) { + Remove-Item -LiteralPath $fixture.Root -Recurse -Force + } + } + + It 'fast-forwards a clean behind checkout to the exact remote head' { + Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('branch', 'local-topic') | Out-Null + Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('tag', 'local-tag') | Out-Null + $topicHead = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'local-topic')).Trim() + $tagHead = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'local-tag')).Trim() + Add-TestCommit -Repository $fixture.Writers.docs -Name 'Advance docs' + Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('push', '--quiet') | Out-Null + Add-TestCommit -Repository $fixture.Writers.memory -Name 'Advance memory' + Invoke-Git -WorkingDirectory $fixture.Writers.memory -Arguments @('push', '--quiet') | Out-Null + $docsHead = (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + $memoryHead = (Invoke-Git -WorkingDirectory $fixture.Writers.memory -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Be 0 -Because $result.Output + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $docsHead + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'origin/main')).Trim() | Should -BeExactly $docsHead + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $memoryHead + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'origin/main')).Trim() | Should -BeExactly $memoryHead + Test-Path -LiteralPath (Join-Path $fixture.Docs '.git') -PathType Leaf | Should -BeTrue + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') -PathType Container | Should -BeTrue + (Invoke-Git -Arguments @("--git-dir=$(Join-Path $fixture.Workspace 'docs.git')", 'rev-parse', '--is-bare-repository')).Trim() | + Should -BeExactly 'true' + (Invoke-Git -Arguments @("--git-dir=$(Join-Path $fixture.Workspace 'docs.git')", 'rev-parse', 'local-topic')).Trim() | + Should -BeExactly $topicHead + (Invoke-Git -Arguments @("--git-dir=$(Join-Path $fixture.Workspace 'docs.git')", 'rev-parse', 'local-tag')).Trim() | + Should -BeExactly $tagHead + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.simple-clone-backup') | Should -BeTrue + Test-Path -LiteralPath (Join-Path $fixture.Memory '.git') -PathType Container | Should -BeTrue + (Invoke-BootstrapFixture -Fixture $fixture).ExitCode | Should -Be 0 + } + + It 'rejects a dirty checkout without updating it' { + Set-Content -LiteralPath (Join-Path $fixture.Docs 'dirty.txt') -Value 'uncommitted' + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'uncommitted changes' + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $before + } + + It 'rejects a locally ahead checkout without stale fallback' { + Add-TestCommit -Repository $fixture.Docs -Name 'Local only' + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'not exactly synchronized' + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $before + } + + It 'rejects diverged history without updating the checkout' { + Add-TestCommit -Repository $fixture.Docs -Name 'Local divergence' + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + Add-TestCommit -Repository $fixture.Writers.docs -Name 'Remote divergence' + Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('push', '--quiet') | Out-Null + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'Could not fast-forward' + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $before + } + + It 'rejects a checkout on a non-default branch' { + Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('switch', '--quiet', '-c', 'topic') | Out-Null + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match "not the default branch 'main'" + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('branch', '--show-current')).Trim() | Should -BeExactly 'topic' + } + + It 'rejects an unreachable remote without using local context' { + $missing = Join-Path $fixture.Root 'missing.git' + Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('remote', 'set-url', 'origin', $missing) | Out-Null + $fixture.Remotes.docs = $missing + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'git fetch failed' + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $before + } + + It 'rejects a non-canonical origin before fetching context' { + Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @( + 'remote', + 'set-url', + 'origin', + $fixture.Remotes.memory + ) | Out-Null + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'not canonical' + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $before + } + + It 'rejects a non-canonical bare docs origin before mutation' { + (Invoke-BootstrapFixture -Fixture $fixture).ExitCode | Should -Be 0 + $backing = Join-Path $fixture.Workspace 'docs.git' + Invoke-Git -Arguments @( + "--git-dir=$backing", + 'remote', + 'set-url', + 'origin', + $fixture.Remotes.memory + ) | Out-Null + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'not canonical' + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $before + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + } + + It 'rejects a non-canonical memory origin before docs migration' { + Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @( + 'remote', + 'set-url', + 'origin', + $fixture.Remotes.docs + ) | Out-Null + $docsBefore = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + $memoryBefore = (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'not canonical' + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') | Should -BeFalse + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $docsBefore + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $memoryBefore + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + } + + It 'rejects a memory worktree before docs migration' { + Remove-Item -LiteralPath $fixture.Memory -Recurse -Force + $memoryBacking = Join-Path $fixture.Root 'memory-backing.git' + Invoke-Git -Arguments @('clone', '--bare', '--quiet', $fixture.Remotes.memory, $memoryBacking) | Out-Null + Invoke-Git -Arguments @( + "--git-dir=$memoryBacking", + 'worktree', + 'add', + '--quiet', + $fixture.Memory, + 'main' + ) | Out-Null + $docsBefore = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + $memoryBefore = (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Match 'memory requires a simple checkout' + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') | Should -BeFalse + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $docsBefore + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $memoryBefore + } + + It 'installs additional project context through plug-in coordinates' { + $runner = Join-Path $fixture.Root 'invoke-project-bootstrap.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } + @{ + Name = 'Project' + Path = './projects/Project/' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Be 0 -Because $output + $projectDocs = Join-Path $fixture.Workspace 'projects/Project/docs' + $projectMemory = Join-Path $fixture.Workspace 'projects/Project/memory' + Test-Path -LiteralPath (Join-Path $projectDocs '.git') | Should -BeTrue + Test-Path -LiteralPath (Join-Path $projectMemory '.git') | Should -BeTrue + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'projects/Project/docs.git') | Should -BeTrue + (Invoke-Git -Arguments @( + "--git-dir=$(Join-Path $fixture.Workspace 'projects/Project/docs.git')", + 'rev-parse', + '--is-bare-repository' + )).Trim() | Should -BeExactly 'true' + (Invoke-Git -WorkingDirectory $projectDocs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + (Invoke-Git -WorkingDirectory $projectMemory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.memory -Arguments @('rev-parse', 'HEAD')).Trim() + } + + It 'rejects duplicate project paths after normalization' { + $runner = Join-Path $fixture.Root 'invoke-duplicate-bootstrap.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'One' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } + @{ + Name = 'Two' + Path = '.' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Not -Be 0 + $output | Should -Match 'workspace paths overlap' + } + + It 'rejects duplicate project names before mutation' -ForEach @( + @{ SecondPath = '' } + @{ SecondPath = 'docs' } + ) { + $beforeDocs = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + $beforeMemory = (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() + $runner = Join-Path $fixture.Root "invoke-duplicate-name-$($SecondPath -replace '[^A-Za-z0-9]', '-').ps1" + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'Duplicate' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } + @{ + Name = 'Duplicate' + Path = '$SecondPath' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Not -Be 0 + $output | Should -Match 'unique names' + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') | Should -BeFalse + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $beforeDocs + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $beforeMemory + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + } + + It 'rejects project paths overlapping canonical context storage' -ForEach @( + @{ UnsafePath = 'docs' } + @{ UnsafePath = 'docs.git' } + @{ UnsafePath = 'memory' } + @{ UnsafePath = 'docs/child' } + @{ UnsafePath = 'docs.git/child' } + @{ UnsafePath = 'memory/child' } + @{ UnsafePath = 'docs.simple-clone-backup' } + @{ UnsafePath = 'docs.simple-clone-backup/child' } + ) { + $beforeDocs = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + $beforeMemory = (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() + $safeName = $UnsafePath -replace '[^A-Za-z0-9-]', '-' + $runner = Join-Path $fixture.Root "invoke-overlap-$safeName.ps1" + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'MSXOrg' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } + @{ + Name = 'Unsafe' + Path = '$UnsafePath' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Not -Be 0 + $output | Should -Match 'workspace paths overlap' + $unsafeRoot = Join-Path $fixture.Workspace $UnsafePath + foreach ($child in @('docs', 'docs.git', 'memory', 'docs.simple-clone-backup')) { + Test-Path -LiteralPath (Join-Path $unsafeRoot $child) | Should -BeFalse + } + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $beforeDocs + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $beforeMemory + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + (Invoke-Git -WorkingDirectory $fixture.Memory -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + } + + It 'rejects overlapping non-empty project roots before mutation' { + $runner = Join-Path $fixture.Root 'invoke-overlapping-roots.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'Parent' + Path = 'projects/Parent' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } + @{ + Name = 'Child' + Path = 'projects/Parent/Child' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Not -Be 0 + $output | Should -Match 'workspace paths overlap' + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'projects') | Should -BeFalse + } + + It 'reuses a canonical docs worktree with a legacy bare backing path' { + Remove-Item -LiteralPath $fixture.Docs -Recurse -Force + $legacyBacking = Join-Path $fixture.Root 'legacy-docs-backing.git' + Invoke-Git -Arguments @('clone', '--bare', '--quiet', $fixture.Remotes.docs, $legacyBacking) | Out-Null + Invoke-Git -Arguments @( + "--git-dir=$legacyBacking", + 'config', + '--add', + 'remote.origin.fetch', + '+refs/heads/*:refs/remotes/origin/*' + ) | Out-Null + Invoke-Git -Arguments @("--git-dir=$legacyBacking", 'fetch', '--quiet', 'origin') | Out-Null + Invoke-Git -Arguments @("--git-dir=$legacyBacking", 'worktree', 'add', '--quiet', $fixture.Docs, 'main') | Out-Null + + $result = Invoke-BootstrapFixture -Fixture $fixture + + $result.ExitCode | Should -Be 0 -Because $result.Output + $commonDir = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @( + 'rev-parse', + '--path-format=absolute', + '--git-common-dir' + )).Trim() + [IO.Path]::GetFullPath($commonDir) | Should -BeExactly ([IO.Path]::GetFullPath($legacyBacking)) + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') | Should -BeFalse + } + + It 'installs missing docs as bare backing plus main worktree and memory as a simple clone' { + $emptyRoot = Join-Path $fixture.Root 'empty-workspace' + $runner = Join-Path $fixture.Root 'invoke-empty-bootstrap.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'Fixture' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$emptyRoot' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Be 0 -Because $output + $docs = Join-Path $emptyRoot 'docs' + $backing = Join-Path $emptyRoot 'docs.git' + $memory = Join-Path $emptyRoot 'memory' + Test-Path -LiteralPath (Join-Path $docs '.git') -PathType Leaf | Should -BeTrue + (Invoke-Git -Arguments @("--git-dir=$backing", 'rev-parse', '--is-bare-repository')).Trim() | + Should -BeExactly 'true' + (Invoke-Git -WorkingDirectory $docs -Arguments @('branch', '--show-current')).Trim() | Should -BeExactly 'main' + (Invoke-Git -WorkingDirectory $docs -Arguments @('status', '--porcelain')) | Should -BeNullOrEmpty + (Invoke-Git -WorkingDirectory $docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + Test-Path -LiteralPath (Join-Path $memory '.git') -PathType Container | + Should -BeTrue -Because $output + (Invoke-Git -WorkingDirectory $memory -Arguments @('branch', '--show-current')).Trim() | + Should -BeExactly 'main' + (Invoke-Git -WorkingDirectory $memory -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + (Invoke-Git -WorkingDirectory $memory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.memory -Arguments @('rev-parse', 'HEAD')).Trim() + foreach ($repository in @($docs, $memory)) { + (Invoke-Git -WorkingDirectory $repository -Arguments @('config', '--local', 'user.name')).Trim() | + Should -BeExactly 'Fixture User' + (Invoke-Git -WorkingDirectory $repository -Arguments @('config', '--local', 'user.email')).Trim() | + Should -BeExactly 'fixture@example.invalid' + } + $emptyFixture = [pscustomobject]@{ + Root = $fixture.Root + Workspace = $emptyRoot + Remotes = $fixture.Remotes + } + (Invoke-BootstrapFixture -Fixture $emptyFixture).ExitCode | Should -Be 0 + (Invoke-Git -WorkingDirectory $docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + (Invoke-Git -WorkingDirectory $memory -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.memory -Arguments @('rev-parse', 'HEAD')).Trim() + } + + It 'fast-forwards an existing bare backing before creating its missing main worktree' { + $emptyRoot = Join-Path $fixture.Root 'backing-only-workspace' + New-Item -ItemType Directory -Path $emptyRoot | Out-Null + $backing = Join-Path $emptyRoot 'docs.git' + Invoke-Git -Arguments @('clone', '--bare', '--quiet', $fixture.Remotes.docs, $backing) | Out-Null + Add-TestCommit -Repository $fixture.Writers.docs -Name 'Advance after bare clone' + Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('push', '--quiet') | Out-Null + $remoteHead = (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + $runner = Join-Path $fixture.Root 'invoke-backing-only-bootstrap.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$projects = @( + @{ + Name = 'Fixture' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$emptyRoot' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Be 0 -Because $output + $docs = Join-Path $emptyRoot 'docs' + (Invoke-Git -WorkingDirectory $docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $remoteHead + (Invoke-Git -Arguments @("--git-dir=$backing", 'rev-parse', 'main')).Trim() | Should -BeExactly $remoteHead + } + + It 'rolls back a post-move migration failure without changing the simple clone' { + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + $runner = Join-Path $fixture.Root 'invoke-failed-migration.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$env:MSX_BOOTSTRAP_TEST_FAIL_AFTER_DOCS_MOVE = '1' +`$projects = @( + @{ + Name = 'Fixture' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Not -Be 0 + $output | Should -Match 'Migration activation failed' + Test-Path -LiteralPath (Join-Path $fixture.Docs '.git') -PathType Container | Should -BeTrue + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') | Should -BeFalse + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.simple-clone-backup') | Should -BeFalse + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $before + } + + It 'removes prepared backing when moving the simple clone fails' { + $before = (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() + $runner = Join-Path $fixture.Root 'invoke-failed-move.ps1' + $bootstrap = $script:bootstrap.Replace("'", "''") + $workspace = $fixture.Workspace.Replace("'", "''") + $docsRemote = $fixture.Remotes.docs.Replace("'", "''") + $memoryRemote = $fixture.Remotes.memory.Replace("'", "''") + @" +`$env:MSX_BOOTSTRAP_TEST_FAIL_DOCS_MOVE = '1' +`$projects = @( + @{ + Name = 'Fixture' + Path = '' + DocsUrl = '$docsRemote' + MemoryUrl = '$memoryRemote' + } +) +& '$bootstrap' -Root '$workspace' -Project `$projects -UserName 'Fixture User' -UserEmail 'fixture@example.invalid' +exit `$LASTEXITCODE +"@ | Set-Content -LiteralPath $runner + + $output = & $script:pwsh -NoProfile -File $runner 2>&1 | Out-String + + $LASTEXITCODE | Should -Not -Be 0 + $output | Should -Match 'Migration activation failed' + Test-Path -LiteralPath (Join-Path $fixture.Docs '.git') -PathType Container | Should -BeTrue + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.git') | Should -BeFalse + Test-Path -LiteralPath (Join-Path $fixture.Workspace 'docs.simple-clone-backup') | Should -BeFalse + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly $before + (Invoke-Git -WorkingDirectory $fixture.Docs -Arguments @('status', '--porcelain')) | + Should -BeNullOrEmpty + } + + It 'installs canonical topology from the seed block' -ForEach @( + @{ Name = 'agent template'; MarkdownPath = '../bootstrap/AGENTS.template.md' } + @{ Name = 'bootstrap README'; MarkdownPath = '../bootstrap/README.md' } + ) { + $workspace = Join-Path $fixture.Root "seed-$($Name.Replace(' ', '-'))" + $seedPath = Join-Path $PSScriptRoot $MarkdownPath + + $result = Invoke-BootstrapSeed -Fixture $fixture -MarkdownPath $seedPath -Workspace $workspace + + $result.ExitCode | Should -Be 0 -Because $result.Output + $docs = Join-Path $workspace 'docs' + $backing = Join-Path $workspace 'docs.git' + $memory = Join-Path $workspace 'memory' + Test-Path -LiteralPath (Join-Path $docs '.git') -PathType Leaf | Should -BeTrue + (Invoke-Git -Arguments @("--git-dir=$backing", 'rev-parse', '--is-bare-repository')).Trim() | + Should -BeExactly 'true' + (Invoke-Git -WorkingDirectory $docs -Arguments @('branch', '--show-current')).Trim() | Should -BeExactly 'main' + (Invoke-Git -WorkingDirectory $docs -Arguments @('status', '--porcelain')) | Should -BeNullOrEmpty + (Invoke-Git -WorkingDirectory $docs -Arguments @('rev-parse', 'HEAD')).Trim() | + Should -BeExactly (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + Test-Path -LiteralPath (Join-Path $memory '.git') -PathType Container | + Should -BeTrue -Because $result.Output + (Invoke-Git -WorkingDirectory $docs -Arguments @('config', '--local', 'user.name')).Trim() | + Should -BeExactly 'Marius Storhaug' + (Invoke-BootstrapSeed -Fixture $fixture -MarkdownPath $seedPath -Workspace $workspace).ExitCode | + Should -Be 0 + } + + It 'refreshes a stale bare backing before the seed creates its canonical worktree' { + $workspace = Join-Path $fixture.Root 'seed-stale-backing' + New-Item -ItemType Directory -Path $workspace | Out-Null + $backing = Join-Path $workspace 'docs.git' + Invoke-Git -Arguments @('clone', '--bare', '--quiet', $fixture.Remotes.docs, $backing) | Out-Null + Add-TestCommit -Repository $fixture.Writers.docs -Name 'Advance before seed' + Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('push', '--quiet') | Out-Null + $remoteHead = (Invoke-Git -WorkingDirectory $fixture.Writers.docs -Arguments @('rev-parse', 'HEAD')).Trim() + + $result = Invoke-BootstrapSeed -Fixture $fixture -MarkdownPath $script:agentTemplate -Workspace $workspace + + $result.ExitCode | Should -Be 0 -Because $result.Output + $docs = Join-Path $workspace 'docs' + (Invoke-Git -Arguments @("--git-dir=$backing", 'rev-parse', 'main')).Trim() | Should -BeExactly $remoteHead + (Invoke-Git -WorkingDirectory $docs -Arguments @('rev-parse', 'HEAD')).Trim() | Should -BeExactly $remoteHead + } +}