From eeb9f48f0ddfc856e1d150b8d2b0d8b81c3f077a Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 5 Jun 2026 10:12:10 -0700 Subject: [PATCH] feat(docs): adopt standard docs/ spec structure, retire tasks/ convention Replace the bespoke tasks/ durable-spec-archive convention with the industry-standard docs/ structure: - docs/specs/ -- PRDs / requirements (the *what*), written by @prd - docs/designs/ -- implementation plans (the *how*), written by @dev-loop/@plan - docs/README.md -- consumer-owned archive guide (same-name scaffold) Pull-SDLC.ai.ps1: TemplateScaffoldMap, UpstreamManagedPaths and AlwaysLocalPaths now carry docs/README.md, docs/specs/ and docs/designs/ instead of tasks/. Consolidate-Tasks.ps1 inverts direction -- it now consolidates legacy locations (tasks/*, docs/prd/*, root docs, sessions) INTO docs/specs/ and docs/designs/, writing docs/MIGRATION.md. The script filename is intentionally unchanged to avoid churning the meta-script lists. Agents (@prd, @plan, @dev-loop), README.md and .gitignore updated to the new paths. tasks/README.md moved to docs/README.md and rewritten. Closes #182 --- .github/agents/dev-loop.agent.md | 12 +- .github/agents/plan.agent.md | 22 ++-- .github/agents/prd.agent.md | 4 +- .gitignore | 10 -- Consolidate-Tasks.Tests.ps1 | 82 +++++++------ Consolidate-Tasks.ps1 | 108 ++++++++++-------- Pull-SDLC.ai.Tests.ps1 | 96 +++++++++------- Pull-SDLC.ai.ps1 | 22 +++- README.md | 10 +- docs/README.md | 90 +++++++++++++++ docs/designs/182-docs-spec-convention-plan.md | 35 ++++++ tasks/.gitkeep | 0 tasks/README.md | 79 ------------- 13 files changed, 322 insertions(+), 248 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/designs/182-docs-spec-convention-plan.md delete mode 100644 tasks/.gitkeep delete mode 100644 tasks/README.md diff --git a/.github/agents/dev-loop.agent.md b/.github/agents/dev-loop.agent.md index 9304097..a978a98 100644 --- a/.github/agents/dev-loop.agent.md +++ b/.github/agents/dev-loop.agent.md @@ -162,9 +162,9 @@ Break the approved design into bite-sized tasks (2-5 minutes each). Each task in - Exact test commands with expected output - Commit message -The file format, slug convention, and lifecycle for `tasks/--plan.md` +The file format, slug convention, and lifecycle for `docs/designs/--plan.md` are defined authoritatively in **`plan.agent.md` § Saving the Plan to -`tasks/`**. Do not duplicate that spec here. +`docs/designs/`**. Do not duplicate that spec here. `@plan` creates the file at issue-creation time with design + acceptance criteria + a skeleton implementation checklist. This phase **resumes / @@ -172,17 +172,17 @@ updates the existing file in place** -- expand each skeleton checklist item into the bite-sized tasks described above. Treat unchecked items as the next tasks to execute. -If `tasks/--prd.md` exists (written by `@prd`), ingest it as +If `docs/specs/--prd.md` exists (written by `@prd`), ingest it as authoritative requirements input alongside the GitHub issue. -If `tasks/--plan.md` does not exist (e.g., the issue was +If `docs/designs/--plan.md` does not exist (e.g., the issue was filed manually, bypassing `@plan`), create it now using the format from -`plan.agent.md` § Saving the Plan to `tasks/`. +`plan.agent.md` § Saving the Plan to `docs/designs/`. After expansion, get user approval and update the GitHub issue with the expanded task checklist. -**Exit criteria:** Plan saved/updated in `tasks/--plan.md`, +**Exit criteria:** Plan saved/updated in `docs/designs/--plan.md`, user approved, issue updated. ### Phase 3 -- TDD (Red -> Green) diff --git a/.github/agents/plan.agent.md b/.github/agents/plan.agent.md index 26656dc..f6efa45 100644 --- a/.github/agents/plan.agent.md +++ b/.github/agents/plan.agent.md @@ -35,13 +35,13 @@ MUST present it and get approval. You MUST complete these steps in order: -1. **Explore project context** — check files, docs, recent commits; **scan `tasks/`** for prior PRDs or plans on the same feature (`tasks/--prd.md`, `tasks/--plan.md`; fall back to a glob `tasks/*--*.md` when the issue number is unknown). If found, surface them and offer to refine the existing design rather than propose a brand-new one. +1. **Explore project context** — check files, docs, recent commits; **scan `docs/specs/` and `docs/designs/`** for prior PRDs or plans on the same feature (`docs/specs/--prd.md`, `docs/designs/--plan.md`; fall back to a glob `docs/specs/*--*.md` / `docs/designs/*--*.md` when the issue number is unknown). If found, surface them and offer to refine the existing design rather than propose a brand-new one. 2. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria 3. **Propose 2-3 approaches** — with trade-offs and your recommendation 4. **Present design** — in sections scaled to complexity, get user approval after each section 5. **Declare the Evidence Plan** — every plan must name the change type, the artifact format, the exact capture command, and the entry-point file the reviewer will open (see "Evidence Plan" below). The dev-loop's Phase 5b verifies the produced artifact matches this declaration. 6. **Create GitHub issue** — save the approved design as a GitHub issue (the primary output) -7. **Save the plan to `tasks/--plan.md`** — durable, in-repo artifact mirroring the issue body. Format defined in **Saving the Plan to `tasks/`** below. This is the single authoritative spec for the `tasks/--plan.md` file -- `@dev-loop` Phase 2 *resumes / expands* this file, it does not redefine the format. +7. **Save the plan to `docs/designs/--plan.md`** — durable, in-repo artifact mirroring the issue body. Format defined in **Saving the Plan to `docs/designs/`** below. This is the single authoritative spec for the `docs/designs/--plan.md` file -- `@dev-loop` Phase 2 *resumes / expands* this file, it does not redefine the format. 8. **Transition to implementation** — hand off to `@dev-loop` for the full quality cycle ## The Process @@ -137,29 +137,29 @@ For pure-internal refactors (no observable behavior change), the artifact format is `attestation` and the capture command is the test runner; the attestation markdown still serves as the entry-point file. -## Saving the Plan to `tasks/` +## Saving the Plan to `docs/designs/` -**This section is the authoritative spec for `tasks/--plan.md`.** +**This section is the authoritative spec for `docs/designs/--plan.md`.** `@dev-loop` Phase 2 references this section by name -- it does not redefine the file format. After the GitHub issue is created, save the same approved plan to a -companion file in the consumer's `tasks/` directory. The file is durable, +companion file in the consumer's `docs/designs/` directory. The file is durable, in-repo, and survives session/scratch loss; `@dev-loop` Phase 2 resumes and expands it. ### Path and Slug Convention -- Path: `tasks/--plan.md` at the repo root. +- Path: `docs/designs/--plan.md` at the repo root. - `` = the GitHub issue number; `` = a short kebab-case description. Together `-` is the shared identifier carried by the GitHub issue, the feature branch (`feat/-`), and the PR. Derive both from the current branch name. -- Look up an existing plan by the exact `tasks/--plan.md` first, - then fall back to a glob `tasks/*--plan.md`, then to a bare - `tasks/-plan.md` for legacy files predating the issue-number prefix. -- Create the `tasks/` directory if it does not yet exist. -- See `tasks/README.md` (consumer-owned) for the project's local +- Look up an existing plan by the exact `docs/designs/--plan.md` first, + then fall back to a glob `docs/designs/*--plan.md`, then to a bare + `docs/designs/-plan.md` for legacy files predating the issue-number prefix. +- Create the `docs/designs/` directory if it does not yet exist. +- See `docs/README.md` (consumer-owned) for the project's local conventions; do not contradict it. ### Required File Structure diff --git a/.github/agents/prd.agent.md b/.github/agents/prd.agent.md index f442d25..a0c4bf8 100644 --- a/.github/agents/prd.agent.md +++ b/.github/agents/prd.agent.md @@ -12,13 +12,13 @@ This agent uses the default model. No specific model override is required. Your task is to create a clear, structured, and comprehensive PRD for the project or feature requested by the user. -Save the PRD to `tasks/--prd.md` at the repo root, where `` is the GitHub issue number and `` is a short kebab-case description -- the same `-` identifier used by the feature branch (`feat/-`). Derive both from the current branch name when one exists. If no issue has been filed yet (a PRD spike), save to the bare `tasks/-prd.md` and rename it to add the `-` prefix once the issue is created. Create the `tasks/` directory first if it does not yet exist (`mkdir tasks` or `New-Item -ItemType Directory -Path tasks -Force`). If the user specifies a different location, use that instead. +Save the PRD to `docs/specs/--prd.md` at the repo root, where `` is the GitHub issue number and `` is a short kebab-case description -- the same `-` identifier used by the feature branch (`feat/-`). Derive both from the current branch name when one exists. If no issue has been filed yet (a PRD spike), save to the bare `docs/specs/-prd.md` and rename it to add the `-` prefix once the issue is created. Create the `docs/specs/` directory first if it does not yet exist (`mkdir docs/specs` or `New-Item -ItemType Directory -Path docs/specs -Force`). If the user specifies a different location, use that instead. Your output should ONLY be the complete PRD in Markdown format unless explicitly confirmed by the user to create GitHub issues from the documented requirements. ## Instructions for Creating the PRD -0. **Check for an existing PRD**: At the start of the workflow, look for an existing PRD before writing a new one. Derive `` and `` from the current branch (`feat/-`) and check `tasks/--prd.md` first. If not found, fall back to a glob `tasks/*--prd.md` (issue number differs or is unknown), then to the bare `tasks/-prd.md` for legacy files predating the issue-number prefix. If a PRD already exists for this feature, read it and offer to **update** it rather than overwrite. Surface the existing content to the user and ask whether to revise sections in place, append new sections, or start fresh. +0. **Check for an existing PRD**: At the start of the workflow, look for an existing PRD before writing a new one. Derive `` and `` from the current branch (`feat/-`) and check `docs/specs/--prd.md` first. If not found, fall back to a glob `docs/specs/*--prd.md` (issue number differs or is unknown), then to the bare `docs/specs/-prd.md` for legacy files predating the issue-number prefix. If a PRD already exists for this feature, read it and offer to **update** it rather than overwrite. Surface the existing content to the user and ask whether to revise sections in place, append new sections, or start fresh. 1. **Ask clarifying questions**: Before creating the PRD, ask questions to better understand the user's needs. diff --git a/.gitignore b/.gitignore index 6a3e230..9e004c2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,14 +23,4 @@ testResults.xml # paths list so # this entry would be pure noise in consumer .gitignore files. .dogfood-output/ - -# Upstream-only: this repo is the source of truth for the tasks/ convention -# but should never contain consumer-style spec artifacts. The tasks/ -# directory is consumer-owned everywhere downstream (see -# script:AlwaysLocalPaths in Pull-SDLC.ai.ps1); here we ship only the -# template and a .gitkeep so the directory exists post-clone. Real PRDs -# and plans should never land in this repo's tasks/ tree. -tasks/* -!tasks/README.md -!tasks/.gitkeep # <<< upstream-only <<< diff --git a/Consolidate-Tasks.Tests.ps1 b/Consolidate-Tasks.Tests.ps1 index ad5428e..44efb12 100644 --- a/Consolidate-Tasks.Tests.ps1 +++ b/Consolidate-Tasks.Tests.ps1 @@ -126,10 +126,14 @@ Describe "Invoke-ConsolidateTasks -- in-repo legacy moves" { $script:repo = Join-Path $TestDrive "consumer" if (Test-Path $script:repo) { Remove-Item -Recurse -Force $script:repo } New-FakeConsumerRepo -Root $script:repo - New-Item -ItemType Directory -Path (Join-Path $script:repo "docs/designs") -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:repo "tasks") -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $script:repo "docs/prd") -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:repo "docs/designs/2026-05-12-alpha-plan.md") -Value "PLAN-ALPHA Closes #7" + # Legacy tasks/ sources: split by -prd / -plan suffix into the new docs structure. + Set-Content -LiteralPath (Join-Path $script:repo "tasks/2026-05-12-alpha-plan.md") -Value "PLAN-ALPHA Closes #7" + Set-Content -LiteralPath (Join-Path $script:repo "tasks/gamma-prd.md") -Value "PRD-GAMMA" + # Legacy docs/prd source. Set-Content -LiteralPath (Join-Path $script:repo "docs/prd/beta-prd.md") -Value "PRD-BETA" + # Root doc source. Set-Content -LiteralPath (Join-Path $script:repo "PRD.md") -Value "ROOT-PRD" Push-Location $script:repo try { @@ -140,64 +144,66 @@ Describe "Invoke-ConsolidateTasks -- in-repo legacy moves" { } It "with -WhatIf, plans actions and writes nothing to disk" { - $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -IncludeDocsPrd -IncludeRootDocs -LinkIssues -WhatIf - $records.Count | Should -BeGreaterOrEqual 3 - ($records | Where-Object { $_.Note -eq "planned (WhatIf)" }).Count | Should -BeGreaterOrEqual 3 - Test-Path (Join-Path $script:repo "docs/designs/2026-05-12-alpha-plan.md") | Should -BeTrue + $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -IncludeDocsPrd -IncludeRootDocs -LinkIssues -WhatIf + $records.Count | Should -BeGreaterOrEqual 4 + ($records | Where-Object { $_.Note -eq "planned (WhatIf)" }).Count | Should -BeGreaterOrEqual 4 + Test-Path (Join-Path $script:repo "tasks/2026-05-12-alpha-plan.md") | Should -BeTrue Test-Path (Join-Path $script:repo "docs/prd/beta-prd.md") | Should -BeTrue Test-Path (Join-Path $script:repo "PRD.md") | Should -BeTrue - Test-Path (Join-Path $script:repo "tasks/alpha-plan.md") | Should -BeFalse - Test-Path (Join-Path $script:repo "tasks/MIGRATION.md") | Should -BeFalse + Test-Path (Join-Path $script:repo "docs/designs/alpha-plan.md") | Should -BeFalse + Test-Path (Join-Path $script:repo "docs/MIGRATION.md") | Should -BeFalse } It "with -Confirm:false, moves files via git mv (history preserved)" { - $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -IncludeDocsPrd -IncludeRootDocs -LinkIssues -Confirm:$false - $records.Count | Should -BeGreaterOrEqual 3 - Test-Path (Join-Path $script:repo "tasks/alpha-plan.md") | Should -BeTrue - Test-Path (Join-Path $script:repo "tasks/beta-prd.md") | Should -BeTrue - Test-Path (Join-Path $script:repo "tasks/legacy-prd.md") | Should -BeTrue - Test-Path (Join-Path $script:repo "docs/designs/2026-05-12-alpha-plan.md") | Should -BeFalse + $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -IncludeDocsPrd -IncludeRootDocs -LinkIssues -Confirm:$false + $records.Count | Should -BeGreaterOrEqual 4 + Test-Path (Join-Path $script:repo "docs/designs/alpha-plan.md") | Should -BeTrue + Test-Path (Join-Path $script:repo "docs/specs/gamma-prd.md") | Should -BeTrue + Test-Path (Join-Path $script:repo "docs/specs/beta-prd.md") | Should -BeTrue + Test-Path (Join-Path $script:repo "docs/specs/legacy-prd.md") | Should -BeTrue + Test-Path (Join-Path $script:repo "tasks/2026-05-12-alpha-plan.md") | Should -BeFalse Test-Path (Join-Path $script:repo "PRD.md") | Should -BeFalse Push-Location $script:repo try { $diff = & git diff --cached --name-status -M - ($diff | Where-Object { $_ -match "^R" }).Count | Should -BeGreaterOrEqual 3 + ($diff | Where-Object { $_ -match "^R" }).Count | Should -BeGreaterOrEqual 4 } finally { Pop-Location } - $manifestPath = Join-Path $script:repo "tasks/MIGRATION.md" + $manifestPath = Join-Path $script:repo "docs/MIGRATION.md" Test-Path $manifestPath | Should -BeTrue $manifest = Get-Content -LiteralPath $manifestPath -Raw - $manifest | Should -Match "# tasks/ Migration Manifest" + $manifest | Should -Match "# docs/ Migration Manifest" $manifest | Should -Match "alpha-plan.md" } - It "normalizes docs/designs date prefixes and docs/prd suffix conventions" { - $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -IncludeDocsPrd -IncludeRootDocs -LinkIssues -Confirm:$false - $records.Destination -join "`n" | Should -Match "tasks/alpha-plan.md" - $records.Destination -join "`n" | Should -Match "tasks/beta-prd.md" - $records.Destination -join "`n" | Should -Match "tasks/legacy-prd.md" + It "routes prd sources to docs/specs and plan sources to docs/designs" { + $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -IncludeDocsPrd -IncludeRootDocs -LinkIssues -Confirm:$false + $records.Destination -join "`n" | Should -Match "docs/designs/alpha-plan.md" + $records.Destination -join "`n" | Should -Match "docs/specs/gamma-prd.md" + $records.Destination -join "`n" | Should -Match "docs/specs/beta-prd.md" + $records.Destination -join "`n" | Should -Match "docs/specs/legacy-prd.md" } It "captures linked issue numbers (Closes #7) in records" { - $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -LinkIssues -Confirm:$false + $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -LinkIssues -Confirm:$false $alpha = $records | Where-Object { $_.Destination -match "alpha-plan" } $alpha.LinkedIssues | Should -Contain 7 } It "is idempotent: a second run with identical source content reports skip" { - Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -Confirm:$false | Out-Null - New-Item -ItemType Directory -Path (Join-Path $script:repo "docs/designs") -Force | Out-Null - Copy-Item -LiteralPath (Join-Path $script:repo "tasks/alpha-plan.md") -Destination (Join-Path $script:repo "docs/designs/alpha-plan.md") - $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -Confirm:$false + Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -Confirm:$false | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:repo "tasks") -Force | Out-Null + Copy-Item -LiteralPath (Join-Path $script:repo "docs/designs/alpha-plan.md") -Destination (Join-Path $script:repo "tasks/2026-05-12-alpha-plan.md") + $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -Confirm:$false $skips = $records | Where-Object { $_.Action -eq "skip" } @($skips).Count | Should -BeGreaterOrEqual 1 } It "appends a sha1 suffix when destination exists with different content" { - Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -Confirm:$false | Out-Null - New-Item -ItemType Directory -Path (Join-Path $script:repo "docs/designs") -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:repo "docs/designs/alpha-plan.md") -Value "DIFFERENT-CONTENT" - $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeDocsDesigns -Confirm:$false + Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -Confirm:$false | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:repo "tasks") -Force | Out-Null + Set-Content -LiteralPath (Join-Path $script:repo "tasks/2026-05-12-alpha-plan.md") -Value "DIFFERENT-CONTENT" + $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeLegacyTasks -Confirm:$false ($records | Where-Object { $_.Destination -match "alpha-plan-[0-9a-f]{8}\.md" }).Count | Should -BeGreaterOrEqual 1 } } @@ -223,12 +229,12 @@ Describe "Invoke-ConsolidateTasks -- copilot session sources" { Set-Content -LiteralPath (Join-Path $script:otherDir "cwd.txt") -Value "C:\some\other\repo" -NoNewline } - It "copies the matching session plan into tasks/" { + It "copies the matching session plan into docs/designs/" { $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeCopilotSessions -CopilotSessionRoot $script:sessionRoot -Confirm:$false @($records).Count | Should -Be 1 $records[0].Action | Should -Be "copy" $records[0].SourceType | Should -Be "copilot-session" - Test-Path (Join-Path $script:repo "tasks/session-abcdef12-plan.md") | Should -BeTrue + Test-Path (Join-Path $script:repo "docs/designs/session-abcdef12-plan.md") | Should -BeTrue } It "leaves the original session plan in place (copy not move)" { @@ -238,14 +244,14 @@ Describe "Invoke-ConsolidateTasks -- copilot session sources" { It "filters out sessions whose recorded repo does not match the target" { Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeCopilotSessions -CopilotSessionRoot $script:sessionRoot -Confirm:$false | Out-Null - Test-Path (Join-Path $script:repo "tasks/session-deadbeef-plan.md") | Should -BeFalse + Test-Path (Join-Path $script:repo "docs/designs/session-deadbeef-plan.md") | Should -BeFalse } It "with -WhatIf, plans copy actions and writes nothing" { $records = Invoke-ConsolidateTasks -RepoRoot $script:repo -IncludeCopilotSessions -CopilotSessionRoot $script:sessionRoot -WhatIf @($records).Count | Should -Be 1 $records[0].Note | Should -Be "planned (WhatIf)" - Test-Path (Join-Path $script:repo "tasks/session-abcdef12-plan.md") | Should -BeFalse + Test-Path (Join-Path $script:repo "docs/designs/session-abcdef12-plan.md") | Should -BeFalse } It "second run is idempotent (skip on identical content)" { @@ -258,13 +264,13 @@ Describe "Invoke-ConsolidateTasks -- copilot session sources" { Describe "Write-MigrationManifest" { It "writes a well-formed markdown table" { $records = @( - (New-MigrationRecord -Source "docs/designs/foo.md" -Destination "tasks/foo-plan.md" -Action "move" -SourceType "docs/designs" -LinkedIssues @(1, 2)), - (New-MigrationRecord -Source "~/.copilot/x/plan.md" -Destination "tasks/session-x-plan.md" -Action "copy" -SourceType "copilot-session") + (New-MigrationRecord -Source "tasks/foo-plan.md" -Destination "docs/designs/foo-plan.md" -Action "move" -SourceType "legacy-tasks" -LinkedIssues @(1, 2)), + (New-MigrationRecord -Source "~/.copilot/x/plan.md" -Destination "docs/designs/session-x-plan.md" -Action "copy" -SourceType "copilot-session") ) $path = Join-Path $TestDrive "MIGRATION.md" Write-MigrationManifest -Path $path -Records $records $text = Get-Content -LiteralPath $path -Raw - $text | Should -Match "# tasks/ Migration Manifest" + $text | Should -Match "# docs/ Migration Manifest" $text | Should -Match "Timestamp \(UTC\)" $text | Should -Match "#1 #2" $text | Should -Match "foo-plan.md" diff --git a/Consolidate-Tasks.ps1 b/Consolidate-Tasks.ps1 index 21775da..682792f 100644 --- a/Consolidate-Tasks.ps1 +++ b/Consolidate-Tasks.ps1 @@ -1,39 +1,42 @@ <# .SYNOPSIS Consolidates historical spec artifacts (PRDs, plans, design notes) into - the unified tasks/ directory at the repo root. + the standard docs/ spec archive at the repo root. .DESCRIPTION - Imports files from legacy locations into tasks/-.md form, - so a project's PRD + implementation plan + design notes live side by - side in a single AI-replay archive. Distributed to all consumers via - Pull-SDLC.ai.ps1. + Imports files from legacy locations into the standard docs/ structure -- + PRDs (the *what*) under docs/specs/ and implementation plans (the *how*) + under docs/designs/ -- so a project's requirements corpus lives in one + place an AI can replay to reconstruct the project. Distributed to all + consumers via Pull-SDLC.ai.ps1. Source policies (configurable via switches): In-repo legacy sources -- MOVED via "git mv" to preserve history: - - docs/designs/*.md -> tasks/-plan.md - - docs/prd/*.md -> tasks/-prd.md - - root PRD.md / plan.md / - IMPLEMENTATION_PLAN.md -> tasks/legacy-.md + - tasks/*-prd.md -> docs/specs/-prd.md + - tasks/*-plan.md -> docs/designs/-plan.md + - docs/prd/*.md -> docs/specs/-prd.md + - root PRD.md -> docs/specs/legacy-prd.md + - root plan.md / IMPLEMENTATION_PLAN.md + -> docs/designs/legacy-plan.md Out-of-repo session sources -- COPIED (originals stay in place): - ~/.copilot/session-state//plan.md - -> tasks/session--plan.md - - ~/.claude/... (opt-in) -> tasks/claude--plan.md + -> docs/designs/session--plan.md + - ~/.claude/... (opt-in) -> docs/designs/claude--plan.md Collision policy: if the destination exists with DIFFERENT content, a - suffix is appended. If it already exists with IDENTICAL content, the action is skipped (idempotent). - Writes tasks/MIGRATION.md with an audit row per action. + Writes docs/MIGRATION.md with an audit row per action. Uses SupportsShouldProcess: -WhatIf is the dry run; -Confirm:$false is required for unattended runs because ConfirmImpact is High. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] param( - [switch]$IncludeDocsDesigns = $true, + [switch]$IncludeLegacyTasks = $true, [switch]$IncludeDocsPrd = $true, [switch]$IncludeRootDocs = $true, [switch]$IncludeCopilotSessions = $true, @@ -168,7 +171,7 @@ function Write-MigrationManifest { [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Records ) $lines = New-Object System.Collections.Generic.List[string] - $lines.Add("# tasks/ Migration Manifest") | Out-Null + $lines.Add("# docs/ Migration Manifest") | Out-Null $lines.Add("") | Out-Null $lines.Add("Generated by ``Consolidate-Tasks.ps1``. Each row records one import.") | Out-Null $lines.Add("") | Out-Null @@ -195,7 +198,7 @@ function Import-InRepoFile { param( [Parameter(Mandatory)][string]$Source, [Parameter(Mandatory)][string]$RepoRoot, - [Parameter(Mandatory)][string]$TasksDir, + [Parameter(Mandatory)][string]$DestinationDir, [Parameter(Mandatory)][string]$KindSuffix, [Parameter(Mandatory)][string]$SourceType, [string]$ForcedBaseName, @@ -205,10 +208,10 @@ function Import-InRepoFile { $content = Get-Content -LiteralPath $Source -Raw -ErrorAction Stop $slug = if ($ForcedBaseName) { $ForcedBaseName } else { Get-FeatureSlug -FileName (Split-Path -Leaf $Source) } $base = "$slug$KindSuffix" - $dest = Resolve-Destination -DestinationDir $TasksDir -BaseName $base -Extension ".md" -SourceContent $content + $dest = Resolve-Destination -DestinationDir $DestinationDir -BaseName $base -Extension ".md" -SourceContent $content $relSource = [System.IO.Path]::GetRelativePath($RepoRoot, $Source) -replace "\\", "/" if ($null -eq $dest) { - $existingDest = Join-Path $TasksDir "$base.md" + $existingDest = Join-Path $DestinationDir "$base.md" $relDest = [System.IO.Path]::GetRelativePath($RepoRoot, $existingDest) -replace "\\", "/" return New-MigrationRecord -Source $relSource -Destination $relDest ` -Action "skip" -SourceType $SourceType ` @@ -247,7 +250,7 @@ function Import-SessionFile { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Source, - [Parameter(Mandatory)][string]$TasksDir, + [Parameter(Mandatory)][string]$DestinationDir, [Parameter(Mandatory)][string]$BaseName, [Parameter(Mandatory)][string]$KindSuffix, [Parameter(Mandatory)][string]$SourceType, @@ -256,9 +259,9 @@ function Import-SessionFile { ) $content = Get-Content -LiteralPath $Source -Raw -ErrorAction Stop $base = "$BaseName$KindSuffix" - $dest = Resolve-Destination -DestinationDir $TasksDir -BaseName $base -Extension ".md" -SourceContent $content + $dest = Resolve-Destination -DestinationDir $DestinationDir -BaseName $base -Extension ".md" -SourceContent $content if ($null -eq $dest) { - $existingDest = Join-Path $TasksDir "$base.md" + $existingDest = Join-Path $DestinationDir "$base.md" return New-MigrationRecord -Source $Source -Destination $existingDest ` -Action "skip" -SourceType $SourceType ` -Note "destination already exists with identical content" @@ -282,7 +285,7 @@ function Invoke-ConsolidateTasks { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] param( [Parameter(Mandatory)][string]$RepoRoot, - [switch]$IncludeDocsDesigns, + [switch]$IncludeLegacyTasks, [switch]$IncludeDocsPrd, [switch]$IncludeRootDocs, [switch]$IncludeCopilotSessions, @@ -293,28 +296,35 @@ function Invoke-ConsolidateTasks { [string]$OriginUrl ) $rootFull = (Resolve-Path -LiteralPath $RepoRoot).Path - $tasksDir = Join-Path $rootFull "tasks" - if (-not (Test-Path -LiteralPath $tasksDir)) { - if ($PSCmdlet.ShouldProcess($tasksDir, "Create tasks/ directory")) { - New-Item -ItemType Directory -Path $tasksDir -Force | Out-Null - } - else { - # In WhatIf mode we still need somewhere to compute paths against; - # create a temp shadow only if it does not exist. Skip silently: - # downstream calls will tolerate the missing directory. - } - } + # Destinations in the standard docs/ spec archive. Created on demand by the + # import helpers when a move/copy actually happens (WhatIf leaves them be). + $specsDir = Join-Path $rootFull "docs/specs" + $designsDir = Join-Path $rootFull "docs/designs" $records = New-Object System.Collections.Generic.List[object] - if ($IncludeDocsDesigns) { - $dir = Join-Path $rootFull "docs/designs" + if ($IncludeLegacyTasks) { + $dir = Join-Path $rootFull "tasks" if (Test-Path -LiteralPath $dir) { Get-ChildItem -LiteralPath $dir -Filter "*.md" -File | ForEach-Object { - $r = Import-InRepoFile -Source $_.FullName ` - -RepoRoot $rootFull -TasksDir $tasksDir ` - -KindSuffix "-plan" -SourceType "docs/designs" ` - -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet + $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) + if ($stem -match "-prd$") { + $r = Import-InRepoFile -Source $_.FullName ` + -RepoRoot $rootFull -DestinationDir $specsDir ` + -KindSuffix "-prd" -SourceType "legacy-tasks" ` + -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet + } + elseif ($stem -match "-plan$") { + $r = Import-InRepoFile -Source $_.FullName ` + -RepoRoot $rootFull -DestinationDir $designsDir ` + -KindSuffix "-plan" -SourceType "legacy-tasks" ` + -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet + } + else { + # Not a PRD/plan artifact (e.g. README.md, MIGRATION.md) -- + # leave it where it is. + $r = $null + } if ($r) { $records.Add($r) | Out-Null } } } @@ -325,7 +335,7 @@ function Invoke-ConsolidateTasks { if (Test-Path -LiteralPath $dir) { Get-ChildItem -LiteralPath $dir -Filter "*.md" -File | ForEach-Object { $r = Import-InRepoFile -Source $_.FullName ` - -RepoRoot $rootFull -TasksDir $tasksDir ` + -RepoRoot $rootFull -DestinationDir $specsDir ` -KindSuffix "-prd" -SourceType "docs/prd" ` -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet if ($r) { $records.Add($r) | Out-Null } @@ -335,16 +345,16 @@ function Invoke-ConsolidateTasks { if ($IncludeRootDocs) { $rootMap = [ordered]@{ - "PRD.md" = "-prd" - "plan.md" = "-plan" - "IMPLEMENTATION_PLAN.md" = "-plan" + "PRD.md" = @{ Suffix = "-prd"; Dir = $specsDir } + "plan.md" = @{ Suffix = "-plan"; Dir = $designsDir } + "IMPLEMENTATION_PLAN.md" = @{ Suffix = "-plan"; Dir = $designsDir } } foreach ($name in $rootMap.Keys) { $src = Join-Path $rootFull $name if (Test-Path -LiteralPath $src) { $r = Import-InRepoFile -Source $src ` - -RepoRoot $rootFull -TasksDir $tasksDir ` - -KindSuffix $rootMap[$name] -SourceType "root-doc" ` + -RepoRoot $rootFull -DestinationDir $rootMap[$name].Dir ` + -KindSuffix $rootMap[$name].Suffix -SourceType "root-doc" ` -ForcedBaseName "legacy" ` -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet if ($r) { $records.Add($r) | Out-Null } @@ -364,7 +374,7 @@ function Invoke-ConsolidateTasks { $short = if ($_.Name.Length -ge 8) { $_.Name.Substring(0, 8) } else { $_.Name } $base = "session-$short" $r = Import-SessionFile -Source $plan ` - -TasksDir $tasksDir -BaseName $base -KindSuffix "-plan" ` + -DestinationDir $designsDir -BaseName $base -KindSuffix "-plan" ` -SourceType "copilot-session" ` -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet if ($r) { $records.Add($r) | Out-Null } @@ -379,7 +389,7 @@ function Invoke-ConsolidateTasks { $short = Get-ShortHash -Text $_.FullName $base = "claude-$short" $r = Import-SessionFile -Source $_.FullName ` - -TasksDir $tasksDir -BaseName $base -KindSuffix "-plan" ` + -DestinationDir $designsDir -BaseName $base -KindSuffix "-plan" ` -SourceType "claude-session" ` -LinkIssues:$LinkIssues -ShouldProcessHandler $PSCmdlet if ($r) { $records.Add($r) | Out-Null } @@ -388,7 +398,7 @@ function Invoke-ConsolidateTasks { } if ($records.Count -gt 0) { - $manifestPath = Join-Path $tasksDir "MIGRATION.md" + $manifestPath = Join-Path $rootFull "docs/MIGRATION.md" if ($PSCmdlet.ShouldProcess($manifestPath, "Write migration manifest")) { Write-MigrationManifest -Path $manifestPath -Records $records.ToArray() } @@ -416,7 +426,7 @@ if ($MyInvocation.InvocationName -ne ".") { $records = Invoke-ConsolidateTasks ` -RepoRoot $RepoRoot ` - -IncludeDocsDesigns:$IncludeDocsDesigns ` + -IncludeLegacyTasks:$IncludeLegacyTasks ` -IncludeDocsPrd:$IncludeDocsPrd ` -IncludeRootDocs:$IncludeRootDocs ` -IncludeCopilotSessions:$IncludeCopilotSessions ` @@ -432,6 +442,6 @@ if ($MyInvocation.InvocationName -ne ".") { } else { $records | Format-Table -AutoSize Action, SourceType, Source, Destination - Write-Host ("Consolidate-Tasks: {0} action(s) recorded in tasks/MIGRATION.md." -f $records.Count) -ForegroundColor Green + Write-Host ("Consolidate-Tasks: {0} action(s) recorded in docs/MIGRATION.md." -f $records.Count) -ForegroundColor Green } } \ No newline at end of file diff --git a/Pull-SDLC.ai.Tests.ps1 b/Pull-SDLC.ai.Tests.ps1 index 899dc35..50240d5 100644 --- a/Pull-SDLC.ai.Tests.ps1 +++ b/Pull-SDLC.ai.Tests.ps1 @@ -190,33 +190,45 @@ Describe 'Test-IsAlwaysLocalPath' { Test-IsAlwaysLocalPath -Path '.\README.md' | Should -BeTrue } - It 'returns $true for tasks/ directory itself' { - Test-IsAlwaysLocalPath -Path 'tasks/' | Should -BeTrue + It 'returns $true for docs/specs/ directory itself' { + Test-IsAlwaysLocalPath -Path 'docs/specs/' | Should -BeTrue } - It 'returns $true for any file under tasks/ (prefix match)' { - Test-IsAlwaysLocalPath -Path 'tasks/foo-prd.md' | Should -BeTrue + It 'returns $true for a PRD under docs/specs/ (prefix match)' { + Test-IsAlwaysLocalPath -Path 'docs/specs/foo-prd.md' | Should -BeTrue } - It 'returns $true for a nested path under tasks/' { - Test-IsAlwaysLocalPath -Path 'tasks/archive/2025/old-plan.md' | Should -BeTrue + It 'returns $true for docs/designs/ directory itself' { + Test-IsAlwaysLocalPath -Path 'docs/designs/' | Should -BeTrue } - It 'returns $true for tasks/README.md (the scaffolded consumer-owned file)' { - Test-IsAlwaysLocalPath -Path 'tasks/README.md' | Should -BeTrue + It 'returns $true for a plan under docs/designs/ (prefix match)' { + Test-IsAlwaysLocalPath -Path 'docs/designs/bar-plan.md' | Should -BeTrue } - It 'returns $false for any hypothetical *.template file under tasks/ (carve-out keeps templates upstream-managed)' { - Test-IsAlwaysLocalPath -Path 'tasks/some-future.template' | Should -BeFalse + It 'returns $true for a nested path under docs/specs/' { + Test-IsAlwaysLocalPath -Path 'docs/specs/archive/2025/old-prd.md' | Should -BeTrue } - It 'returns $false for tasks/.gitkeep (directory anchor flows from upstream)' { - Test-IsAlwaysLocalPath -Path 'tasks/.gitkeep' | Should -BeFalse + It 'returns $true for docs/README.md (the scaffolded consumer-owned file)' { + Test-IsAlwaysLocalPath -Path 'docs/README.md' | Should -BeTrue } - It 'does not match a path that merely starts with the letters "tasks" but is not the directory' { - Test-IsAlwaysLocalPath -Path 'tasksy.md' | Should -BeFalse - Test-IsAlwaysLocalPath -Path 'src/tasks-runner.cs' | Should -BeFalse + It 'returns $false for an unrelated file directly under docs/ (only specs/designs/README are consumer-owned)' { + Test-IsAlwaysLocalPath -Path 'docs/whatever.md' | Should -BeFalse + } + + It 'returns $false for any hypothetical *.template file under docs/designs/ (carve-out keeps templates upstream-managed)' { + Test-IsAlwaysLocalPath -Path 'docs/designs/some-future.template' | Should -BeFalse + } + + It 'returns $false for docs/specs/.gitkeep (directory anchor flows from upstream)' { + Test-IsAlwaysLocalPath -Path 'docs/specs/.gitkeep' | Should -BeFalse + } + + It 'does not match a path that merely starts with the letters "docs" but is not a consumer-owned directory' { + Test-IsAlwaysLocalPath -Path 'docsy.md' | Should -BeFalse + Test-IsAlwaysLocalPath -Path 'src/docs-runner.cs' | Should -BeFalse } } @@ -294,7 +306,7 @@ Describe 'Invoke-TemplateScaffold' { Describe 'Invoke-TemplateScaffold same-name scaffold from git ref (issue #156)' { BeforeEach { - # Build a tiny upstream-style git repo containing tasks/README.md so we + # Build a tiny upstream-style git repo containing docs/README.md so we # can verify the function pulls same-name scaffold content from a ref # rather than from the working tree. SourceRoot doubles as the repo # passed to `git -C` for the `git show` lookup. @@ -307,30 +319,30 @@ Describe 'Invoke-TemplateScaffold same-name scaffold from git ref (issue #156)' git init -q -b main git config user.email t@t.t git config user.name t - New-Item -ItemType Directory -Path tasks -Force | Out-Null - 'UPSTREAM_TASKS_README_BODY' | Out-File -Encoding utf8 tasks/README.md -NoNewline + New-Item -ItemType Directory -Path docs -Force | Out-Null + 'UPSTREAM_DOCS_README_BODY' | Out-File -Encoding utf8 docs/README.md -NoNewline git add -A | Out-Null git commit -q -m 'seed' } finally { Pop-Location } - $script:samenameMap = [ordered]@{ 'tasks/README.md' = 'tasks/README.md' } + $script:samenameMap = [ordered]@{ 'docs/README.md' = 'docs/README.md' } } It 'reads upstream content via git show Ref colon path when source equals target and target is missing' { $result = @(Invoke-TemplateScaffold -SourceRoot $script:srcRepo -TargetRoot $script:dstRoot -ScaffoldMap $script:samenameMap -Ref HEAD) - $result | Should -Contain 'tasks/README.md' - $written = Join-Path $script:dstRoot 'tasks/README.md' + $result | Should -Contain 'docs/README.md' + $written = Join-Path $script:dstRoot 'docs/README.md' Test-Path $written | Should -BeTrue - (Get-Content $written -Raw).Trim() | Should -Be 'UPSTREAM_TASKS_README_BODY' + (Get-Content $written -Raw).Trim() | Should -Be 'UPSTREAM_DOCS_README_BODY' } It 'leaves an existing consumer same-name target untouched (consumer edits preserved)' { - New-Item -ItemType Directory -Path (Join-Path $script:dstRoot 'tasks') -Force | Out-Null - Set-Content -Path (Join-Path $script:dstRoot 'tasks/README.md') -Value 'CONSUMER_EDITED_BODY' + New-Item -ItemType Directory -Path (Join-Path $script:dstRoot 'docs') -Force | Out-Null + Set-Content -Path (Join-Path $script:dstRoot 'docs/README.md') -Value 'CONSUMER_EDITED_BODY' $result = @(Invoke-TemplateScaffold -SourceRoot $script:srcRepo -TargetRoot $script:dstRoot -ScaffoldMap $script:samenameMap -Ref HEAD) $result.Count | Should -Be 0 - (Get-Content (Join-Path $script:dstRoot 'tasks/README.md') -Raw).Trim() | Should -Be 'CONSUMER_EDITED_BODY' + (Get-Content (Join-Path $script:dstRoot 'docs/README.md') -Raw).Trim() | Should -Be 'CONSUMER_EDITED_BODY' } It 'skips silently when -Ref is omitted and the same-name source is absent from the working tree' { @@ -339,7 +351,7 @@ Describe 'Invoke-TemplateScaffold same-name scaffold from git ref (issue #156)' # rather than throwing. $result = @(Invoke-TemplateScaffold -SourceRoot $script:dstRoot -TargetRoot $script:dstRoot -ScaffoldMap $script:samenameMap) $result.Count | Should -Be 0 - Test-Path (Join-Path $script:dstRoot 'tasks/README.md') | Should -BeFalse + Test-Path (Join-Path $script:dstRoot 'docs/README.md') | Should -BeFalse } } @@ -783,45 +795,45 @@ Describe 'Invoke-PullSDLC end-to-end' { (Get-Content (Join-Path $fx.Consumer 'CLAUDE.md') -Raw) | Should -Be 'new claude' } - It 'scaffolds tasks/README.md from upstream content on first sync into an empty consumer (issue #156)' { + It 'scaffolds docs/README.md from upstream content on first sync into an empty consumer (issue #156)' { $fx = New-DiffReplayFixture -Root $script:fixtureRoot ` -Seed { 'baseline-claude' | Out-File -Encoding utf8 CLAUDE.md -NoNewline - New-Item -ItemType Directory -Path tasks -Force | Out-Null - 'UPSTREAM_TASKS_README_BODY' | Out-File -Encoding utf8 tasks/README.md -NoNewline + New-Item -ItemType Directory -Path docs -Force | Out-Null + 'UPSTREAM_DOCS_README_BODY' | Out-File -Encoding utf8 docs/README.md -NoNewline } - # Consumer has no tasks/ at all -- this is the "first sync into empty consumer" case. - Remove-Item -Recurse -Force (Join-Path $fx.Consumer 'tasks') -ErrorAction SilentlyContinue + # Consumer has no docs/ at all -- this is the "first sync into empty consumer" case. + Remove-Item -Recurse -Force (Join-Path $fx.Consumer 'docs') -ErrorAction SilentlyContinue $rc = Invoke-PullSDLC -RepoRoot $fx.Consumer -RemoteName 'sdlc.ai' -Bootstrap -NoFetch $rc | Should -Be 0 - $scaffolded = Join-Path $fx.Consumer 'tasks/README.md' + $scaffolded = Join-Path $fx.Consumer 'docs/README.md' Test-Path $scaffolded | Should -BeTrue - (Get-Content $scaffolded -Raw).Trim() | Should -Be 'UPSTREAM_TASKS_README_BODY' + (Get-Content $scaffolded -Raw).Trim() | Should -Be 'UPSTREAM_DOCS_README_BODY' } - It 'preserves consumer edits to tasks/README.md on subsequent sync (issue #156)' { + It 'preserves consumer edits to docs/README.md on subsequent sync (issue #156)' { $fx = New-DiffReplayFixture -Root $script:fixtureRoot ` -Seed { 'baseline-claude' | Out-File -Encoding utf8 CLAUDE.md -NoNewline - New-Item -ItemType Directory -Path tasks -Force | Out-Null - 'UPSTREAM_TASKS_README_BODY' | Out-File -Encoding utf8 tasks/README.md -NoNewline + New-Item -ItemType Directory -Path docs -Force | Out-Null + 'UPSTREAM_DOCS_README_BODY' | Out-File -Encoding utf8 docs/README.md -NoNewline } ` -Tweak { - 'UPSTREAM_TASKS_README_BODY_V2' | Out-File -Encoding utf8 tasks/README.md -NoNewline + 'UPSTREAM_DOCS_README_BODY_V2' | Out-File -Encoding utf8 docs/README.md -NoNewline } - # Consumer has its own edited tasks/README.md tracked in git. - New-Item -ItemType Directory -Path (Join-Path $fx.Consumer 'tasks') -Force | Out-Null - 'CONSUMER_EDITED_BODY' | Out-File -Encoding utf8 (Join-Path $fx.Consumer 'tasks/README.md') -NoNewline + # Consumer has its own edited docs/README.md tracked in git. + New-Item -ItemType Directory -Path (Join-Path $fx.Consumer 'docs') -Force | Out-Null + 'CONSUMER_EDITED_BODY' | Out-File -Encoding utf8 (Join-Path $fx.Consumer 'docs/README.md') -NoNewline Push-Location $fx.Consumer - try { git add tasks/README.md; git commit -q -m 'consumer tasks readme' } finally { Pop-Location } + try { git add docs/README.md; git commit -q -m 'consumer docs readme' } finally { Pop-Location } Set-SdlcSyncState -RepoRoot $fx.Consumer -Remote 'sdlc.ai' -Ref 'main' -Commit $fx.AnchorSha Push-Location $fx.Consumer try { git add .sdlc-ai-sync.json; git commit -q -m 'seed state' } finally { Pop-Location } $rc = Invoke-PullSDLC -RepoRoot $fx.Consumer -RemoteName 'sdlc.ai' -NoFetch $rc | Should -Be 0 - (Get-Content (Join-Path $fx.Consumer 'tasks/README.md') -Raw) | Should -Be 'CONSUMER_EDITED_BODY' + (Get-Content (Join-Path $fx.Consumer 'docs/README.md') -Raw) | Should -Be 'CONSUMER_EDITED_BODY' } It 'scaffolds README.md from README.md.template on first sync into an empty consumer (issue #158)' { diff --git a/Pull-SDLC.ai.ps1 b/Pull-SDLC.ai.ps1 index 2b5c134..4c52068 100644 --- a/Pull-SDLC.ai.ps1 +++ b/Pull-SDLC.ai.ps1 @@ -167,7 +167,7 @@ $script:TemplateScaffoldMap = [ordered]@{ '.github/instructions/project.instructions.md.template' = '.github/instructions/project.instructions.md' 'CLAUDE.project.md.template' = 'CLAUDE.project.md' 'README.md.template' = 'README.md' - 'tasks/README.md' = 'tasks/README.md' + 'docs/README.md' = 'docs/README.md' } # Paths (file or directory prefixes) that upstream owns. Anything under one @@ -179,7 +179,11 @@ $script:UpstreamManagedPaths = @( '.github/agents/', '.github/skills/', '.github/instructions/', - 'tasks/', + # The consumer-owned spec-archive guide. Sync-managed so the same-name + # scaffold delivers `docs/README.md` on first sync and the file is + # reconciled against upstream until the consumer takes ownership (it is + # also on $script:AlwaysLocalPaths, which trumps once it exists locally). + 'docs/README.md', # Meta-scripts: the bootstrap script the user downloads via `iwr` and # its siblings. Sync-managed so the user's local copy is reconciled # against upstream on every run (including the very first carve-out @@ -217,7 +221,13 @@ $script:AlwaysLocalPaths = @( 'CLAUDE.project.md', '.gitattributes', '.sdlc-ai-sync.json', - 'tasks/' + # The standard spec archive: PRDs (the *what*) under docs/specs/, + # implementation plans (the *how*) under docs/designs/, and the + # consumer-owned archive guide docs/README.md. All consumer-owned so + # sync never overwrites a project's requirements corpus. + 'docs/specs/', + 'docs/designs/', + 'docs/README.md' ) # Paths whose upstream content is union-merged into the consumer's copy rather @@ -461,9 +471,9 @@ function Invoke-TemplateScaffold { # When set, same-name scaffold entries (where the map key equals the # value) read upstream content via `git -C $SourceRoot show $Ref:$key` # instead of copying from the working tree. This is how - # `tasks/README.md` (issue #156) -- a committed-in-upstream consumer - # first-draft file that lives under the consumer-owned `tasks/` - # always-local prefix -- is delivered on first sync. + # `docs/README.md` (issue #156) -- a committed-in-upstream consumer + # first-draft file that lives under the consumer-owned `docs/` + # always-local prefixes -- is delivered on first sync. [string]$Ref ) $scaffolded = New-Object System.Collections.Generic.List[string] diff --git a/README.md b/README.md index 9c843e7..08f7d48 100644 --- a/README.md +++ b/README.md @@ -114,15 +114,15 @@ fully consumer-owned -- edit them freely. > so the sync never touches it once you have one. `README.md` uses the indirect (`.template` -> bare) pattern -- not the -same-name pattern used for `tasks/README.md` below -- because the +same-name pattern used for `docs/README.md` below -- because the upstream's own root `README.md` describes IntelliSDLC.ai itself and cannot double as the consumer skeleton. -`tasks/README.md` is shipped directly (not via a `.template` indirection) +`docs/README.md` is shipped directly (not via a `.template` indirection) because its content is the same for every consumer. On first sync the script -copies the upstream `tasks/README.md` into the consumer working tree if no -`tasks/README.md` is present; afterwards `tasks/` is consumer-owned and the -file is never overwritten. +copies the upstream `docs/README.md` into the consumer working tree if no +`docs/README.md` is present; afterwards `docs/specs/`, `docs/designs/`, and +`docs/README.md` are consumer-owned and the file is never overwritten. ## File Ownership diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..fd95ef6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,90 @@ +# docs/ AI-Replay Spec Archive + +This directory is the **durable spec archive** for this project -- the standard +`docs/` structure that holds the product requirements documents (PRDs) and +implementation plans produced by the AI agents that ship with IntelliSDLC.ai. +Hand `docs/` to a fresh AI session and the project can be reconstructed from +these files alone. + +The archive is split into two standard subfolders: + +- **`docs/specs/`** -- PRDs / requirements: *what* to build (user stories, + acceptance criteria, metrics). Written by the `@prd` agent. +- **`docs/designs/`** -- implementation plans: *how* to build it (ordered + tasks, file paths, code snippets, commit messages). Written by `@dev-loop` + Phase 2 (the `@plan` agent seeds them). + +> This `README.md` is **consumer-owned**. The upstream copy is scaffolded +> into your project on first sync and is never overwritten by +> `Pull-SDLC.ai.ps1` afterwards. Customize it freely for your project. + +## Filename Convention + +One feature per `-` identifier, shared by the PRD, plan, GitHub +issue, branch (`feat/-`), and PR: + +- `` -- the GitHub issue number; the leading token is identical to the + branch name, so each folder sorts naturally by issue number. +- `` -- a short kebab-case description. + +| File | Written by | Purpose | +|---------------------------------------|--------------------------|------------------------------------------------------| +| `docs/specs/--prd.md` | `@prd` agent | Product requirements: user stories, acceptance criteria, metrics. | +| `docs/designs/--plan.md`| `@dev-loop` Phase 2 | Implementation plan: ordered tasks with file paths, code snippets, commit messages. | +| `docs/MIGRATION.md` | `Consolidate-Tasks.ps1` | Audit trail of files imported from legacy locations. | + +A PRD spike filed before an issue exists may use the bare `-prd.md` and +get renamed to add the `-` prefix once the issue is created. Legacy +files predating this convention may also appear as a bare `-{prd,plan}.md`. + +## Cross-Referencing + +Each artifact should link to its companion GitHub issue and PR. Recommended +top-of-file header: + +```markdown +# + +- Issue: https://github.com///issues/ +- PR: https://github.com///pull/ +- Slug: - +``` + +When a PRD or plan references stories or tasks by identifier, prefer the +GitHub issue number once it exists; use a local `--NN` form only +until the issues are filed. + +## Sync Immunity + +`docs/specs/`, `docs/designs/`, and this `docs/README.md` are listed in +`$script:AlwaysLocalPaths` inside `Pull-SDLC.ai.ps1`, so their contents are +**never** touched by upstream sync. Edit freely; the next `Pull-SDLC.ai.ps1` +run will leave your requirements corpus alone. + +## Migrating Legacy Spec Files + +The repo-root script `Consolidate-Tasks.ps1` imports historical spec +artifacts into this archive: + +- `tasks/*-prd.md` -> moved (`git mv`) into `docs/specs/-prd.md` +- `tasks/*-plan.md` -> moved (`git mv`) into `docs/designs/-plan.md` +- `docs/prd/*.md` -> moved (`git mv`) into `docs/specs/-prd.md` +- Root `PRD.md` -> `docs/specs/legacy-prd.md`; `plan.md` / `IMPLEMENTATION_PLAN.md` -> `docs/designs/legacy-plan.md` +- `~/.copilot/session-state/*/plan.md` -> copied into `docs/designs/` (repo-scoped via the session-store DB), default ON +- `~/.claude/...` session notes -> copied into `docs/designs/`, opt-in (`-IncludeClaudeSessions`) + +It uses the standard PowerShell `SupportsShouldProcess` pattern: + +```powershell +# Dry run -- prints planned actions, writes nothing: +./Consolidate-Tasks.ps1 -WhatIf + +# Actually perform the migration without per-action prompts: +./Consolidate-Tasks.ps1 -Confirm:$false +``` + +The script is idempotent (re-runs are no-ops) and writes a manifest to +`docs/MIGRATION.md` recording each action. + +See the inline comment-based help (`Get-Help ./Consolidate-Tasks.ps1 -Full`) +for the full switch list. \ No newline at end of file diff --git a/docs/designs/182-docs-spec-convention-plan.md b/docs/designs/182-docs-spec-convention-plan.md new file mode 100644 index 0000000..9bf8e74 --- /dev/null +++ b/docs/designs/182-docs-spec-convention-plan.md @@ -0,0 +1,35 @@ +# Adopt standard docs/ spec structure, retire tasks/ convention + +- Issue: https://github.com/IntelliTect-Samples/IntelliSDLC.ai/issues/182 +- Slug: 182-docs-spec-convention + +## Goal + +Replace the bespoke `tasks/` durable-spec-archive convention with the +industry-standard `docs/` structure: `docs/specs/` (PRDs / the *what*), +`docs/designs/` (implementation plans / the *how*), and a consumer-owned +`docs/README.md` guide. + +## Mapping + +| Old | New | +|---|---| +| `tasks/--prd.md` | `docs/specs/--prd.md` | +| `tasks/--plan.md` | `docs/designs/--plan.md` | +| `tasks/README.md` | `docs/README.md` | +| `tasks/MIGRATION.md` | `docs/MIGRATION.md` | + +## Tasks (behavior-first) + +1. Update Pester tests (Red) in `Pull-SDLC.ai.Tests.ps1` and + `Consolidate-Tasks.Tests.ps1` to encode the new behavior. +2. Update `Pull-SDLC.ai.ps1`: `TemplateScaffoldMap` (`docs/README.md`), + `UpstreamManagedPaths` (`docs/README.md`), `AlwaysLocalPaths` + (`docs/specs/`, `docs/designs/`, `docs/README.md`), carve-out comments. +3. Invert `Consolidate-Tasks.ps1`: destinations `docs/specs/`/`docs/designs/`; + sources add `tasks/*` and `docs/prd/*`, drop `docs/designs/*` as source; + manifest -> `docs/MIGRATION.md`. Keep filename. +4. `git mv tasks/README.md docs/README.md`; rewrite content; remove + `tasks/.gitkeep` + empty `tasks/`. +5. Update agents (`prd`, `plan`, `dev-loop`), `README.md`, `.gitignore`. +6. Run full Pester suites green; preserve CRLF; open PR; rebase-merge. \ No newline at end of file diff --git a/tasks/.gitkeep b/tasks/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tasks/README.md b/tasks/README.md deleted file mode 100644 index 95259f1..0000000 --- a/tasks/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# tasks/ AI-Replay Spec Archive - -This directory is the **durable spec archive** for this project. It holds the -product requirements documents (PRDs) and implementation plans produced by the -AI agents that ship with IntelliSDLC.ai. Hand this directory to a fresh AI -session and the project can be reconstructed from these files alone. - -> This `README.md` is **consumer-owned**. The upstream copy is scaffolded -> into your project on first sync and is never overwritten by -> `Pull-SDLC.ai.ps1` afterwards. Customize it freely for your project. - -## Filename Convention - -One feature per `-` identifier, shared by the PRD, plan, GitHub -issue, branch (`feat/-`), and PR: - -- `` -- the GitHub issue number; the leading token is identical to the - branch name, so `tasks/` sorts naturally by issue number. -- `` -- a short kebab-case description. - -| File | Written by | Purpose | -|-------------------------------|--------------------------|------------------------------------------------------| -| `--prd.md` | `@prd` agent | Product requirements: user stories, acceptance criteria, metrics. | -| `--plan.md` | `@dev-loop` Phase 2 | Implementation plan: ordered tasks with file paths, code snippets, commit messages. | -| `MIGRATION.md` | `Consolidate-Tasks.ps1` | Audit trail of files imported from legacy locations. | - -A PRD spike filed before an issue exists may use the bare `-prd.md` and -get renamed to add the `-` prefix once the issue is created. Legacy -files predating this convention may also appear as a bare `-{prd,plan}.md`. - -## Cross-Referencing - -Each artifact should link to its companion GitHub issue and PR. Recommended -top-of-file header: - -```markdown -# - -- Issue: https://github.com///issues/ -- PR: https://github.com///pull/ -- Slug: - -``` - -When a PRD or plan references stories or tasks by identifier, prefer the -GitHub issue number once it exists; use a local `--NN` form only -until the issues are filed. - -## Sync Immunity - -`tasks/` is listed in `$script:AlwaysLocalPaths` inside `Pull-SDLC.ai.ps1`, -so its contents are **never** touched by upstream sync. Edit freely; the next -`Pull-SDLC.ai.ps1` run will leave this directory alone. - -## Migrating Legacy Spec Files - -The repo-root script `Consolidate-Tasks.ps1` imports historical spec -artifacts into this directory: - -- `docs/designs/*.md` -> moved (`git mv`) and renamed to `-plan.md` -- `docs/prd/*.md` -> moved (`git mv`) and renamed to `-prd.md` -- Root `PRD.md`, `plan.md`, `IMPLEMENTATION_PLAN.md` -> moved -- `~/.copilot/session-state/*/plan.md` -> copied (repo-scoped via the session-store DB), default ON -- `~/.claude/...` session notes -> copied, opt-in (`-IncludeClaudeSessions`) - -It uses the standard PowerShell `SupportsShouldProcess` pattern: - -```powershell -# Dry run -- prints planned actions, writes nothing: -./Consolidate-Tasks.ps1 -WhatIf - -# Actually perform the migration without per-action prompts: -./Consolidate-Tasks.ps1 -Confirm:$false -``` - -The script is idempotent (re-runs are no-ops) and writes a manifest to -`tasks/MIGRATION.md` recording each action. - -See the inline comment-based help (`Get-Help ./Consolidate-Tasks.ps1 -Full`) -for the full switch list. \ No newline at end of file