diff --git a/.agents/skills/setup_setup-repo/SKILL.md b/.agents/skills/setup_setup-repo/SKILL.md new file mode 100644 index 0000000..fe826cd --- /dev/null +++ b/.agents/skills/setup_setup-repo/SKILL.md @@ -0,0 +1,84 @@ +--- +name: "setup_setup-repo" +description: "Bootstrap this repository after clone by enabling git hooks and syncing generated skill mirrors. Use when user asks to set up the repo, bootstrap local tooling, or initialize mirrors/hooks for this clone." +version: "1.0.0" +--- + + +# Setup Repo + +Bootstraps this clone in one pass: + +1. Activates repo git hooks +2. Syncs generated skill mirrors for Claude, Codex, and Copilot + +Implementation layout: + +- Canonical entrypoint: `ai-artifacts/skills/shared/setup/setup-repo/scripts/Invoke-SetupRepo.ps1` +- Repo convenience wrapper: `scripts/setup-repo.ps1` + +## Applicability (capability contract) + +This skill needs shell + filesystem access in a local git checkout. In chat-only environments, +provide the exact commands and explain what each command configures. + +## Default command + +```powershell +pwsh scripts/setup-repo.ps1 +``` + +## Common variants + +Project-local mirrors for one harness: + +```powershell +pwsh scripts/setup-repo.ps1 -Target Copilot -Scope Project +``` + +User-scope mirrors for all harnesses: + +```powershell +pwsh scripts/setup-repo.ps1 -Scope User +``` + +Bootstrap only missing generated mirrors (keeps existing generated files untouched): + +```powershell +pwsh scripts/setup-repo.ps1 -IfMissing +``` + +Refresh generated mirrors without touching git hooks: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks +``` + +Check generated mirrors for drift: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks -Check +pwsh scripts/setup-repo.ps1 -SkipHooks -Target Codex -Skill tdd -Check +``` + +Run only one phase: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks +pwsh scripts/setup-repo.ps1 -SkipSkillSync +``` + +## Verify + +```powershell +git config --get core.hooksPath +Test-Path .claude/commands +Test-Path .agents/skills +Test-Path .github/skills +pwsh scripts/setup-repo.ps1 -SkipHooks -Check +``` + +Expected: + +- `core.hooksPath` prints `.githooks` +- mirror folders exist for selected targets diff --git a/.agents/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1 b/.agents/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1 new file mode 100644 index 0000000..d1c0f05 --- /dev/null +++ b/.agents/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1 @@ -0,0 +1,584 @@ +#Requires -Version 7.0 +#Requires -PSEdition Core +# RuntimePolicy: core-first + +[CmdletBinding()] +param( + [string]$RepoRoot = "", + + [ValidateSet('All', 'Claude', 'Codex', 'Copilot')] + [string]$Target = 'All', + + [Alias('Scope')] + [ValidateSet('Project', 'User')] + [string]$MirrorScope = 'Project', + + [string]$Skill = '', + + [switch]$IfMissing, + [switch]$Check, + [switch]$SkipHooks, + [switch]$SkipSkillSync +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($Check -and $IfMissing) { + throw "-Check is read-only; do not combine it with -IfMissing." +} + +function Get-UserHomePath { + if ($env:HOME) { return $env:HOME } + if ($env:USERPROFILE) { return $env:USERPROFILE } + throw "Cannot resolve user home path from HOME/USERPROFILE." +} + +function Get-RelativePathText { + param( + [string]$BasePath, + [string]$TargetPath + ) + + $rel = [System.IO.Path]::GetRelativePath($BasePath, $TargetPath) + return $rel.TrimStart('\', '/') +} + +function Get-MirrorRootPath { + param( + [ValidateSet('Claude', 'Codex', 'Copilot')] + [string]$TargetName, + + [ValidateSet('Project', 'User')] + [string]$ScopeName, + + [string]$ResolvedRepoRoot + ) + + switch ("$TargetName|$ScopeName") { + 'Claude|Project' { return (Join-Path $ResolvedRepoRoot '.claude/commands') } + 'Claude|User' { return (Join-Path (Get-UserHomePath) '.claude/commands') } + 'Codex|Project' { return (Join-Path $ResolvedRepoRoot '.agents/skills') } + 'Codex|User' { return (Join-Path (Get-UserHomePath) '.codex/skills') } + 'Copilot|Project' { return (Join-Path $ResolvedRepoRoot '.github/skills') } + 'Copilot|User' { return (Join-Path (Get-UserHomePath) '.copilot/skills') } + default { throw "Unsupported mirror target/scope combination: $TargetName / $ScopeName" } + } +} + +function Get-SkillDirectories { + param( + [string]$SkillsRoot, + [string]$SkillName + ) + + $dirs = @(Get-ChildItem $SkillsRoot -Recurse -Filter 'SKILL.md' -File | ForEach-Object { $_.Directory }) + if ($SkillName) { + $dirs = @($dirs | Where-Object { $_.Name -eq $SkillName }) + if ($dirs.Count -eq 0) { + throw "No SKILL.md found for skill '$SkillName' under $SkillsRoot" + } + } + if ($dirs.Count -eq 0) { + throw "No SKILL.md files found under $SkillsRoot" + } + return $dirs +} + +function Get-SyncReportEntry { + param( + [string]$Name, + [string]$Detail + ) + + return [PSCustomObject]@{ + Name = $Name + Detail = $Detail + } +} + +function Write-SyncReport { + param( + [string]$Flavor, + [string]$TargetPath, + [object[]]$Entries, + [string]$Scope + ) + + $scopeText = if ($Scope) { " (scope=$Scope)" } else { '' } + Write-Output "Synced $Flavor -> $TargetPath$scopeText" + + if (-not $Entries -or @($Entries).Count -eq 0) { + return + } + + $nameWidth = ($Entries | ForEach-Object { $_.Name.Length } | Measure-Object -Maximum).Maximum + if (-not $nameWidth) { $nameWidth = 0 } + + foreach ($entry in $Entries) { + Write-Output (" {0} {1}" -f $entry.Name.PadRight($nameWidth), $entry.Detail) + } +} + +function Get-SkillResources { + param([string]$SkillDir) + + return @(Get-ChildItem $SkillDir -Recurse -File | + Where-Object { $_.Name -notin @('SKILL.md', 'METADATA.md') }) +} + +function Copy-MirroredResources { + param( + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetBase + ) + + foreach ($res in $Resources) { + $rel = Get-RelativePathText -BasePath $SourceBase -TargetPath $res.FullName + $dest = Join-Path $TargetBase $rel + New-Item -ItemType Directory -Path (Split-Path $dest -Parent) -Force | Out-Null + Copy-Item $res.FullName $dest -Force + } +} + +function Test-MirroredResourcesMatch { + param( + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetBase, + [switch]$ExcludeTargetSkillFile + ) + + foreach ($res in $Resources) { + $rel = Get-RelativePathText -BasePath $SourceBase -TargetPath $res.FullName + $dest = Join-Path $TargetBase $rel + if (-not (Test-Path $dest) -or + (Get-FileHash $res.FullName).Hash -ne (Get-FileHash $dest).Hash) { + return $false + } + } + + $resourceCount = @($Resources).Count + $mirroredCount = if (Test-Path $TargetBase) { + @(Get-ChildItem $TargetBase -Recurse -File | + Where-Object { -not $ExcludeTargetSkillFile -or $_.Name -ne 'SKILL.md' }).Count + } + else { + 0 + } + + return $mirroredCount -eq $resourceCount +} + +function Get-SkillMirrorState { + param( + [string]$TargetSkillPath, + [string]$ExpectedSkill, + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetResourceBase, + [switch]$ExcludeTargetSkillFile + ) + + if (-not (Test-Path $TargetSkillPath)) { return 'MISSING' } + if ((Get-Content $TargetSkillPath -Raw) -cne $ExpectedSkill) { return 'STALE' } + + if (-not (Test-MirroredResourcesMatch -Resources $Resources -SourceBase $SourceBase -TargetBase $TargetResourceBase -ExcludeTargetSkillFile:$ExcludeTargetSkillFile)) { + return 'STALE' + } + + return 'UP-TO-DATE' +} + +function Convert-ResourceLinks { + param( + [string]$Body, + [string]$Name, + [string[]]$ResourceRelPaths + ) + + foreach ($path in $ResourceRelPaths) { + $Body = $Body.Replace("](./$path)", "]($Name/$path)") + $Body = $Body.Replace("]($path)", "]($Name/$path)") + $Body = $Body -replace ("(?<=@)" + [regex]::Escape($path) + "\b"), "$Name/$path" + } + return $Body +} + +function ConvertTo-YamlScalar { + param([string]$Value) + + if ($null -eq $Value) { return '""' } + return '"' + ($Value -replace '\\', '\\' -replace '"', '\"') + '"' +} + +function Get-FrontmatterValue { + param( + [string]$Document, + [string]$Key + ) + + if ($Document -notmatch "(?s)\A---\r?\n(.*?)\r?\n---\r?\n") { return $null } + $frontmatter = $Matches[1] + $match = [regex]::Match($frontmatter, "(?m)^$([regex]::Escape($Key)):\s*(.*)$") + if (-not $match.Success) { return $null } + + $value = $match.Groups[1].Value.Trim() + if (($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'"))) { + $value = $value.Substring(1, $value.Length - 2) + } + return $value +} + +function Get-SkillBody { + param([string]$Document) + + if ($Document -match "(?s)\A---\r?\n.*?\r?\n---\r?\n(.*)\z") { + return $Matches[1].TrimStart() + } + return $Document.TrimStart() +} + +function ConvertTo-CodexSkill { + param( + [string]$SourceDocument, + [string]$CodexName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $CodexName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function ConvertTo-CopilotSkill { + param( + [string]$SourceDocument, + [string]$SkillName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $SkillName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function Invoke-InstallGitHooksInternal { + param([string]$ResolvedRepoRoot) + + $hooksDir = Join-Path $ResolvedRepoRoot '.githooks' + if (Test-Path $hooksDir) { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + Get-ChildItem -Path $hooksDir -File | ForEach-Object { + $raw = [System.IO.File]::ReadAllText($_.FullName) + $normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n" + if ($normalized -ne $raw) { + [System.IO.File]::WriteAllText($_.FullName, $normalized, $utf8NoBom) + Write-Output "Normalized LF line endings: $($_.Name)" + } + } + } + + Write-Output '== Git hooks ==' + git config core.hooksPath .githooks + Write-Output 'Configured core.hooksPath to .githooks' + + if ($IsLinux -or $IsMacOS) { + $preCommit = Join-Path $hooksDir 'pre-commit' + if (Test-Path $preCommit) { + & chmod +x $preCommit + } + } + + Write-Output 'Git hooks are now active for this clone.' +} + +function Sync-ClaudeSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck + ) + + $commandsRoot = Get-MirrorRootPath -TargetName 'Claude' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + Write-Output '' + Write-Output '== Claude Code skill mirror ==' + + foreach ($dir in $SkillDirs) { + $name = $dir.Name + $group = $dir.Parent.Name + $groupDir = Join-Path $commandsRoot $group + $targetMd = Join-Path $groupDir "$name.md" + $targetRes = Join-Path $groupDir $name + + if ($OnlyIfMissing -and (Test-Path $targetMd)) { + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail 'present - skipped (-IfMissing)')) + continue + } + + $resources = Get-SkillResources -SkillDir $dir.FullName + $resRelPaths = $resources | ForEach-Object { + (Get-RelativePathText -BasePath $dir.FullName -TargetPath $_.FullName).Replace('\\', '/') + } + + $body = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + if ($resRelPaths) { + $body = Convert-ResourceLinks -Body $body -Name $name -ResourceRelPaths $resRelPaths + } + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetMd -ExpectedSkill $body -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetRes + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail $state)) + continue + } + + if (Test-Path $targetMd) { Remove-Item $targetMd -Force } + if (Test-Path $targetRes) { Remove-Item $targetRes -Recurse -Force } + New-Item -ItemType Directory -Path $groupDir -Force | Out-Null + Set-Content -Path $targetMd -Value $body -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetRes + + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + Write-SyncReport -Flavor 'Claude skills' -TargetPath $commandsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CodexSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $agentsRoot = Get-MirrorRootPath -TargetName 'Codex' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + Remove-Item $agentsRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Codex skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $codexName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($codexName) + $targetDir = Join-Path $agentsRoot $codexName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CodexSkill -SourceDocument $sourceDocument -CodexName $codexName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + foreach ($dir in Get-ChildItem $agentsRoot -Directory) { + if (-not $expectedSkillNames.Contains($dir.Name)) { + $driftCount++ + $entries.Add((Get-SyncReportEntry -Name $dir.Name -Detail 'EXTRA')) + } + } + } + + Write-SyncReport -Flavor 'Codex skills' -TargetPath $agentsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CopilotSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $skillsMirrorRoot = Get-MirrorRootPath -TargetName 'Copilot' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + Remove-Item $skillsMirrorRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Copilot skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $copilotSkillName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($copilotSkillName) + $targetDir = Join-Path $skillsMirrorRoot $copilotSkillName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CopilotSkill -SourceDocument $sourceDocument -SkillName $copilotSkillName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + foreach ($dir in Get-ChildItem $skillsMirrorRoot -Directory) { + $skillFile = Join-Path $dir.FullName 'SKILL.md' + if (-not (Test-Path $skillFile)) { continue } + if ((Get-Content $skillFile -Raw) -notmatch '" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function ConvertTo-CopilotSkill { + param( + [string]$SourceDocument, + [string]$SkillName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $SkillName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function Invoke-InstallGitHooksInternal { + param([string]$ResolvedRepoRoot) + + $hooksDir = Join-Path $ResolvedRepoRoot '.githooks' + if (Test-Path $hooksDir) { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + Get-ChildItem -Path $hooksDir -File | ForEach-Object { + $raw = [System.IO.File]::ReadAllText($_.FullName) + $normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n" + if ($normalized -ne $raw) { + [System.IO.File]::WriteAllText($_.FullName, $normalized, $utf8NoBom) + Write-Output "Normalized LF line endings: $($_.Name)" + } + } + } + + Write-Output '== Git hooks ==' + git config core.hooksPath .githooks + Write-Output 'Configured core.hooksPath to .githooks' + + if ($IsLinux -or $IsMacOS) { + $preCommit = Join-Path $hooksDir 'pre-commit' + if (Test-Path $preCommit) { + & chmod +x $preCommit + } + } + + Write-Output 'Git hooks are now active for this clone.' +} + +function Sync-ClaudeSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck + ) + + $commandsRoot = Get-MirrorRootPath -TargetName 'Claude' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + Write-Output '' + Write-Output '== Claude Code skill mirror ==' + + foreach ($dir in $SkillDirs) { + $name = $dir.Name + $group = $dir.Parent.Name + $groupDir = Join-Path $commandsRoot $group + $targetMd = Join-Path $groupDir "$name.md" + $targetRes = Join-Path $groupDir $name + + if ($OnlyIfMissing -and (Test-Path $targetMd)) { + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail 'present - skipped (-IfMissing)')) + continue + } + + $resources = Get-SkillResources -SkillDir $dir.FullName + $resRelPaths = $resources | ForEach-Object { + (Get-RelativePathText -BasePath $dir.FullName -TargetPath $_.FullName).Replace('\\', '/') + } + + $body = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + if ($resRelPaths) { + $body = Convert-ResourceLinks -Body $body -Name $name -ResourceRelPaths $resRelPaths + } + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetMd -ExpectedSkill $body -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetRes + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail $state)) + continue + } + + if (Test-Path $targetMd) { Remove-Item $targetMd -Force } + if (Test-Path $targetRes) { Remove-Item $targetRes -Recurse -Force } + New-Item -ItemType Directory -Path $groupDir -Force | Out-Null + Set-Content -Path $targetMd -Value $body -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetRes + + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + Write-SyncReport -Flavor 'Claude skills' -TargetPath $commandsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CodexSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $agentsRoot = Get-MirrorRootPath -TargetName 'Codex' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + Remove-Item $agentsRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Codex skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $codexName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($codexName) + $targetDir = Join-Path $agentsRoot $codexName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CodexSkill -SourceDocument $sourceDocument -CodexName $codexName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + foreach ($dir in Get-ChildItem $agentsRoot -Directory) { + if (-not $expectedSkillNames.Contains($dir.Name)) { + $driftCount++ + $entries.Add((Get-SyncReportEntry -Name $dir.Name -Detail 'EXTRA')) + } + } + } + + Write-SyncReport -Flavor 'Codex skills' -TargetPath $agentsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CopilotSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $skillsMirrorRoot = Get-MirrorRootPath -TargetName 'Copilot' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + Remove-Item $skillsMirrorRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Copilot skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $copilotSkillName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($copilotSkillName) + $targetDir = Join-Path $skillsMirrorRoot $copilotSkillName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CopilotSkill -SourceDocument $sourceDocument -SkillName $copilotSkillName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + foreach ($dir in Get-ChildItem $skillsMirrorRoot -Directory) { + $skillFile = Join-Path $dir.FullName 'SKILL.md' + if (-not (Test-Path $skillFile)) { continue } + if ((Get-Content $skillFile -Raw) -notmatch ' +# Setup Repo + +Bootstraps this clone in one pass: + +1. Activates repo git hooks +2. Syncs generated skill mirrors for Claude, Codex, and Copilot + +Implementation layout: + +- Canonical entrypoint: `ai-artifacts/skills/shared/setup/setup-repo/scripts/Invoke-SetupRepo.ps1` +- Repo convenience wrapper: `scripts/setup-repo.ps1` + +## Applicability (capability contract) + +This skill needs shell + filesystem access in a local git checkout. In chat-only environments, +provide the exact commands and explain what each command configures. + +## Default command + +```powershell +pwsh scripts/setup-repo.ps1 +``` + +## Common variants + +Project-local mirrors for one harness: + +```powershell +pwsh scripts/setup-repo.ps1 -Target Copilot -Scope Project +``` + +User-scope mirrors for all harnesses: + +```powershell +pwsh scripts/setup-repo.ps1 -Scope User +``` + +Bootstrap only missing generated mirrors (keeps existing generated files untouched): + +```powershell +pwsh scripts/setup-repo.ps1 -IfMissing +``` + +Refresh generated mirrors without touching git hooks: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks +``` + +Check generated mirrors for drift: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks -Check +pwsh scripts/setup-repo.ps1 -SkipHooks -Target Codex -Skill tdd -Check +``` + +Run only one phase: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks +pwsh scripts/setup-repo.ps1 -SkipSkillSync +``` + +## Verify + +```powershell +git config --get core.hooksPath +Test-Path .claude/commands +Test-Path .agents/skills +Test-Path .github/skills +pwsh scripts/setup-repo.ps1 -SkipHooks -Check +``` + +Expected: + +- `core.hooksPath` prints `.githooks` +- mirror folders exist for selected targets diff --git a/.github/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1 b/.github/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1 new file mode 100644 index 0000000..d1c0f05 --- /dev/null +++ b/.github/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1 @@ -0,0 +1,584 @@ +#Requires -Version 7.0 +#Requires -PSEdition Core +# RuntimePolicy: core-first + +[CmdletBinding()] +param( + [string]$RepoRoot = "", + + [ValidateSet('All', 'Claude', 'Codex', 'Copilot')] + [string]$Target = 'All', + + [Alias('Scope')] + [ValidateSet('Project', 'User')] + [string]$MirrorScope = 'Project', + + [string]$Skill = '', + + [switch]$IfMissing, + [switch]$Check, + [switch]$SkipHooks, + [switch]$SkipSkillSync +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($Check -and $IfMissing) { + throw "-Check is read-only; do not combine it with -IfMissing." +} + +function Get-UserHomePath { + if ($env:HOME) { return $env:HOME } + if ($env:USERPROFILE) { return $env:USERPROFILE } + throw "Cannot resolve user home path from HOME/USERPROFILE." +} + +function Get-RelativePathText { + param( + [string]$BasePath, + [string]$TargetPath + ) + + $rel = [System.IO.Path]::GetRelativePath($BasePath, $TargetPath) + return $rel.TrimStart('\', '/') +} + +function Get-MirrorRootPath { + param( + [ValidateSet('Claude', 'Codex', 'Copilot')] + [string]$TargetName, + + [ValidateSet('Project', 'User')] + [string]$ScopeName, + + [string]$ResolvedRepoRoot + ) + + switch ("$TargetName|$ScopeName") { + 'Claude|Project' { return (Join-Path $ResolvedRepoRoot '.claude/commands') } + 'Claude|User' { return (Join-Path (Get-UserHomePath) '.claude/commands') } + 'Codex|Project' { return (Join-Path $ResolvedRepoRoot '.agents/skills') } + 'Codex|User' { return (Join-Path (Get-UserHomePath) '.codex/skills') } + 'Copilot|Project' { return (Join-Path $ResolvedRepoRoot '.github/skills') } + 'Copilot|User' { return (Join-Path (Get-UserHomePath) '.copilot/skills') } + default { throw "Unsupported mirror target/scope combination: $TargetName / $ScopeName" } + } +} + +function Get-SkillDirectories { + param( + [string]$SkillsRoot, + [string]$SkillName + ) + + $dirs = @(Get-ChildItem $SkillsRoot -Recurse -Filter 'SKILL.md' -File | ForEach-Object { $_.Directory }) + if ($SkillName) { + $dirs = @($dirs | Where-Object { $_.Name -eq $SkillName }) + if ($dirs.Count -eq 0) { + throw "No SKILL.md found for skill '$SkillName' under $SkillsRoot" + } + } + if ($dirs.Count -eq 0) { + throw "No SKILL.md files found under $SkillsRoot" + } + return $dirs +} + +function Get-SyncReportEntry { + param( + [string]$Name, + [string]$Detail + ) + + return [PSCustomObject]@{ + Name = $Name + Detail = $Detail + } +} + +function Write-SyncReport { + param( + [string]$Flavor, + [string]$TargetPath, + [object[]]$Entries, + [string]$Scope + ) + + $scopeText = if ($Scope) { " (scope=$Scope)" } else { '' } + Write-Output "Synced $Flavor -> $TargetPath$scopeText" + + if (-not $Entries -or @($Entries).Count -eq 0) { + return + } + + $nameWidth = ($Entries | ForEach-Object { $_.Name.Length } | Measure-Object -Maximum).Maximum + if (-not $nameWidth) { $nameWidth = 0 } + + foreach ($entry in $Entries) { + Write-Output (" {0} {1}" -f $entry.Name.PadRight($nameWidth), $entry.Detail) + } +} + +function Get-SkillResources { + param([string]$SkillDir) + + return @(Get-ChildItem $SkillDir -Recurse -File | + Where-Object { $_.Name -notin @('SKILL.md', 'METADATA.md') }) +} + +function Copy-MirroredResources { + param( + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetBase + ) + + foreach ($res in $Resources) { + $rel = Get-RelativePathText -BasePath $SourceBase -TargetPath $res.FullName + $dest = Join-Path $TargetBase $rel + New-Item -ItemType Directory -Path (Split-Path $dest -Parent) -Force | Out-Null + Copy-Item $res.FullName $dest -Force + } +} + +function Test-MirroredResourcesMatch { + param( + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetBase, + [switch]$ExcludeTargetSkillFile + ) + + foreach ($res in $Resources) { + $rel = Get-RelativePathText -BasePath $SourceBase -TargetPath $res.FullName + $dest = Join-Path $TargetBase $rel + if (-not (Test-Path $dest) -or + (Get-FileHash $res.FullName).Hash -ne (Get-FileHash $dest).Hash) { + return $false + } + } + + $resourceCount = @($Resources).Count + $mirroredCount = if (Test-Path $TargetBase) { + @(Get-ChildItem $TargetBase -Recurse -File | + Where-Object { -not $ExcludeTargetSkillFile -or $_.Name -ne 'SKILL.md' }).Count + } + else { + 0 + } + + return $mirroredCount -eq $resourceCount +} + +function Get-SkillMirrorState { + param( + [string]$TargetSkillPath, + [string]$ExpectedSkill, + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetResourceBase, + [switch]$ExcludeTargetSkillFile + ) + + if (-not (Test-Path $TargetSkillPath)) { return 'MISSING' } + if ((Get-Content $TargetSkillPath -Raw) -cne $ExpectedSkill) { return 'STALE' } + + if (-not (Test-MirroredResourcesMatch -Resources $Resources -SourceBase $SourceBase -TargetBase $TargetResourceBase -ExcludeTargetSkillFile:$ExcludeTargetSkillFile)) { + return 'STALE' + } + + return 'UP-TO-DATE' +} + +function Convert-ResourceLinks { + param( + [string]$Body, + [string]$Name, + [string[]]$ResourceRelPaths + ) + + foreach ($path in $ResourceRelPaths) { + $Body = $Body.Replace("](./$path)", "]($Name/$path)") + $Body = $Body.Replace("]($path)", "]($Name/$path)") + $Body = $Body -replace ("(?<=@)" + [regex]::Escape($path) + "\b"), "$Name/$path" + } + return $Body +} + +function ConvertTo-YamlScalar { + param([string]$Value) + + if ($null -eq $Value) { return '""' } + return '"' + ($Value -replace '\\', '\\' -replace '"', '\"') + '"' +} + +function Get-FrontmatterValue { + param( + [string]$Document, + [string]$Key + ) + + if ($Document -notmatch "(?s)\A---\r?\n(.*?)\r?\n---\r?\n") { return $null } + $frontmatter = $Matches[1] + $match = [regex]::Match($frontmatter, "(?m)^$([regex]::Escape($Key)):\s*(.*)$") + if (-not $match.Success) { return $null } + + $value = $match.Groups[1].Value.Trim() + if (($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'"))) { + $value = $value.Substring(1, $value.Length - 2) + } + return $value +} + +function Get-SkillBody { + param([string]$Document) + + if ($Document -match "(?s)\A---\r?\n.*?\r?\n---\r?\n(.*)\z") { + return $Matches[1].TrimStart() + } + return $Document.TrimStart() +} + +function ConvertTo-CodexSkill { + param( + [string]$SourceDocument, + [string]$CodexName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $CodexName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function ConvertTo-CopilotSkill { + param( + [string]$SourceDocument, + [string]$SkillName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $SkillName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function Invoke-InstallGitHooksInternal { + param([string]$ResolvedRepoRoot) + + $hooksDir = Join-Path $ResolvedRepoRoot '.githooks' + if (Test-Path $hooksDir) { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + Get-ChildItem -Path $hooksDir -File | ForEach-Object { + $raw = [System.IO.File]::ReadAllText($_.FullName) + $normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n" + if ($normalized -ne $raw) { + [System.IO.File]::WriteAllText($_.FullName, $normalized, $utf8NoBom) + Write-Output "Normalized LF line endings: $($_.Name)" + } + } + } + + Write-Output '== Git hooks ==' + git config core.hooksPath .githooks + Write-Output 'Configured core.hooksPath to .githooks' + + if ($IsLinux -or $IsMacOS) { + $preCommit = Join-Path $hooksDir 'pre-commit' + if (Test-Path $preCommit) { + & chmod +x $preCommit + } + } + + Write-Output 'Git hooks are now active for this clone.' +} + +function Sync-ClaudeSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck + ) + + $commandsRoot = Get-MirrorRootPath -TargetName 'Claude' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + Write-Output '' + Write-Output '== Claude Code skill mirror ==' + + foreach ($dir in $SkillDirs) { + $name = $dir.Name + $group = $dir.Parent.Name + $groupDir = Join-Path $commandsRoot $group + $targetMd = Join-Path $groupDir "$name.md" + $targetRes = Join-Path $groupDir $name + + if ($OnlyIfMissing -and (Test-Path $targetMd)) { + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail 'present - skipped (-IfMissing)')) + continue + } + + $resources = Get-SkillResources -SkillDir $dir.FullName + $resRelPaths = $resources | ForEach-Object { + (Get-RelativePathText -BasePath $dir.FullName -TargetPath $_.FullName).Replace('\\', '/') + } + + $body = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + if ($resRelPaths) { + $body = Convert-ResourceLinks -Body $body -Name $name -ResourceRelPaths $resRelPaths + } + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetMd -ExpectedSkill $body -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetRes + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail $state)) + continue + } + + if (Test-Path $targetMd) { Remove-Item $targetMd -Force } + if (Test-Path $targetRes) { Remove-Item $targetRes -Recurse -Force } + New-Item -ItemType Directory -Path $groupDir -Force | Out-Null + Set-Content -Path $targetMd -Value $body -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetRes + + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + Write-SyncReport -Flavor 'Claude skills' -TargetPath $commandsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CodexSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $agentsRoot = Get-MirrorRootPath -TargetName 'Codex' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + Remove-Item $agentsRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Codex skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $codexName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($codexName) + $targetDir = Join-Path $agentsRoot $codexName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CodexSkill -SourceDocument $sourceDocument -CodexName $codexName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + foreach ($dir in Get-ChildItem $agentsRoot -Directory) { + if (-not $expectedSkillNames.Contains($dir.Name)) { + $driftCount++ + $entries.Add((Get-SyncReportEntry -Name $dir.Name -Detail 'EXTRA')) + } + } + } + + Write-SyncReport -Flavor 'Codex skills' -TargetPath $agentsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CopilotSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $skillsMirrorRoot = Get-MirrorRootPath -TargetName 'Copilot' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + Remove-Item $skillsMirrorRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Copilot skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $copilotSkillName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($copilotSkillName) + $targetDir = Join-Path $skillsMirrorRoot $copilotSkillName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CopilotSkill -SourceDocument $sourceDocument -SkillName $copilotSkillName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + foreach ($dir in Get-ChildItem $skillsMirrorRoot -Directory) { + $skillFile = Join-Path $dir.FullName 'SKILL.md' + if (-not (Test-Path $skillFile)) { continue } + if ((Get-Content $skillFile -Raw) -notmatch ' @@ -59,4 +60,4 @@ version introduce unnecessary verbosity? Apply the better formulation back to - The CLAUDE.md at `~/.claude/CLAUDE.md` is a different file (global Claude Code instructions); do not conflate with this profile - The profile header "Paste into: Settings → Instructions for Claude" should stay in - `instructions/claude-ai/profile.md` as a placement reminder but must be stripped before pasting + `ai-artifacts/instructions/anthropic/claude-ai/profile.md` as a placement reminder but must be stripped before pasting diff --git a/.scratch/public-repo-compliance/PRD.md b/.scratch/public-repo-compliance/PRD.md index 856f00c..a88f21a 100644 --- a/.scratch/public-repo-compliance/PRD.md +++ b/.scratch/public-repo-compliance/PRD.md @@ -11,12 +11,12 @@ The repo is already **PUBLIC** (`github.com/DenWin/ai-lab`), but the items gated publishing" never ran: 1. **Third-party attribution.** [import-upstream-skills](../import-upstream-skills/PRD.md) says to - add upstream's LICENSE (e.g. `THIRD-PARTY/mattpocock-skills.LICENSE`) "before publishing, since + add upstream's LICENSE (historically tracked in a dedicated third-party attribution folder) "before publishing, since the repo redistributes adapted copies of his work." Vendored copies are committed (mattpocock - skills under `shared/skills/` + `.scratch/*/artifacts/`; MIT-licensed `claude-video` under + skills under `ai-artifacts/skills/shared/` + `.scratch/*/artifacts/`; MIT-licensed `claude-video` under `.scratch/add-watch-skill/artifacts/`), and the repo is live. 2. **Profile exposure.** The personal behavioral profile - ([anthropic/claude-ai/instructions/profile.md](../../anthropic/claude-ai/instructions/profile.md)) + ([ai-artifacts/instructions/anthropic/claude-ai/profile.md](../../ai-artifacts/instructions/anthropic/claude-ai/profile.md)) and committed `.scratch` history are public. Probably fine — but it should be a conscious decision, not a side effect of `gh repo create`. 3. **Free hardening wins.** [[harden-github-repo]] is still needs-triage, and its central open @@ -26,9 +26,9 @@ publishing" never ran: ## Solution -_Proposed — refine in triage:_ +*Proposed — refine in triage:* -- Add `THIRD-PARTY/` attribution files for all vendored upstream content; audit `.scratch/*/artifacts/` +- Add attribution files for all vendored upstream content; audit `.scratch/*/artifacts/` for anything else redistributed. - Explicit go/no-go on public visibility of the profile and scratch history (alternative: flip repo to private until [[harden-github-repo]] lands). @@ -38,16 +38,17 @@ _Proposed — refine in triage:_ ## Progress (2026-07-04) - ✅ **Item 1 — attribution:** both upstreams verified MIT. - [THIRD-PARTY/](../../THIRD-PARTY/README.md) created with license copies for vendored sources - (`mattpocock-skills.LICENSE`, `bradautomates-claude-video.LICENSE`) plus a notice map. Exact - upstream checkpoints for skills live in each skill's `METADATA.md`, not in summary docs. + Historical note: a dedicated third-party attribution folder existed when this item was first + completed, but those repo copies are no longer tracked after later cleanup. The remaining durable + provenance in this repo is each imported skill's `METADATA.md`; this entry should not be read as a + current inventory of committed license-copy files. - ✅ **Item 3 — free hardening wins:** secret scanning, push protection, and Dependabot alerts enabled via `gh api`. **Branch protection deliberately NOT enabled** — it would block the current direct-to-main workflow; decide it together with [[gated-work-prd-issue-approval]] (which wants a PR flow anyway) inside [[harden-github-repo]]. - ✅ **Item 2 — decided 2026-07-05: keep public, after a scrub.** Decision: `keep-public`. Before confirming, a redaction audit swept the personal profile - ([anthropic/claude-ai/instructions/profile.md](../../anthropic/claude-ai/instructions/profile.md)) + ([ai-artifacts/instructions/anthropic/claude-ai/profile.md](../../ai-artifacts/instructions/anthropic/claude-ai/profile.md)) and the full `.scratch/` tree (+ the committed config artifacts) for anything personal or sensitive. **Audit scope & result (nothing required redaction):** @@ -65,6 +66,6 @@ _Proposed — refine in triage:_ ## Further Notes -- Related: [[harden-github-repo]] (settings/Actions side), [[import-upstream-skills]] (where the +- Related: [[harden-github-repo]] (GitHub Actions/settings side), [[import-upstream-skills]] (where the attribution requirement was first recorded). -- _Created by Claude Fable 5 via /planning:scratch._ +- *Created by Claude Fable 5 via /planning:scratch.* diff --git a/.scratch/repo-scaffold/PRD.md b/.scratch/repo-scaffold/PRD.md index 858ee41..9fb28b2 100644 --- a/.scratch/repo-scaffold/PRD.md +++ b/.scratch/repo-scaffold/PRD.md @@ -16,8 +16,8 @@ The structure decisions were resolved via grill-me (2026-06-04) and the layout m > superseded. Canonical layout is now `shared/` (default) + `/` + `//`, > most-specific-wins; **loose folders** (no plugin bundles); folders created **on demand**. See > [docs/repo-layout.adoc](../../docs/repo-layout.adoc) — the canonical layout reference. -> Done so far: `git init`; skills → `shared/skills/`; profile → -> `anthropic/claude-ai/instructions/profile.md`; `sync-skills.ps1` retargeted + re-run; layout doc +> Done so far: `git init`; skills → `ai-artifacts/skills/shared/`; profile → +> `ai-artifacts/instructions/anthropic/claude-ai/profile.md`; setup-repo mirror sync retargeted + re-run; layout doc > authored. Remaining: `AGENTS.md`, file placement, remote, initial commit. `git init` (done) unblocks the `tdd`-skill→foundation-doc git-repo reference (see @@ -26,15 +26,15 @@ This also folded in the earlier `init-git-repo` stub. ## Scope -Phase 1 of the handoff (skill adaptation) is largely done — skills live under `shared/skills/` and +Phase 1 of the handoff (skill adaptation) is largely done — skills live under `ai-artifacts/skills/shared/` and remaining work is tracked in [claude-code-skill-adaptation](../claude-code-skill-adaptation/PRD.md) and [import-upstream-skills](../import-upstream-skills/PRD.md). This feature is **Phase 2 only**: 1. ✅ `git init`, default branch `main`. `.gitignore` already reconciled (`.claude/commands/*` generated; `.temp/*` staged) — no change needed. -2. ✅ Structure resolved + migrated: skills → `shared/skills/`, profile → - `anthropic/claude-ai/instructions/`, `sync-skills.ps1` retargeted, [docs/repo-layout.adoc](../../docs/repo-layout.adoc) - authored. Per-harness artifact folders (`settings/`, `mcp/`, `hooks/`, `output-styles/`) are +2. ✅ Structure resolved + migrated: skills → `ai-artifacts/skills/shared/`, profile → + `ai-artifacts/instructions/anthropic/claude-ai/`, setup-repo mirror sync retargeted, [docs/repo-layout.adoc](../../docs/repo-layout.adoc) + authored. Per-harness artifact folders (`ai-artifacts/mcp-config/`, `ai-artifacts/hooks/`, `ai-artifacts/output-styles/`) are created **on demand**, not pre-scaffolded. 3. ✅ Root `AGENTS.md` authored (2026-07-04) as an **operational stub**: cross-harness facts only (layout, source-of-truth/mirror rules, `.scratch` workflow, conventions). The behavioral-overlap @@ -53,8 +53,8 @@ and [import-upstream-skills](../import-upstream-skills/PRD.md). This feature is ## Decisions 1. **#6 Taxonomy keying — RESOLVED (grill-me 2026-06-04).** `shared/` (default) + `/` + - `//`, most-specific-wins. Skills default to `shared/skills/`; config - (instructions/settings/mcp/hooks/output-styles) lives under `//`. claude.ai adds + `//`, most-specific-wins. Skills default to `ai-artifacts/skills/shared/`; config + (instructions/mcp-config/hooks/output-styles) lives under `//`. claude.ai adds a `projects//` layer. No vendor-only "platform" key. Ref: docs/repo-layout.adoc. 2. **#7 Packaging — RESOLVED.** Loose folders, not plugin bundles (personal use; can be wrapped into a plugin later if distribution ever matters). diff --git a/.scratch/repo-scaffold/artifacts/HANDOFF-ai-lab-repo-structure.md b/.scratch/repo-scaffold/artifacts/HANDOFF-ai-lab-repo-structure.md index f2e25e7..1d47f8d 100644 --- a/.scratch/repo-scaffold/artifacts/HANDOFF-ai-lab-repo-structure.md +++ b/.scratch/repo-scaffold/artifacts/HANDOFF-ai-lab-repo-structure.md @@ -98,12 +98,15 @@ ai-lab/ ├── instructions/ │ ├── global/ # account-wide / cross-vendor (e.g. profile) │ └── // # specific overrides — keying per #6 -├── skills/ # // OR nested in plugin bundles — per #6/#7 -├── agents/ # subagents -├── hooks/ # hook scripts / hooks.json -├── mcp/ # MCP server defs (.mcp.json) — was missing -├── output-styles/ # was missing -├── settings/ # settings.json / permissions — was missing +├── ai-artifacts/ +│ ├── skills/ # // OR nested in plugin bundles — per #6/#7 +│ ├── agents/ # subagents +│ ├── hooks/ # hook scripts / hooks.json +│ ├── mcp-config/ # MCP server defs (.mcp.json) +│ ├── output-styles/ # reusable output styles +│ ├── prompts/ # reusable prompt packs +│ ├── instructions/ # harness instruction surfaces +│ └── plugins/ # plugin packaging └── eval/ └── INSTRUCTION-EVAL.md ``` @@ -111,10 +114,10 @@ ai-lab/ **Design rationale:** - `AGENTS.md` at root is the broadest-compatible entrypoint; harness/vendor-specific files - (`CLAUDE.md`, `copilot-instructions.md`) live under `instructions/`, keyed per #6. + (`CLAUDE.md`, `copilot-instructions.md`) live under `ai-artifacts/instructions/`, keyed per #6. - The living doc maps (harness × artifact type) with vendor as a tag — not "platform × harness". Start minimal; grow as knowledge grows. -- If plugins win (#7), `skills/`, `agents/`, `hooks/`, `mcp/`, `output-styles/` nest inside +- If plugins win (#7), `ai-artifacts/skills/`, `ai-artifacts/agents/`, `ai-artifacts/hooks/`, `mcp/`, `ai-artifacts/output-styles/` nest inside `plugins//` rather than at top level. - `eval/` holds `INSTRUCTION-EVAL.md` (built) + the pending harness (separate handoff). - `instructions/global/` holds the account-wide profile copy (source of truth stays Settings → @@ -129,9 +132,9 @@ ai-lab/ and grow detail pages lazily? 3. PowerShell-Skripte project-specific files (`02-project-instructions.md`, `CLAUDE.md`, `POWERSHELL.md`, `powershell.yml`) — these belong to a specific claude.ai project, not - the global repo. Options: (a) move under `instructions/` keyed per #6 (e.g. + the global repo. Options: (a) move under `ai-artifacts/instructions/` keyed per #6 (e.g. `…/anthropic/projects/pwsh/`); (b) leave them in the claude.ai project only; (c) both. Confirm. -4. `skills/shared/` concept: define what makes a skill "shared" — a skill whose _intent_ is +4. `ai-artifacts/skills/shared/` concept: define what makes a skill "shared" — a skill whose _intent_ is cross-vendor (logic documented once, adapted per harness), or one whose _file format_ runs on multiple harnesses unchanged? (Ties to the capability-contract principle below.) 5. Setup/init skill shape: monolithic setup skill (Matt's pattern) vs self-configuring vs @@ -140,12 +143,12 @@ ai-lab/ 6. Terminology alignment: the matrix taxonomy below settles on **harness** (primary) + **vendor** (tag), with **model** excluded. The structure proposal and Requirements still say "platform" (and list Copilot as one — it's a harness). When scaffolding, decide the folder keys - accordingly: e.g. `instructions///`, `skills///`, and rename + accordingly: e.g. `ai-artifacts/instructions///`, `ai-artifacts/skills///`, and rename `docs/platforms/` → `docs/harnesses/`. Don't silently keep "platform" — it's the ambiguity this session flagged. This includes the living doc's own filename: rename `platform-matrix.md` → e.g. `compatibility-matrix.md` or `harness-matrix.md`. -7. Packaging: loose artifact folders (`skills/`, `agents/`, `hooks/`, `mcp/`, `output-styles/`, - `settings/`) vs Claude Code **plugin bundles** (`.claude-plugin/plugin.json` wrapping them). +7. Packaging: loose artifact folders (`ai-artifacts/skills/`, `ai-artifacts/agents/`, `ai-artifacts/hooks/`, `mcp/`, `ai-artifacts/output-styles/`, + `ai-artifacts/mcp-config/`, `ai-artifacts/plugins/`) vs Claude Code **plugin bundles** (`.claude-plugin/plugin.json` wrapping them). Plugins are the native share/install unit and compose cleanly; loose folders are simpler but not directly installable. Regardless of choice, the structure currently has no home for MCP defs, output styles, or settings — add them. @@ -193,7 +196,7 @@ in plugin bundles) — treat them as illustrative, not committed. if user wants an adversarial pass). 8. Place existing non-skill files (profile, INSTRUCTION-EVAL, project files) per the artifact inventory above. -9. Place the adapted Claude Code skills from Phase 1 into `skills/`. +9. Place the adapted Claude Code skills from Phase 1 into `ai-artifacts/skills/`. 10. Author `AGENTS.md` root stub and the living compatibility-matrix draft (filename per #6). 11. Initial commit + push. 12. (Optional) In claude.ai: Settings → Connectors → GitHub Integration → connect + add @@ -245,7 +248,7 @@ The "artifact type" axis needs a fuller vocabulary. Claude Code (richest harness | Skills (slash commands now unified in) | `skills//SKILL.md` | Claude family; others vary | | Subagents | `agents/*.md` | Claude Code | | Hooks | `hooks.json` / `settings.json` | Claude Code | -| Output styles | `output-styles/` | Claude Code | +| Output styles | `ai-artifacts/output-styles/` | Claude Code | | **MCP servers** | `.mcp.json`, `~/.claude.json` | **cross-vendor (open protocol)** — Codex, Cursor, etc.; config location varies | | Settings / permissions | `settings.json`, `settings.local.json` | Claude Code | | Plugins (bundle wrapping all the above) | `.claude-plugin/plugin.json` | Claude Code | diff --git a/.scratch/repo-scope-strays/PRD.md b/.scratch/repo-scope-strays/PRD.md index 414f830..cdd9d6c 100644 --- a/.scratch/repo-scope-strays/PRD.md +++ b/.scratch/repo-scope-strays/PRD.md @@ -10,7 +10,7 @@ Quick capture — iron out in scratch-planning, don't action yet. The repo's declared identity (AI-configuration lab: skills, harness docs, instructions, tracker) is being diluted by undeclared strays: -- `VSCode_Extsion/` (note the folder-name typo — "Extsion") — a shipped VS Code extension at repo +- `VSCode_Extension/` (historically created as `VSCode_Extsion/`, with the typo) — a shipped VS Code extension at repo root, unrelated to the lab's stated purpose, with no README tying it in. - [mail-to-doc](../mail-to-doc/PRD.md) — a general software project (eml→AsciiDoc converter) riding in the `.scratch` tracker. @@ -21,7 +21,7 @@ doesn't account for. ## Solution -_Proposed — refine in triage. Two clean options:_ +*Proposed — refine in triage. Two clean options:* 1. **Declare incubation:** "the lab also incubates small tools" — give them a home (e.g. `tools/` or `projects/`), fix the folder-name typo, add a README per tool, and record the @@ -35,8 +35,8 @@ Either answer is fine — the undeclared middle isn't. Renaming/moving the exten ## Further Notes - Touches the repo-layout doc owned by [[repo-scaffold]]; coordinate if both are in flight. -- 2026-07-04 scratch-plan (user input): `VSCode_Extsion/` **needs a README**; it is an outlier but +- 2026-07-04 scratch-plan (user input): `VSCode_Extension/` **needs a README**; it is an outlier but not unrelated — it exists to make reading AsciiDoc files in VS Code easier, in support of AI projects. And [[mail-to-doc]] is a **skill**, not a software project — which weakens the "stray software project" framing above; re-frame during triage. -- _Created by Claude Fable 5 via /planning:scratch._ +- *Created by Claude Fable 5 via /planning:scratch.* diff --git a/.scratch/sync-skills-drift-check/PRD.md b/.scratch/sync-skills-drift-check/PRD.md index e4c5586..aaf273d 100644 --- a/.scratch/sync-skills-drift-check/PRD.md +++ b/.scratch/sync-skills-drift-check/PRD.md @@ -1,21 +1,21 @@ -# PRD — Add a drift check to sync-skills.ps1 +# PRD — Add a drift check to generated mirror sync Status: done (2026-07-04 — see Progress) Origin: fable (Claude Fable 5 repo review, 2026-07-04) ## Problem Statement -The SessionStart hook runs `sync-skills.ps1 -IfMissing`, which by design never refreshes a skill +The SessionStart hook runs `setup-repo.ps1 -IfMissing`, which by design never refreshes a skill whose target already exists. There is no drift detection between the source of truth -(`shared/skills/`) and the generated mirror (`.claude/commands/`). Consequence: after editing a +(`ai-artifacts/skills/shared/`) and the generated mirror (`.claude/commands/`). Consequence: after editing a skill, the stale-mirror failure mode is the **default** path — the hook silently skips, and the session keeps invoking the old version until someone remembers to re-run the sync manually. ## Solution -_Proposed — refine in triage:_ +*Proposed — refine in triage:* -- Add a `-Check` mode to [scripts/sync-skills.ps1](../../scripts/sync-skills.ps1): compare source +- Add a `-Check` mode to [scripts/setup-repo.ps1](../../scripts/setup-repo.ps1): compare source vs. mirror content (per-skill hash over SKILL.md + resources, with the same link rewriting applied), list stale skills, and return a nonzero exit code / warning line. - Have the SessionStart hook run `-IfMissing` **plus** `-Check`, so a stale mirror produces a @@ -26,7 +26,7 @@ _Proposed — refine in triage:_ ## Progress (2026-07-04 — done) -- ✅ `-Check` mode added to [scripts/sync-skills.ps1](../../scripts/sync-skills.ps1): read-only, +- ✅ `-Check` mode added to [scripts/setup-repo.ps1](../../scripts/setup-repo.ps1): read-only, compares expected mirror output (link rewriting applied) against the actual tree — command-file content (`-cne`), per-resource hashes, and extra-files-in-mirror all count as drift. Reports UP-TO-DATE / STALE / MISSING per skill; exit 1 on any drift; guarded against combining with @@ -44,4 +44,4 @@ _Proposed — refine in triage:_ - Failure mode identified in the Fable repo review (2026-07-04); the `-IfMissing` limitation is already documented in the script's own help — this scratch makes it observable. -- _Created by Claude Fable 5 via /planning:scratch._ +- *Created by Claude Fable 5 via /planning:scratch.* diff --git a/.scratch/testing-methodologies-foundation/issues/01-reference-doc-from-tdd-skill.md b/.scratch/testing-methodologies-foundation/issues/01-reference-doc-from-tdd-skill.md index 3b7d378..a916414 100644 --- a/.scratch/testing-methodologies-foundation/issues/01-reference-doc-from-tdd-skill.md +++ b/.scratch/testing-methodologies-foundation/issues/01-reference-doc-from-tdd-skill.md @@ -15,7 +15,7 @@ section pointing at `docs/testing-methodologies-foundation.adoc` as the canonica - `SKILL.md` names the foundation doc as its source-of-truth and links it via a git-tracked path. - The link resolves in the published repo (not just on this machine). -- After the edit: re-run `scripts/sync-skills.ps1` so the synced command copy carries the reference. +- After the edit: re-run `scripts/setup-repo.ps1 -SkipHooks` so the synced command copy carries the reference. ## Blocked by diff --git a/.scratch/understand-scratch-skill/PRD.md b/.scratch/understand-scratch-skill/PRD.md index fe22d00..308f5d8 100644 --- a/.scratch/understand-scratch-skill/PRD.md +++ b/.scratch/understand-scratch-skill/PRD.md @@ -16,20 +16,20 @@ its real behavior, not the one-line description. Establish ground truth before r - What files does quick-capture create/touch — `PRD.md`, the `BACKLOG.md` row, anything else? What status/ranking does a new entry get, and how is the slug chosen? - How does it relate to the sibling skills (`scratch-plan`, `to-issues`, `to-prd`, `triage`) and the - canonical [LAYOUT.md](../../shared/skills/planning/scratch/LAYOUT.md) they all reference? + canonical [LAYOUT.md](../../ai-artifacts/skills/shared/planning/scratch/LAYOUT.md) they all reference? - Does the skill's documented behavior match what's been happening in practice (e.g. the stub PRDs + TBD backlog rows captured this session)? Any gaps between the SKILL.md and actual effect? ## Notes -- Source of truth: `shared/skills/planning/scratch/SKILL.md` (+ `LAYOUT.md`, `RANKING.md`); deployed +- Source of truth: `ai-artifacts/skills/shared/planning/scratch/SKILL.md` (+ `LAYOUT.md`, `RANKING.md`); deployed mirror under `.claude/commands/planning/scratch/`. - Motivation seems to be calibrating trust in the scratch workflow before leaning on it more (cf. [[gated-work-prd-issue-approval]], [[capture-not-execute]]). ## Findings (investigation 2026-07-05) -Ground truth read from `shared/skills/planning/scratch/SKILL.md` (+ `LAYOUT.md`, `RANKING.md`) and +Ground truth read from `ai-artifacts/skills/shared/planning/scratch/SKILL.md` (+ `LAYOUT.md`, `RANKING.md`) and `scratch-plan/SKILL.md`, cross-checked against the actual `.scratch/` tree — and, for the sibling relationship, against the committed mattpocock upstream artifacts of `to-issues`/`to-prd`/`triage` (§3a). @@ -72,7 +72,7 @@ does no ranking. Ranking is `scratch-plan`'s job (§3). runs a one-question-at-a-time interview (priority / importance / effort, with fuzzy-input bucket rounding), computes `P × I × E`, and **rewrites** `BACKLOG.md` sorted with a `Last updated:` line. Division of labour: `scratch` = capture + display; `scratch-plan` = calibrate + rank. -- **`to-issues` / `to-prd` / `triage` aren't imported into `shared/skills/` yet** — but their +- **`to-issues` / `to-prd` / `triage` aren't imported into `ai-artifacts/skills/shared/` yet** — but their mattpocock **upstream versions are committed as artifacts** under `import-upstream-skills/artifacts/engineering/{to-issues,to-prd,triage}/SKILL.md` (plus the config docs `setup-matt-pocock-skills/{issue-tracker-local,triage-labels,domain}.md`). Import into @@ -141,7 +141,7 @@ conventions from `LAYOUT.md` directly plus a small triage-label map. `scratch`'s append. The LAYOUT template documents only the minimal header a fresh `BACKLOG.md` gets. - **Deployment caveat.** `/planning:scratch` only resolves after the generated mirror exists (`.claude/commands/planning/scratch.md`). On a fresh clone / non-Windows sandbox the mirror is - absent (gitignored; `pwsh scripts/sync-skills.ps1` not yet run) — so the *source* behavior above is + absent (gitignored; `pwsh scripts/setup-repo.ps1 -SkipHooks` not yet run) — so the *source* behavior above is authoritative, but the slash command itself may be unavailable until sync runs. ### Reevaluation (2026-07-06) @@ -152,7 +152,7 @@ User-requested recheck of the last open question ("how does it relate to the sib "Tracker-contract prerequisites" checklist on [`import-upstream-skills` issue 04](../import-upstream-skills/issues/04-import-planning-cluster.md). Verified against the repo today: the three siblings are still not imported -(`shared/skills/planning/` = `scratch` + `scratch-plan` only), so the §3a answer stands unchanged. +(`ai-artifacts/skills/shared/planning/` = `scratch` + `scratch-plan` only), so the §3a answer stands unchanged. Nothing remains in this scratch; follow-up work lives in [[import-upstream-skills]] issues 02/04. ### Verdict diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..08fc951 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "**/CODEOWNERS": "plaintext" + }, + "task.allowAutomaticTasks": "on" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b4c57b5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,38 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Bootstrap repo mirrors and hooks (if missing)", + "type": "shell", + "command": "pwsh", + "args": [ + "-NoProfile", + "-File", + ".github/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1", + "-IfMissing" + ], + "runOptions": { + "runOn": "folderOpen" + }, + "presentation": { + "reveal": "never", + "panel": "dedicated", + "focus": false, + "showReuseMessage": false, + "clear": false + }, + "problemMatcher": [] + }, + { + "label": "Bootstrap repo mirrors and hooks (full refresh)", + "type": "shell", + "command": "pwsh", + "args": [ + "-NoProfile", + "-File", + ".github/skills/setup_setup-repo/scripts/Invoke-SetupRepo.ps1" + ], + "problemMatcher": [] + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 1a7d30d..98bf42e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,8 +8,10 @@ of truth; each harness loads live copies from its own locations. Canonical reference: [docs/repo-layout.adoc](docs/repo-layout.adoc). Short version: -- `shared/` — vendor- and harness-agnostic artifacts (the default home; all skills live here) -- `//` — harness-specific config (e.g. `anthropic/claude-ai/instructions/`) +- `ai-artifacts/skills/shared/` — source skills; scope-specific skills go under `ai-artifacts/skills///` +- `ai-artifacts/instructions///` — repo copies of harness instruction surfaces +- `ai-artifacts/hooks/`, `ai-artifacts/mcp-config/`, `ai-artifacts/output-styles/`, `ai-artifacts/agents/`, `ai-artifacts/prompts/`, `ai-artifacts/plugins/` — artifact-type roots, each scoped below by + `shared/`, `/`, or `//` - `docs/harnesses/` — per-harness self-descriptions: instruction surfaces, load models, limits - `.scratch/` — committed local-markdown issue tracker (PRDs + issues + ranked `BACKLOG.md`) - `.temp/` — gitignored landing zone for transient local working files, downloads, and scratch output @@ -19,14 +21,14 @@ Most-specific wins; folders are created on demand, never pre-scaffolded empty. ## Rules that prevent damage -- **Never edit `.claude/commands/`** — it is a generated, gitignored mirror. Edit the source under - `shared/skills///`, then rebuild: `pwsh scripts/sync-skills.ps1`. -- **Never edit `.agents/skills/`** — it is the generated Codex skill mirror. Edit the source under - `shared/skills///`, then rebuild: `pwsh scripts/sync-skills.ps1`. -- On a fresh clone or a cloud/sandbox session the generated mirrors may not exist. Run the sync script once — +- **Never edit generated skill mirrors directly** (`.claude/commands/`, `.agents/skills/`, + `.github/skills/`) — these are generated, gitignored harness mirrors. Edit the source under + `ai-artifacts/skills/shared///`, then rebuild with `pwsh scripts/setup-repo.ps1`. +- For a fresh clone bootstrap, run `pwsh scripts/setup-repo.ps1` to enable git hooks and refresh mirrors in one pass. +- On a fresh clone or a cloud/sandbox session the generated mirrors may not exist. Run setup once — the SessionStart hook that does this locally lives in machine-local settings and won't be there. -- Files under `instructions/` and `anthropic/*/instructions/` are repo copies for editing; the live - version sits in each harness's own surface (see [instructions/README.md](instructions/README.md)). +- Files under `ai-artifacts/instructions///` are repo copies for editing; the live version sits + in each harness's own surface (see [ai-artifacts/instructions/README.md](ai-artifacts/instructions/README.md)). Editing the repo copy changes nothing until it is deployed there. - Use `git mv` when moving or renaming tracked files. - This repo is **public**. No secrets anywhere — including instruction files and `.scratch/`. @@ -46,25 +48,32 @@ explicitly requested against a specific scratch. The working rules for agents operating in `.scratch/` — capture≠execute, **deliverables live outside the scratch** (`artifacts/` is supporting material only), and ranking hygiene — live in the folder guide [.scratch/AGENTS.md](.scratch/AGENTS.md); structural layout stays in the `scratch` skill's -[LAYOUT.md](shared/skills/planning/scratch/LAYOUT.md). +[LAYOUT.md](ai-artifacts/skills/shared/planning/scratch/LAYOUT.md). ## Conventions - Primary environment: Windows, PowerShell 7 (`pwsh`). Write scripts in pwsh unless the target is cross-platform. +- PowerShell runtime policy: each script declares `# RuntimePolicy: core-first|dual-runtime|desktop-only`. + Core-first is default (`#Requires -Version 7.0` + `#Requires -PSEdition Core`), dual-runtime must run + in both Windows PowerShell 5.1 and pwsh 7+, and desktop-only exceptions must be named + `*-windowsps.ps1` with `# RuntimeJustification: ...` plus + `#Requires -Version 5.1` + `#Requires -PSEdition Desktop`. - Docs: Markdown by default; AsciiDoc (`docs/*.adoc`) where richer syntax is needed. +- Keep `AGENTS.md` Markdown because harnesses load it directly; keep `docs/repo-layout.adoc` + AsciiDoc because it is the richer canonical layout reference. - OKF: durable markdown reference/catalog docs should follow [docs/okf-adoption.md](docs/okf-adoption.md) unless another local format owns the file. - Skills follow the **capability contract**: if shell/filesystem is available, take the full agentic path; otherwise degrade to a conversational fallback. Write "if shell available" — - never "if ". + never "if [harness name]". - AI-generated code changes must follow the coding policies in `coding-policies/`: load `polyglot-policy.yaml` first, then the resolved language policy from `coding-policies/languages/` per `usage-policy.yaml`. - Vendored/imported skill provenance lives in each skill's `METADATA.md`; the origin map is - [shared/skills/README.md](shared/skills/README.md). + [ai-artifacts/skills/shared/README.md](ai-artifacts/skills/shared/README.md). - **Single owner per fact:** each fact lives in one canonical file; other docs link to it instead - of restating (layout → `docs/repo-layout.adoc`, skill origins → `shared/skills/README.md`, + of restating (layout → `docs/repo-layout.adoc`, skill origins → `ai-artifacts/skills/shared/README.md`, scratch working rules → `.scratch/AGENTS.md`, scratch structural layout → the `scratch` skill's `LAYOUT.md`). @@ -81,4 +90,4 @@ hoist (`.scratch/incorporate-global-claude-setup/`) consolidates the shared subs - Keep changes scoped so only relevant workflows run; avoid touching unrelated files in the same PR. - CI lints exclude `.scratch/*/artefacts/**` and `.scratch/*/artifacts/**` on purpose; those paths are treated as supporting artifacts. Safety checks (secret/policy) still apply. -- Enable local pre-commit checks in your clone with `pwsh scripts/install-git-hooks.ps1`. +- Run `pwsh scripts/setup-repo.ps1` to bootstrap hooks and generated mirrors. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..8f93312 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,165 @@ +# AI Lab + +AI Lab is a source-of-truth repo for AI-assisted-work configuration. It organizes shared skills, +instruction surfaces, and harness-specific support artifacts so multiple AI harnesses can work from +one maintained repo. + +## Language + +### Core context + +**AI Lab**: +The repo itself: the maintained source of truth for AI-assisted-work configuration, documentation, +and sync tooling. +*Avoid*: workspace, toolkit, playground + +**Harness**: +A concrete AI runtime or product surface that loads instructions, tools, and config in its own +way. Claude Code, claude.ai, Copilot, and Codex are different harnesses. +*Avoid*: model, vendor, agent + +**Vendor**: +The platform/provider namespace above a harness, used for folder scoping such as `anthropic/` or +`openai/`. +*Avoid*: harness, runtime + +**Artifact type**: +A top-level repo family defined by what something is, not by who uses it. Examples include +`ai-artifacts/skills/`, `ai-artifacts/instructions/`, `ai-artifacts/mcp-config/`, +`ai-artifacts/prompts/`, and `ai-artifacts/plugins/`. +*Avoid*: bucket, misc, catch-all + +**Scope tier**: +The level at which an artifact applies: `shared/`, `/`, or `//`. +More-specific tiers override more-general ones. +*Avoid*: environment, layer + +### Skill system + +**Skill**: +An invocable, task-shaped instruction package with a `SKILL.md` and optional bundled resources. +In this repo, skills are grouped by intent under `ai-artifacts/skills/shared///`. +*Avoid*: script, prompt, command + +**Source skill**: +The maintained skill definition under `ai-artifacts/skills/shared/` or another scoped source folder. This is the +editable copy. +*Avoid*: mirror, generated skill + +**Generated mirror**: +A harness-specific build artifact produced from source skills, such as `.claude/commands/` or +`.agents/skills/`. +*Avoid*: source skill, canonical copy + +**Capability contract**: +The rule that a skill should take the full agentic path when shell/filesystem access exists and +degrade to a conversational fallback when it does not. +*Avoid*: harness-specific branch, hardcoded runtime path + +**Sync**: +The act of rebuilding generated mirrors from source artifacts, typically via +`pwsh scripts/setup-repo.ps1 -SkipHooks`. +*Avoid*: deploy, publish + +### Planning and work tracking + +**Scratch**: +The repo's committed local-markdown work tracker under `.scratch/`, used for PRDs, issues, ranking, +and support artifacts. +*Avoid*: backlog file, temp folder + +**PRD**: +Product Requirements Document. In this repo it is the primary planning document for a scratch +feature folder. +*Avoid*: issue, ADR, spec note + +**Issue**: +A smaller tracked work item inside a scratch feature, usually under `issues/`. +*Avoid*: PRD, artifact + +**Scratch artifact**: +Supporting material stored under `.scratch//artifacts/`. It exists to support planning or +analysis, not to be the final deliverable. +*Avoid*: deliverable, permanent home + +**Deliverable**: +A finished repo output that belongs in its real artifact-type home, such as `ai-artifacts/skills/`, `docs/`, or +`ai-artifacts/mcp-config/`, rather than inside `.scratch/`. +*Avoid*: scratch artifact, draft input + +### Instruction and config surfaces + +**Instruction surface**: +A file or location a harness actually reads as instructions. Repo copies are edited here, but the +live loaded copy may exist elsewhere. +*Avoid*: any markdown file, README + +**Repo copy**: +A source-controlled editing copy of a harness-owned artifact whose live version is stored outside +the repo. +*Avoid*: live version, generated mirror + +**Live version**: +The copy actually loaded by a harness at runtime, such as a profile field, settings location, or +generated mirror. +*Avoid*: repo copy, source file + +**MCP**: +Model Context Protocol. In this repo, MCP-related material belongs under `ai-artifacts/mcp-config/` when it is a +durable repo artifact. +*Avoid*: generic tool config, extension + +**Output style**: +A reusable asset that shapes how a model formats or frames its responses. +*Avoid*: skill, instruction surface + +### Documentation and provenance + +**ADR**: +Architecture Decision Record. It captures a hard-to-reverse, context-sensitive decision that would +otherwise be surprising later. +*Avoid*: PRD, issue, meeting note + +**OKF**: +Open Knowledge Format. In this repo it is the lightweight metadata/documentation convention used +where no harness-owned format already controls the file. +*Avoid*: frontmatter in general, runtime instruction format + +**Origin map**: +The human-readable summary of where skills came from, maintained in `ai-artifacts/skills/shared/README.md`. +*Avoid*: provenance file, metadata file + +## Flagged ambiguities + +**Artifact vs scratch artifact**: +An artifact in the broad sense is any repo asset. A scratch artifact is specifically supporting +material inside `.scratch//artifacts/` and is not the final home of deliverables. + +**Instruction vs settings**: +Instructions tell a harness how to behave. Settings configure a harness or tool. They may overlap in +purpose but do not belong in the same artifact-type folder by default. + +**Source of truth vs live version**: +The repo is the source of truth for maintained copies, but some live harness-loaded files exist +outside the repo. Source of truth does not mean every runtime file is read from the repo directly. + +**Vendor vs harness**: +The vendor is the provider namespace; the harness is the specific runtime/product surface beneath +it. `anthropic/claude-ai/` names both, in that order. + +## Example dialogue + +Dev: Should this Codex-specific config live in `.scratch/` until we wire it up? + +Domain expert: No. If it is a real deliverable, put it in its artifact-type home. For repo config, +that means something like `ai-artifacts/mcp-config/openai/codex/`. + +Dev: Then what belongs in `.scratch/`? + +Domain expert: The PRD, any follow-up issues, and supporting artifacts used to reason about the +change. `.scratch/` tracks the work; it is not the final home of the output. + +Dev: And if I need to adapt a skill for two harnesses? + +Domain expert: Edit the source skill first. The generated mirrors are build artifacts, so you sync +them after the source change rather than editing `.claude/commands/` or `.agents/skills/` directly. diff --git a/THIRD-PARTY/README.md b/THIRD-PARTY/README.md deleted file mode 100644 index 7837e6d..0000000 --- a/THIRD-PARTY/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Third-party notices - -This repo redistributes adapted copies and verbatim snapshots of upstream work. Each upstream's -license is preserved here; per-skill provenance (`upstream-*` frontmatter) records the exact -source path and pinned commit. The human-readable origin map is -[shared/skills/README.md](../shared/skills/README.md). - -| Upstream | License | Pinned commit | Where used in this repo | -|---|---|---|---| -| [mattpocock/skills](https://github.com/mattpocock/skills) | [MIT](mattpocock-skills.LICENSE) | `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` | Adapted skills under `shared/skills/` (tdd, prototype, caveman, grill-me, handoff, write-a-skill); verbatim snapshots under `.scratch/import-upstream-skills/artifacts/` and `.scratch/claude-code-skill-adaptation/artifacts/` | -| [bradautomates/claude-video](https://github.com/bradautomates/claude-video) | [MIT](bradautomates-claude-video.LICENSE) | `c333c2289e57bf040b32846f18d669e3f8edad9b` | Verbatim snapshot under `.scratch/add-watch-skill/artifacts/watch/` (not yet adapted) | - -When vendoring a new upstream, add its license file here (`-.LICENSE`) and a row to -this table in the same commit that lands the vendored content. diff --git a/THIRD-PARTY/bradautomates-claude-video.LICENSE b/THIRD-PARTY/bradautomates-claude-video.LICENSE deleted file mode 100644 index e23e30f..0000000 --- a/THIRD-PARTY/bradautomates-claude-video.LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Bradley Bonanno - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/THIRD-PARTY/mattpocock-skills.LICENSE b/THIRD-PARTY/mattpocock-skills.LICENSE deleted file mode 100644 index f1dd2c0..0000000 --- a/THIRD-PARTY/mattpocock-skills.LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Matt Pocock - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/VSCode_Extsion/adoc-preview-menu-1.0.0/extension.js b/VSCode_Extension/adoc-preview-menu-1.0.0/extension.js similarity index 100% rename from VSCode_Extsion/adoc-preview-menu-1.0.0/extension.js rename to VSCode_Extension/adoc-preview-menu-1.0.0/extension.js diff --git a/VSCode_Extsion/adoc-preview-menu-1.0.0/package.json b/VSCode_Extension/adoc-preview-menu-1.0.0/package.json similarity index 100% rename from VSCode_Extsion/adoc-preview-menu-1.0.0/package.json rename to VSCode_Extension/adoc-preview-menu-1.0.0/package.json diff --git a/ai-artifacts/agents/README.md b/ai-artifacts/agents/README.md new file mode 100644 index 0000000..5fc2376 --- /dev/null +++ b/ai-artifacts/agents/README.md @@ -0,0 +1,5 @@ +# Agents + +Agent and subagent definitions plus any durable support material they need. + +Use `ai-artifacts/agents/shared/`, `ai-artifacts/agents//`, or `ai-artifacts/agents///`. diff --git a/ai-artifacts/hooks/README.md b/ai-artifacts/hooks/README.md new file mode 100644 index 0000000..6ce7c45 --- /dev/null +++ b/ai-artifacts/hooks/README.md @@ -0,0 +1,6 @@ +# Hooks + +Source-controlled hook definitions for AI harnesses. + +Use `ai-artifacts/hooks/shared/`, `ai-artifacts/hooks//`, or `ai-artifacts/hooks///` when a hook artifact itself +belongs in the repo. Hook-related skills remain under `ai-artifacts/skills/`. diff --git a/ai-artifacts/instructions/README.md b/ai-artifacts/instructions/README.md new file mode 100644 index 0000000..3f23b8d --- /dev/null +++ b/ai-artifacts/instructions/README.md @@ -0,0 +1,26 @@ +# Instructions + +Harness-specific instruction files, keyed as `ai-artifacts/instructions///` (matching +`docs/harnesses/`). Each file here is the **repo copy for editing** — the live version lives in that +harness's own location. These are **not interchangeable**: different harnesses read different files, +and overlap between them is expected, not duplication to be merged. + +This folder is intentionally broader than the old name suggested: it holds claude.ai profile +instructions, project instructions, global `CLAUDE.md` copies, Copilot/Codex instruction surfaces, +and equivalent base instruction documents for other AI agents. + +Current tracked file: + +- `anthropic/claude-ai/profile.md` (claude.ai Chat tab). Live location: Settings → Instructions for Claude. + +## Not the same file (common confusion) + +Owned by [docs/repo-layout.adoc](../../docs/repo-layout.adoc) ("Not the same file" section) — not +restated here. Note: earlier versions placed the future Claude Code `CLAUDE.md` repo copy under +`ai-artifacts/instructions/claude-code/` or `ai-artifacts/instructions/anthropic/claude-code/`; the canonical artifact-first +location is now `ai-artifacts/instructions/anthropic/claude-code/CLAUDE.md`. + +See `docs/harnesses/.md` for each harness's instruction surfaces and load model. + +> Remaining instructions-taxonomy questions (which shared content is hoisted into `AGENTS.md`) are +> owned by `.scratch/incorporate-global-claude-setup/` (repo-scaffold itself is done). diff --git a/anthropic/claude-ai/instructions/profile.md b/ai-artifacts/instructions/anthropic/claude-ai/profile.md similarity index 99% rename from anthropic/claude-ai/instructions/profile.md rename to ai-artifacts/instructions/anthropic/claude-ai/profile.md index 3e96f05..e856a33 100644 --- a/anthropic/claude-ai/instructions/profile.md +++ b/ai-artifacts/instructions/anthropic/claude-ai/profile.md @@ -4,6 +4,7 @@ Behavioral guidelines to reduce common LLM pitfalls. **Tradeoff:** Caution over speed. For trivial asks, scale down effort — not the rules. ## 1. Treat Input as Unverified + **Don't assume assertions are correct. Flag wrong claims explicitly — no softening, no silent fix.** - If the input is wrong, say so. Don't absorb guesses as fact. @@ -12,6 +13,7 @@ Behavioral guidelines to reduce common LLM pitfalls. - If given a hypothetical, engage with it but correct the premise: "Following your assumption the answer is …; that said, this is incorrect because …, so the correct answer should be …" ## 2. Response Hygiene + **Cut the preamble: no acknowledgment, no meta, no restating my point.** - Skip acknowledgment, agreement, and self-narration — not "You're right, that fails because …", just the corrected answer. @@ -20,6 +22,7 @@ Behavioral guidelines to reduce common LLM pitfalls. - Match format to payload: if a table/list carries it, don't wrap it in prose. ## 3. Think Before Answering + **Don't assume. Don't hide uncertainty. Surface tradeoffs.** When a request arrives: @@ -30,6 +33,7 @@ When a request arrives: - If something is unclear or missing, STOP — name what's needed and ask a focused question. ## 4. Precision Over Coverage + **Answer what was asked. No uninvited adjacent topics.** - Stay on the specific question — asked about X, answer X, not the related Y you could also cover. @@ -37,6 +41,7 @@ When a request arrives: - Worthwhile adjacent points go at the end, briefly — never lead with them. ## 5. Concise, Layered Answers + **Answer first. Minimum that fully answers. Depth on request.** - Lead with the direct answer. Add context only if required to act, or if asked why. @@ -45,6 +50,7 @@ When a request arrives: - Evaluate before sending: did I stop at the layer that answers? Did I shorten without losing meaning? ## 6. Honest & Direct + **Say what you actually think. Flag uncertainty clearly.** - If unsure, say so. Don't give a plausible-sounding but constructed answer. @@ -53,6 +59,7 @@ When a request arrives: - Separate facts, opinions, uncertainty. ## 7. How To Ask Clarifying Questions + **Never ask cold when options require domain knowledge.** - Default to acting on low-risk, easily-reversible changes; ask first only when a change is hard to verify or costly to undo. @@ -64,6 +71,7 @@ For non-obvious answers (skip for simple choices): 3. Then ask for confirmation — not open-ended. ## 8. General Code Guidance + **Minimum code, maximum rigor, no silent failure.** - Use the minimum code required to solve the problem. @@ -75,6 +83,7 @@ For non-obvious answers (skip for simple choices): - Separate output from logging: don't mix stdout and stderr unless explicitly required; use appropriate log levels / status streams. ## 9. Surface Conventions Are Mine + **Match source surface style; propose changes, don't silently apply them.** - Mirror punctuation, unicode, emoji, and formatting already in my message or the file being edited — e.g. if I write " - " and "...", don't swap them for "—" and "…". Don't normalize or "improve" it. diff --git a/ai-artifacts/mcp-config/README.md b/ai-artifacts/mcp-config/README.md new file mode 100644 index 0000000..67a8bbf --- /dev/null +++ b/ai-artifacts/mcp-config/README.md @@ -0,0 +1,5 @@ +# MCP Config + +Source-controlled MCP server manifests, connection definitions, and adjacent support files. + +Use `ai-artifacts/mcp-config/shared/`, `ai-artifacts/mcp-config//`, or `ai-artifacts/mcp-config///`. diff --git a/ai-artifacts/output-styles/README.md b/ai-artifacts/output-styles/README.md new file mode 100644 index 0000000..876ec31 --- /dev/null +++ b/ai-artifacts/output-styles/README.md @@ -0,0 +1,5 @@ +# Output Styles + +Reusable output-format and response-style assets that shape model output. + +Use `ai-artifacts/output-styles/shared/`, `ai-artifacts/output-styles//`, or `ai-artifacts/output-styles///`. diff --git a/ai-artifacts/plugins/README.md b/ai-artifacts/plugins/README.md new file mode 100644 index 0000000..268ff0d --- /dev/null +++ b/ai-artifacts/plugins/README.md @@ -0,0 +1,6 @@ +# Plugins + +Plugin packaging, manifests, and durable support material. + +Use `ai-artifacts/plugins/shared/`, `ai-artifacts/plugins//`, or +`ai-artifacts/plugins///`. diff --git a/ai-artifacts/prompts/README.md b/ai-artifacts/prompts/README.md new file mode 100644 index 0000000..ed6f3c2 --- /dev/null +++ b/ai-artifacts/prompts/README.md @@ -0,0 +1,5 @@ +# Prompts + +Reusable prompts and prompt packs that are not instruction surfaces. + +Use `ai-artifacts/prompts/shared/`, `ai-artifacts/prompts//`, or `ai-artifacts/prompts///`. diff --git a/shared/skills/README.md b/ai-artifacts/skills/shared/README.md similarity index 86% rename from shared/skills/README.md rename to ai-artifacts/skills/shared/README.md index 7a0284a..13e1ca1 100644 --- a/shared/skills/README.md +++ b/ai-artifacts/skills/shared/README.md @@ -6,26 +6,27 @@ the `/setup:check-skill-updates` skill, and anything about that process lives in ## Layout -`shared/skills///SKILL.md` (+ bundled runtime resources) is the **single source of -truth** for skill behavior. `shared/skills///METADATA.md` is the OKF-style catalog and +`ai-artifacts/skills/shared///SKILL.md` (+ bundled runtime resources) is the **single source of +truth** for skill behavior. `ai-artifacts/skills/shared///METADATA.md` is the OKF-style catalog and provenance file for that skill. The invocable copies under generated harness mirrors are build artifacts — never edit them; edit the source and re-run: ```powershell -pwsh scripts/sync-skills.ps1 +pwsh scripts/setup-repo.ps1 -SkipHooks ``` That updates every supported generated skill mirror. `METADATA.md` files are source catalog files and are not deployed as runtime skill resources: - Claude Code: `.claude/commands//.md` plus resources -- Codex: `.agents/skills/-/SKILL.md` plus resources +- Codex: `.agents/skills/_/SKILL.md` plus resources +- Copilot: `.github/skills/_/SKILL.md` plus resources -Both generated trees are gitignored; never edit either directly. Use -`pwsh scripts/sync-skills.ps1 -Target Claude` or `-Target Codex` only when you intentionally want a +All generated trees are gitignored; never edit them directly. Use +`pwsh scripts/setup-repo.ps1 -SkipHooks -Target Claude`, `-Target Codex`, or `-Target Copilot` only when you intentionally want a single mirror. -Namespacing follows the directory: `shared/skills/coding/tdd/` → `/coding:tdd`. +Namespacing follows the directory: `ai-artifacts/skills/shared/coding/tdd/` → `/coding:tdd`. | Group | Intent | | ----------- | ----------------------------------------------------------------- | @@ -33,6 +34,7 @@ Namespacing follows the directory: `shared/skills/coding/tdd/` → `/coding:tdd` | `planning` | Backlog / PRD / issue workflow (the `.scratch/` tracker) | | `session` | Conversational / process skills that shape a working session | | `setup` | Repo tooling and skill maintenance | +| `workflow` | Running deterministic local workflow/CI-equivalent sequences | | `documents` | Producing / converting documents (e.g. email → AsciiDoc/Markdown) | ## Skill Metadata @@ -76,8 +78,10 @@ table is the human-readable summary. | `recon` | session | — (local original) | No upstream | | `check-skill-updates` | setup | — (local original) | No upstream; the update tool itself | | `import-upstream-skill` | setup | — (local original) | No upstream; the generic import process itself | +| `setup-repo` | setup | — (local original) | Self-contained bootstrap skill for clone setup (hooks + mirror generation in one command) | | `git-guardrails` | setup | mattpocock `skills/misc/git-guardrails-claude-code` | Localized from the global-prior (pwsh + bash guards); Claude-Code-hook skill, N/A in chat. Exact upstream checkpoint lives only in the skill's `METADATA.md` | | `setup-pre-commit` | setup | mattpocock `skills/misc/setup-pre-commit` (**local fork**) | Diverged entirely: `pre-commit` framework for PS/MD/AsciiDoc/SQL, not Husky/lint-staged/Prettier. Carries **no** `upstream-*` (lineage in a comment); `check-skill-updates` skips it | +| `simulate-workflows` | workflow | — (local original) | Deterministic script that runs local CI-equivalent workflow checks (Python, PowerShell, linting) | | `scratch` | planning | — (local original) | The `.scratch/` tracker; owns `LAYOUT.md` / `RANKING.md` | | `scratch-plan` | planning | — (local original) | Backlog ranking companion to `scratch` | | `mail-to-adoc` | documents | — (local original) | `.msg`/`.eml` → AsciiDoc; personal-workflow tool (redacted). Rename to `mail-to-doc` + Markdown target is `.scratch/mail-to-doc` issue 03 | diff --git a/shared/skills/coding/diagnose/METADATA.md b/ai-artifacts/skills/shared/coding/diagnose/METADATA.md similarity index 95% rename from shared/skills/coding/diagnose/METADATA.md rename to ai-artifacts/skills/shared/coding/diagnose/METADATA.md index 64bcb75..4ab7fe4 100644 --- a/shared/skills/coding/diagnose/METADATA.md +++ b/ai-artifacts/skills/shared/coding/diagnose/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/engineering/diagnose/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `coding` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Dual-mode capability contract; HITL loop ships pwsh (primary) and bash templates. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/coding/diagnose/SKILL.md b/ai-artifacts/skills/shared/coding/diagnose/SKILL.md similarity index 96% rename from shared/skills/coding/diagnose/SKILL.md rename to ai-artifacts/skills/shared/coding/diagnose/SKILL.md index 8f6de63..e0b2129 100644 --- a/shared/skills/coding/diagnose/SKILL.md +++ b/ai-artifacts/skills/shared/coding/diagnose/SKILL.md @@ -43,8 +43,8 @@ Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give 7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. 8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. 9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. -10. **HITL script.** Last resort. If a human must click, drive _them_ with a structured loop — - `scripts/hitl-loop.template.ps1` (pwsh, primary) or `scripts/hitl-loop.template.sh` (bash). With a +10. **HITL script.** Last resort. If a human must click, drive *them* with a structured loop — + `templates/hitl-loop.template.ps1` (pwsh, primary) or `templates/hitl-loop.template.sh` (bash). With a shell, generate and run it; without one, hand the user the same numbered steps in chat and collect their answers. Captured output feeds back to you. @@ -52,7 +52,7 @@ Build the right feedback loop, and the bug is 90% fixed. ### Iterate on the loop itself -Treat the loop as a product. Once you have _a_ loop, ask: +Treat the loop as a product. Once you have *a* loop, ask: - Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) - Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) diff --git a/shared/skills/coding/diagnose/scripts/hitl-loop.template.ps1 b/ai-artifacts/skills/shared/coding/diagnose/templates/hitl-loop.template.ps1 similarity index 94% rename from shared/skills/coding/diagnose/scripts/hitl-loop.template.ps1 rename to ai-artifacts/skills/shared/coding/diagnose/templates/hitl-loop.template.ps1 index b66e9ce..c577531 100644 --- a/shared/skills/coding/diagnose/scripts/hitl-loop.template.ps1 +++ b/ai-artifacts/skills/shared/coding/diagnose/templates/hitl-loop.template.ps1 @@ -1,3 +1,7 @@ +#Requires -Version 7.0 +#Requires -PSEdition Core +# RuntimePolicy: core-first + # Human-in-the-loop reproduction loop (PowerShell 7 primary; pwsh on Windows/macOS/Linux). # Copy this file, edit the steps below, and run it: pwsh hitl-loop.template.ps1 # The agent runs the script; the user follows prompts in their terminal. diff --git a/shared/skills/coding/diagnose/scripts/hitl-loop.template.sh b/ai-artifacts/skills/shared/coding/diagnose/templates/hitl-loop.template.sh similarity index 100% rename from shared/skills/coding/diagnose/scripts/hitl-loop.template.sh rename to ai-artifacts/skills/shared/coding/diagnose/templates/hitl-loop.template.sh diff --git a/shared/skills/coding/improve-codebase-architecture/METADATA.md b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/METADATA.md similarity index 95% rename from shared/skills/coding/improve-codebase-architecture/METADATA.md rename to ai-artifacts/skills/shared/coding/improve-codebase-architecture/METADATA.md index beb1ab4..9c66f77 100644 --- a/shared/skills/coding/improve-codebase-architecture/METADATA.md +++ b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/engineering/improve-codebase-architecture/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `coding` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Dual-mode report workflow; grill-with-docs links repointed to session/grill-me. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/coding/improve-codebase-architecture/SKILL.md b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/SKILL.md similarity index 87% rename from shared/skills/coding/improve-codebase-architecture/SKILL.md rename to ai-artifacts/skills/shared/coding/improve-codebase-architecture/SKILL.md index cb4d800..31bb000 100644 --- a/shared/skills/coding/improve-codebase-architecture/SKILL.md +++ b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/SKILL.md @@ -22,7 +22,7 @@ The analysis is identical; only how you gather code and deliver the report chang ## Glossary -Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md). +Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](docs/LANGUAGE.md). - **Module** — anything with an interface and an implementation (function, class, package, slice). - **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature. @@ -33,13 +33,13 @@ Use these terms exactly in every suggestion. Consistent language is the point - **Leverage** — what callers get from depth. - **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place. -Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list): +Key principles (see [LANGUAGE.md](docs/LANGUAGE.md) for the full list): - **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. - **The interface is the test surface.** - **One adapter = hypothetical seam. Two adapters = real seam.** -This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. +This skill is *informed* by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. ## Process @@ -81,11 +81,11 @@ For each candidate, render a card with: End the report with a **Top recommendation** section: which candidate you'd tackle first and why. -**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." +**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](docs/LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." -**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. +**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: *"contradicts ADR-0007 — but worth reopening because…"*). Don't list every theoretical refactor an ADR forbids. -See [HTML-REPORT.md](HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance. +See [HTML-REPORT.md](docs/HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance. Do NOT propose interfaces yet. After the report is delivered, ask the user: "Which of these would you like to explore?" @@ -95,7 +95,7 @@ Once the user picks a candidate, drop into a grilling conversation. Walk the des Side effects happen inline as decisions crystallize (with a filesystem: edit the files; without one: propose the exact text to add): -- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/session:grill-me` (see [CONTEXT-FORMAT.md](../../session/grill-me/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist. +- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/session:grill-me` (see [CONTEXT-FORMAT.md](../../session/grill-me/docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist. - **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. -- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../../session/grill-me/ADR-FORMAT.md). -- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). +- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: *"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"* Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../../session/grill-me/docs/ADR-FORMAT.md). +- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](docs/INTERFACE-DESIGN.md). diff --git a/shared/skills/coding/improve-codebase-architecture/DEEPENING.md b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/DEEPENING.md similarity index 100% rename from shared/skills/coding/improve-codebase-architecture/DEEPENING.md rename to ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/DEEPENING.md diff --git a/shared/skills/coding/improve-codebase-architecture/HTML-REPORT.md b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/HTML-REPORT.md similarity index 100% rename from shared/skills/coding/improve-codebase-architecture/HTML-REPORT.md rename to ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/HTML-REPORT.md diff --git a/shared/skills/coding/improve-codebase-architecture/INTERFACE-DESIGN.md b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/INTERFACE-DESIGN.md similarity index 100% rename from shared/skills/coding/improve-codebase-architecture/INTERFACE-DESIGN.md rename to ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/INTERFACE-DESIGN.md diff --git a/shared/skills/coding/improve-codebase-architecture/LANGUAGE.md b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/LANGUAGE.md similarity index 94% rename from shared/skills/coding/improve-codebase-architecture/LANGUAGE.md rename to ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/LANGUAGE.md index 530c276..de64f8b 100644 --- a/shared/skills/coding/improve-codebase-architecture/LANGUAGE.md +++ b/ai-artifacts/skills/shared/coding/improve-codebase-architecture/docs/LANGUAGE.md @@ -6,11 +6,11 @@ Shared vocabulary for every suggestion this skill makes. Use these terms exactly **Module** Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice. -_Avoid_: unit, component, service. +*Avoid*: unit, component, service. **Interface** Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. -_Avoid_: API, signature (too narrow — those refer only to the type-level surface). +*Avoid*: API, signature (too narrow — those refer only to the type-level surface). **Implementation** What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. @@ -18,9 +18,9 @@ What's inside a module — its body of code. Distinct from **Adapter**: a thing **Depth** Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation. -**Seam** _(from Michael Feathers)_ +**Seam** *(from Michael Feathers)* A place where you can alter behaviour without editing in that place. The *location* at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it. -_Avoid_: boundary (overloaded with DDD's bounded context). +*Avoid*: boundary (overloaded with DDD's bounded context). **Adapter** A concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside). diff --git a/shared/skills/coding/prototype/METADATA.md b/ai-artifacts/skills/shared/coding/prototype/METADATA.md similarity index 96% rename from shared/skills/coding/prototype/METADATA.md rename to ai-artifacts/skills/shared/coding/prototype/METADATA.md index 027d232..7ebbc1a 100644 --- a/shared/skills/coding/prototype/METADATA.md +++ b/ai-artifacts/skills/shared/coding/prototype/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/engineering/prototype/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `coding` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Localized. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/coding/prototype/SKILL.md b/ai-artifacts/skills/shared/coding/prototype/SKILL.md similarity index 97% rename from shared/skills/coding/prototype/SKILL.md rename to ai-artifacts/skills/shared/coding/prototype/SKILL.md index a44af08..0569169 100644 --- a/shared/skills/coding/prototype/SKILL.md +++ b/ai-artifacts/skills/shared/coding/prototype/SKILL.md @@ -1,8 +1,8 @@ ---- -name: prototype -version: 1.0.0 -description: Build a throwaway prototype to flesh out a design before committing to it. Build a tiny interactive terminal app that drives a state model, data shape, command surface, or output format by hand — pushing it through cases that are hard to reason about on paper. Use when the user wants to prototype, sanity-check a data model or state machine, feel out an API/cmdlet surface or SQL schema, explore an idea, or says "prototype this", "let me play with it", "does this shape feel right". ---- +--- +name: prototype +version: 1.0.0 +description: Build a throwaway prototype to flesh out a design before committing to it. Build a tiny interactive terminal app that drives a state model, data shape, command surface, or output format by hand — pushing it through cases that are hard to reason about on paper. Use when the user wants to prototype, sanity-check a data model or state machine, feel out an API/cmdlet surface or SQL schema, explore an idea, or says "prototype this", "let me play with it", "does this shape feel right". +--- # Prototype @@ -22,7 +22,7 @@ Typical questions it answers: 1. **Throwaway from day one, and marked as such.** Put it next to the module it's prototyping for so context is obvious, but name it so a casual reader sees it's a prototype, not production (`proto-.ps1`, `Proto/`, `proto_.py`). 2. **One command to run.** Via the project's existing task runner or a single documented invocation — `pwsh ./proto-.ps1`, `dotnet run --project Proto`, `python proto_.py`. The user starts it without thinking. Don't add a new runtime or package manager just for the prototype. -3. **No persistence by default.** State lives in memory. Persistence is usually the thing being _checked_, not depended on. If the question is specifically about persistence, hit a scratch target with an obvious throwaway name (`tempdb` table `Proto_WipeMe`, a `proto-wipe-me.db`), never a real one. +3. **No persistence by default.** State lives in memory. Persistence is usually the thing being *checked*, not depended on. If the question is specifically about persistence, hit a scratch target with an obvious throwaway name (`tempdb` table `Proto_WipeMe`, a `proto-wipe-me.db`), never a real one. 4. **Skip the polish.** No tests, no abstractions, no error handling beyond what makes it runnable. The point is to learn fast and delete. 5. **Surface the state.** Re-render the full relevant state after every action so the user sees exactly what changed. 6. **Delete or absorb when done.** Once it has answered its question, fold the validated decision into the real code or delete it — don't leave it rotting in the repo. @@ -75,7 +75,7 @@ Add it to the project's existing task runner if there is one (`*.psd1`/build scr ### 6. Hand it over -Give the run command. The user drives it; the valuable moments are "wait, that shouldn't be possible" or "huh, I assumed X would differ" — those are bugs in the _idea_, which is the whole point. If they want new actions, add them. Prototypes evolve. +Give the run command. The user drives it; the valuable moments are "wait, that shouldn't be possible" or "huh, I assumed X would differ" — those are bugs in the *idea*, which is the whole point. If they want new actions, add them. Prototypes evolve. ### 7. Capture the answer diff --git a/shared/skills/coding/tdd/METADATA.md b/ai-artifacts/skills/shared/coding/tdd/METADATA.md similarity index 96% rename from shared/skills/coding/tdd/METADATA.md rename to ai-artifacts/skills/shared/coding/tdd/METADATA.md index 1833bd0..b34ac8b 100644 --- a/shared/skills/coding/tdd/METADATA.md +++ b/ai-artifacts/skills/shared/coding/tdd/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/engineering/tdd/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `coding` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Heavily localized: stack rules (PowerShell/SQL/Python/C#), reworked resources. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/coding/tdd/SKILL.md b/ai-artifacts/skills/shared/coding/tdd/SKILL.md similarity index 86% rename from shared/skills/coding/tdd/SKILL.md rename to ai-artifacts/skills/shared/coding/tdd/SKILL.md index 19d8bb9..8be4104 100644 --- a/shared/skills/coding/tdd/SKILL.md +++ b/ai-artifacts/skills/shared/coding/tdd/SKILL.md @@ -3,86 +3,86 @@ name: tdd version: 1.0.0 description: Test-driven development applied as a workflow across unit, integration, and acceptance levels, with on-demand stack-specific rules for PowerShell, SQL, Python, and C#. Use when building features or fixing bugs test-first, when writing or reviewing tests, when choosing test doubles, when picking a companion technique (property-based, mutation, contract, approval, snapshot testing), or when the user mentions TDD, red-green-refactor, BDD, acceptance tests, or mocking. --- - -# Test-Driven Development - -These are my operating rules for test and development work. Apply them; do -not restate them back to me; do not soften them. When a request of mine -conflicts with a rule, say so and ask before proceeding rather than -silently complying. - -## Core Frame - -- Tests do three jobs: **verification**, **design feedback**, **specification**. - Identify which job applies before suggesting a test approach. -- TDD's primary value is design feedback and incremental progress. - Verification is a side effect. Do not pitch TDD as "a way to find bugs." -- BDD is TDD with behavior-focused vocabulary and an outside-in perspective. - It is not a separate methodology and not defined by Gherkin/Cucumber. -- TDD is a **workflow** (write failing test → pass → refactor), not a scope. - It applies at unit, integration, acceptance, and E2E levels. Do not equate - TDD with unit testing, and do not reject it because something cannot be - unit-tested. -- A test must **fail on behavior change and pass on structure change.** If a - proposed test would break on a legitimate refactor, reject it and propose a - behavior-level alternative. - -## Workflow - -### 1. Plan - -- Confirm what interface changes are needed and which behaviors matter most. - I cannot test everything — focus on critical paths and complex logic. -- List behaviors to test (not implementation steps). -- Design interfaces for testability — see [design-for-testability.md](design-for-testability.md). -- Use the project's domain vocabulary for test names; respect existing ADRs - and the existing test suite's style. -- **Load the stack file for the language in play, and only that one:** - [PowerShell](stacks/powershell.md) · [SQL](stacks/sql.md) · - [Python](stacks/python.md) · [C#](stacks/csharp.md). Do not read stack files - for languages not in use. -- Get my approval on the plan before writing code. - -### 2. Tracer bullet - -Write ONE test that confirms ONE thing end-to-end. Red → green. This proves -the path works before you build on it. - -### 3. Incremental loop - -For each remaining behavior: red → green, one test at a time, minimal code, -no anticipation of future tests. **Drive vertical, not horizontal** — one -test, one implementation, learn, then the next test. Never write all tests -first then all code (see [reviewing-and-cycle.md](reviewing-and-cycle.md)). - -### 4. Refactor - -Only once green (never while red). Look for: duplication, long methods, -shallow modules to deepen, logic that belongs where its data lives, and what -the new code reveals about existing code. Run tests after each step. -Refactor is non-optional — skipping it degrades TDD into "tests plus mess." - -## Per-Cycle Checklist - -``` -[ ] Test describes behavior, not implementation -[ ] Test uses the public interface only -[ ] Test would survive an internal refactor -[ ] Red was a real assertion failure, not just a compile error -[ ] Code is minimal for this test; no speculative features -[ ] Structural and behavioral changes are not mixed in one step -``` - -## Reference Files (load as needed) - -- [reviewing-and-cycle.md](reviewing-and-cycle.md) — rules for writing/reviewing - tests and the red-green-refactor cycle (strategies, what counts as red). -- [test-doubles.md](test-doubles.md) — mock/stub/fake selection, mock at - boundaries, third-party wrapping, classicist vs London. -- [companions.md](companions.md) — when to suggest property-based, mutation, - contract, approval, snapshot, and integration testing. -- [behaviors.md](behaviors.md) — what not to do, how to handle ambiguous - requests, and output format when producing tests. -- [design-for-testability.md](design-for-testability.md) — interface design - that makes tests natural. -- `stacks/.md` — stack-specific rules; load only the one in use. + +# Test-Driven Development + +These are my operating rules for test and development work. Apply them; do +not restate them back to me; do not soften them. When a request of mine +conflicts with a rule, say so and ask before proceeding rather than +silently complying. + +## Core Frame + +- Tests do three jobs: **verification**, **design feedback**, **specification**. + Identify which job applies before suggesting a test approach. +- TDD's primary value is design feedback and incremental progress. + Verification is a side effect. Do not pitch TDD as "a way to find bugs." +- BDD is TDD with behavior-focused vocabulary and an outside-in perspective. + It is not a separate methodology and not defined by Gherkin/Cucumber. +- TDD is a **workflow** (write failing test → pass → refactor), not a scope. + It applies at unit, integration, acceptance, and E2E levels. Do not equate + TDD with unit testing, and do not reject it because something cannot be + unit-tested. +- A test must **fail on behavior change and pass on structure change.** If a + proposed test would break on a legitimate refactor, reject it and propose a + behavior-level alternative. + +## Workflow + +### 1. Plan + +- Confirm what interface changes are needed and which behaviors matter most. + I cannot test everything — focus on critical paths and complex logic. +- List behaviors to test (not implementation steps). +- Design interfaces for testability — see [design-for-testability.md](docs/design-for-testability.md). +- Use the project's domain vocabulary for test names; respect existing ADRs + and the existing test suite's style. +- **Load the stack file for the language in play, and only that one:** + [PowerShell](stacks/powershell.md) · [SQL](stacks/sql.md) · + [Python](stacks/python.md) · [C#](stacks/csharp.md). Do not read stack files + for languages not in use. +- Get my approval on the plan before writing code. + +### 2. Tracer bullet + +Write ONE test that confirms ONE thing end-to-end. Red → green. This proves +the path works before you build on it. + +### 3. Incremental loop + +For each remaining behavior: red → green, one test at a time, minimal code, +no anticipation of future tests. **Drive vertical, not horizontal** — one +test, one implementation, learn, then the next test. Never write all tests +first then all code (see [reviewing-and-cycle.md](docs/reviewing-and-cycle.md)). + +### 4. Refactor + +Only once green (never while red). Look for: duplication, long methods, +shallow modules to deepen, logic that belongs where its data lives, and what +the new code reveals about existing code. Run tests after each step. +Refactor is non-optional — skipping it degrades TDD into "tests plus mess." + +## Per-Cycle Checklist + +```text +[ ] Test describes behavior, not implementation +[ ] Test uses the public interface only +[ ] Test would survive an internal refactor +[ ] Red was a real assertion failure, not just a compile error +[ ] Code is minimal for this test; no speculative features +[ ] Structural and behavioral changes are not mixed in one step +``` + +## Reference Files (load as needed) + +- [reviewing-and-cycle.md](docs/reviewing-and-cycle.md) — rules for writing/reviewing + tests and the red-green-refactor cycle (strategies, what counts as red). +- [test-doubles.md](docs/test-doubles.md) — mock/stub/fake selection, mock at + boundaries, third-party wrapping, classicist vs London. +- [companions.md](docs/companions.md) — when to suggest property-based, mutation, + contract, approval, snapshot, and integration testing. +- [behaviors.md](docs/behaviors.md) — what not to do, how to handle ambiguous + requests, and output format when producing tests. +- [design-for-testability.md](docs/design-for-testability.md) — interface design + that makes tests natural. +- `stacks/.md` — stack-specific rules; load only the one in use. diff --git a/shared/skills/coding/tdd/behaviors.md b/ai-artifacts/skills/shared/coding/tdd/docs/behaviors.md similarity index 100% rename from shared/skills/coding/tdd/behaviors.md rename to ai-artifacts/skills/shared/coding/tdd/docs/behaviors.md diff --git a/shared/skills/coding/tdd/companions.md b/ai-artifacts/skills/shared/coding/tdd/docs/companions.md similarity index 100% rename from shared/skills/coding/tdd/companions.md rename to ai-artifacts/skills/shared/coding/tdd/docs/companions.md diff --git a/shared/skills/coding/tdd/design-for-testability.md b/ai-artifacts/skills/shared/coding/tdd/docs/design-for-testability.md similarity index 100% rename from shared/skills/coding/tdd/design-for-testability.md rename to ai-artifacts/skills/shared/coding/tdd/docs/design-for-testability.md diff --git a/shared/skills/coding/tdd/reviewing-and-cycle.md b/ai-artifacts/skills/shared/coding/tdd/docs/reviewing-and-cycle.md similarity index 100% rename from shared/skills/coding/tdd/reviewing-and-cycle.md rename to ai-artifacts/skills/shared/coding/tdd/docs/reviewing-and-cycle.md diff --git a/shared/skills/coding/tdd/test-doubles.md b/ai-artifacts/skills/shared/coding/tdd/docs/test-doubles.md similarity index 78% rename from shared/skills/coding/tdd/test-doubles.md rename to ai-artifacts/skills/shared/coding/tdd/docs/test-doubles.md index cfe3c15..ff7d04b 100644 --- a/shared/skills/coding/tdd/test-doubles.md +++ b/ai-artifacts/skills/shared/coding/tdd/docs/test-doubles.md @@ -45,13 +45,13 @@ ## Double Taxonomy (Meszaros) -| Kind | Purpose | -| --- | --- | -| Dummy | Fills a parameter slot; never used. | -| Stub | Returns canned answers. Use when the code needs specific inputs from a collaborator. | -| Spy | A stub that records how it was called. Use when verifying an interaction occurred. | -| Mock | A spy with pre-set expectations; fails if they are not met. Strictest. | -| Fake | A working but production-unsuitable implementation (in-memory DB/queue). | +| Kind | Purpose | +| ----- | ------------------------------------------------------------------------------------ | +| Dummy | Fills a parameter slot; never used. | +| Stub | Returns canned answers. Use when the code needs specific inputs from a collaborator. | +| Spy | A stub that records how it was called. Use when verifying an interaction occurred. | +| Mock | A spy with pre-set expectations; fails if they are not met. Strictest. | +| Fake | A working but production-unsuitable implementation (in-memory DB/queue). | Using a mock where a stub would do creates over-specified tests that fail on irrelevant changes. Using a stub where a mock is needed leaves interaction bugs diff --git a/shared/skills/coding/tdd/stacks/csharp.md b/ai-artifacts/skills/shared/coding/tdd/stacks/csharp.md similarity index 100% rename from shared/skills/coding/tdd/stacks/csharp.md rename to ai-artifacts/skills/shared/coding/tdd/stacks/csharp.md diff --git a/shared/skills/coding/tdd/stacks/powershell.md b/ai-artifacts/skills/shared/coding/tdd/stacks/powershell.md similarity index 100% rename from shared/skills/coding/tdd/stacks/powershell.md rename to ai-artifacts/skills/shared/coding/tdd/stacks/powershell.md diff --git a/shared/skills/coding/tdd/stacks/python.md b/ai-artifacts/skills/shared/coding/tdd/stacks/python.md similarity index 100% rename from shared/skills/coding/tdd/stacks/python.md rename to ai-artifacts/skills/shared/coding/tdd/stacks/python.md diff --git a/shared/skills/coding/tdd/stacks/sql.md b/ai-artifacts/skills/shared/coding/tdd/stacks/sql.md similarity index 100% rename from shared/skills/coding/tdd/stacks/sql.md rename to ai-artifacts/skills/shared/coding/tdd/stacks/sql.md diff --git a/shared/skills/coding/zoom-out/METADATA.md b/ai-artifacts/skills/shared/coding/zoom-out/METADATA.md similarity index 94% rename from shared/skills/coding/zoom-out/METADATA.md rename to ai-artifacts/skills/shared/coding/zoom-out/METADATA.md index afb7a50..b328086 100644 --- a/shared/skills/coding/zoom-out/METADATA.md +++ b/ai-artifacts/skills/shared/coding/zoom-out/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/engineering/zoom-out/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `coding` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Prompt-only; dual-mode note added. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/coding/zoom-out/SKILL.md b/ai-artifacts/skills/shared/coding/zoom-out/SKILL.md similarity index 100% rename from shared/skills/coding/zoom-out/SKILL.md rename to ai-artifacts/skills/shared/coding/zoom-out/SKILL.md diff --git a/shared/skills/documents/mail-to-adoc/METADATA.md b/ai-artifacts/skills/shared/documents/mail-to-adoc/METADATA.md similarity index 85% rename from shared/skills/documents/mail-to-adoc/METADATA.md rename to ai-artifacts/skills/shared/documents/mail-to-adoc/METADATA.md index 8ea06b5..e3751e7 100644 --- a/shared/skills/documents/mail-to-adoc/METADATA.md +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/METADATA.md @@ -6,9 +6,9 @@ resource: ./SKILL.md tags: [documents, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `documents` - Origin: local -- Notes: Local original; personal workflow tool. Beta maturity. \ No newline at end of file +- Notes: Local original; personal workflow tool. Beta maturity. diff --git a/shared/skills/documents/mail-to-adoc/README.md b/ai-artifacts/skills/shared/documents/mail-to-adoc/README.md similarity index 100% rename from shared/skills/documents/mail-to-adoc/README.md rename to ai-artifacts/skills/shared/documents/mail-to-adoc/README.md diff --git a/shared/skills/documents/mail-to-adoc/SKILL.md b/ai-artifacts/skills/shared/documents/mail-to-adoc/SKILL.md similarity index 94% rename from shared/skills/documents/mail-to-adoc/SKILL.md rename to ai-artifacts/skills/shared/documents/mail-to-adoc/SKILL.md index 2d3362b..dce356a 100644 --- a/shared/skills/documents/mail-to-adoc/SKILL.md +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/SKILL.md @@ -34,7 +34,7 @@ pip install extract-msg Add MD5 checksums of attachments to ignore (e.g. Outlook signature images) to: -``` +```text .github/skills/mail-to-adoc/attachment-blocklist.txt ``` @@ -53,8 +53,8 @@ python '.github/skills/mail-to-adoc/scripts/mail_to_adoc.py' ' python '.github/skills/mail-to-adoc/scripts/mail_to_adoc.py' '' '' ``` -3. Open the generated `.adoc` file to review. -4. The source file is automatically moved to `mails/converted/`. +1. Open the generated `.adoc` file to review. +2. The source file is automatically moved to `mails/converted/`. ## Notes diff --git a/shared/skills/documents/mail-to-adoc/attachment-blocklist.txt b/ai-artifacts/skills/shared/documents/mail-to-adoc/attachment-blocklist.txt similarity index 100% rename from shared/skills/documents/mail-to-adoc/attachment-blocklist.txt rename to ai-artifacts/skills/shared/documents/mail-to-adoc/attachment-blocklist.txt diff --git a/shared/skills/documents/mail-to-adoc/docs/prd/PRD.md b/ai-artifacts/skills/shared/documents/mail-to-adoc/docs/prd/PRD.md similarity index 95% rename from shared/skills/documents/mail-to-adoc/docs/prd/PRD.md rename to ai-artifacts/skills/shared/documents/mail-to-adoc/docs/prd/PRD.md index 14f3299..7b75c4f 100644 --- a/shared/skills/documents/mail-to-adoc/docs/prd/PRD.md +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/docs/prd/PRD.md @@ -33,18 +33,18 @@ Email formats vary by client and encoding. .msg and .eml representations of the - The tool shall optionally accept an explicit output .adoc path. - The tool shall return a clear error for unsupported extensions. -2. Output naming and placement +1. Output naming and placement - Without explicit output path, output filename shall be auto-derived from date + source stem. - If source is in a raw directory, output shall be placed in the parent directory. -3. Metadata extraction +1. Metadata extraction - The output shall include mail metadata table fields where available: - From, Sent, To, Reply-To, CC, BCC, Importance, Sensitivity, Categories. - Subject shall be used as document title and colon spacing shall be escaped for AsciiDoc safety. -4. Body extraction and normalization +1. Body extraction and normalization - .msg conversion shall prefer plain text body, then HTML fallback. - .eml conversion shall prefer HTML body, then plain text fallback. @@ -52,7 +52,7 @@ Email formats vary by client and encoding. .msg and .eml representations of the - Body normalization shall remove null bytes, NBSP artifacts, invisible unicode, repeated blank lines, and Outlook-specific noise blocks. - Quoted header blocks shall be prefixed with [%hardbreaks] for readability. -5. Attachment processing +1. Attachment processing - All non-inline attachments shall be inspected. - Attachments shall be deduplicated by MD5 checksum. @@ -61,14 +61,14 @@ Email formats vary by client and encoding. .msg and .eml representations of the - Image attachments shall be rendered as image:: entries with links. - Other attachments shall be rendered as link: entries. -6. Nested mail attachment processing +1. Nested mail attachment processing - Attached .msg/.eml files shall not be written to docs/. - Attached .msg/.eml files shall be written to mails/raw/. - Nested mail attachments shall be converted recursively to .adoc. - Parent document attachment list shall link to nested conversion output when available, otherwise to raw source. -7. Sibling parity conversion and comparison +1. Sibling parity conversion and comparison - If counterpart extension exists for the same stem (foo.msg + foo.eml), both shall be converted. - Tool shall compare generated outputs and print: @@ -76,7 +76,7 @@ Email formats vary by client and encoding. .msg and .eml representations of the - Similarity ratio when different - Limited unified diff preview -8. Source lifecycle +1. Source lifecycle - After successful conversion run, the primary source file shall be moved to a converted subfolder unless already inside converted. diff --git a/shared/skills/documents/mail-to-adoc/docs/prd/PRD_EDGE_CASES.md b/ai-artifacts/skills/shared/documents/mail-to-adoc/docs/prd/PRD_EDGE_CASES.md similarity index 81% rename from shared/skills/documents/mail-to-adoc/docs/prd/PRD_EDGE_CASES.md rename to ai-artifacts/skills/shared/documents/mail-to-adoc/docs/prd/PRD_EDGE_CASES.md index 9b5a76c..cbc5e17 100644 --- a/shared/skills/documents/mail-to-adoc/docs/prd/PRD_EDGE_CASES.md +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/docs/prd/PRD_EDGE_CASES.md @@ -12,123 +12,123 @@ Define edge-case behavior required to faithfully recreate the skill. - Expected: fail fast with explicit file-not-found error. -2. Unsupported extension +1. Unsupported extension - Expected: fail fast and print supported extensions (.msg, .eml). -3. Explicit output path provided +1. Explicit output path provided - Expected: use explicit path and skip auto-name for primary file. -4. Source file already in converted folder +1. Source file already in converted folder - Expected: do not re-move source. ### Date and Naming -5. Missing or unparsable date +1. Missing or unparsable date - Expected: output name derived from stem only, no date prefix. -6. Source stem already includes date prefix +1. Source stem already includes date prefix - Expected: avoid double-prefixing by trimming known date forms. -7. Filename collisions in output dirs +1. Filename collisions in output dirs - Expected: append numeric suffix until unique. ### Metadata Normalization -8. Folded headers in .eml (RFC line wrapping) +1. Folded headers in .eml (RFC line wrapping) - Expected: unfolded values appear as single-line fields. -9. Address fields with mixed display-name/email forms +1. Address fields with mixed display-name/email forms - Expected: parsed, normalized, sorted representation. -10. More than 10 recipients in a field +1. More than 10 recipients in a field - Expected: comma-separated single-cell rendering. ### Body Extraction and Cleanup -11. .msg has empty plain text but valid HTML +1. .msg has empty plain text but valid HTML - Expected: HTML fallback used. -12. .eml has both HTML and plain text +1. .eml has both HTML and plain text - Expected: HTML chosen. -13. HTML contains style/script/comment/o:p/conditional blocks +1. HTML contains style/script/comment/o:p/conditional blocks - Expected: removed before conversion. -14. Body contains null bytes, NBSP, zero-width chars +1. Body contains null bytes, NBSP, zero-width chars - Expected: stripped or normalized. -15. Body has long runs of blank lines +1. Body has long runs of blank lines - Expected: collapsed to stable spacing. -16. Reply chain header block in body +1. Reply chain header block in body - Expected: [%hardbreaks] inserted before each header run. ### Attachment Handling -17. Inline image with filename +1. Inline image with filename - Expected: skipped as inline artifact. -18. Duplicate attachment content with different names +1. Duplicate attachment content with different names - Expected: dedupe by checksum; link to existing stored file. -19. Blocklisted attachment checksum +1. Blocklisted attachment checksum - Expected: skipped without writing file. -20. Attachment filename with directory traversal pattern +1. Attachment filename with directory traversal pattern - Expected: sanitized to basename before writing. -21. Image attachment +1. Image attachment - Expected: rendered with image:: thumbnail and click-through link. -22. Non-image attachment +1. Non-image attachment - Expected: rendered as link: entry. ### Nested Mail Attachments -23. Attached .msg or .eml present +1. Attached .msg or .eml present - Expected: write to mails/raw, convert nested mail, link to nested .adoc. -24. Nested conversion fails +1. Nested conversion fails - Expected: warning logged; parent links to raw nested source. -25. Duplicate nested mail attachment by checksum +1. Duplicate nested mail attachment by checksum - Expected: existing raw file reused. ### Sibling Pair Comparison -26. Input file has sibling counterpart extension +1. Input file has sibling counterpart extension - Expected: both converted; comparison status printed. -27. Outputs differ +1. Outputs differ - Expected: similarity percentage + bounded diff preview printed. -28. Outputs identical +1. Outputs identical - Expected: IDENTICAL status printed. diff --git a/shared/skills/documents/mail-to-adoc/scripts/Invoke-MailToAdoc.ps1 b/ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/Invoke-MailToAdoc.ps1 similarity index 99% rename from shared/skills/documents/mail-to-adoc/scripts/Invoke-MailToAdoc.ps1 rename to ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/Invoke-MailToAdoc.ps1 index 05fce65..017dd4e 100644 --- a/shared/skills/documents/mail-to-adoc/scripts/Invoke-MailToAdoc.ps1 +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/Invoke-MailToAdoc.ps1 @@ -1,4 +1,6 @@ #Requires -Version 7 +#Requires -PSEdition Core +# RuntimePolicy: core-first <# .SYNOPSIS Batch-converts .eml/.msg files in a source folder to AsciiDoc via mail_to_adoc.py. diff --git a/shared/skills/documents/mail-to-adoc/scripts/Move-Belege.ps1 b/ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/Move-Belege.ps1 similarity index 93% rename from shared/skills/documents/mail-to-adoc/scripts/Move-Belege.ps1 rename to ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/Move-Belege.ps1 index 07a4ced..f8995fb 100644 --- a/shared/skills/documents/mail-to-adoc/scripts/Move-Belege.ps1 +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/Move-Belege.ps1 @@ -1,4 +1,6 @@ #Requires -Version 7 +#Requires -PSEdition Core +# RuntimePolicy: core-first <# .SYNOPSIS Moves files from a staging folder to their target folder, removing duplicates. @@ -52,8 +54,8 @@ $ErrorActionPreference = 'Stop' $Root = (Get-Location).Path -if (-not $SourceDir) { $SourceDir = Join-Path $Root '.new\Abrechnungen' } -if (-not $DestDir) { $DestDir = Join-Path $Root '02_Auskunft-Einkommen\Gehaltsabrechnungen' } +if (-not $SourceDir) { $SourceDir = Join-Path (Join-Path $Root '.new') 'Abrechnungen' } +if (-not $DestDir) { $DestDir = Join-Path (Join-Path $Root '02_Auskunft-Einkommen') 'Gehaltsabrechnungen' } if (-not (Test-Path $SourceDir)) { throw "SourceDir not found: $SourceDir" } diff --git a/shared/skills/documents/mail-to-adoc/scripts/mail_to_adoc.py b/ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/mail_to_adoc.py similarity index 88% rename from shared/skills/documents/mail-to-adoc/scripts/mail_to_adoc.py rename to ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/mail_to_adoc.py index ce62ba9..79c8c89 100644 --- a/shared/skills/documents/mail-to-adoc/scripts/mail_to_adoc.py +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/scripts/mail_to_adoc.py @@ -19,17 +19,17 @@ try: _LOCAL_TZ = ZoneInfo("Europe/Berlin") except Exception: - raise SystemExit( - "Missing timezone data. Run: pip install tzdata" - ) + raise SystemExit("Missing timezone data. Run: pip install tzdata") _LOG_DIR = _PROJECT_ROOT / ".logs" #: Dennis's own email addresses — used to determine sent vs. received direction. #: [REDACTED for public repo — real values in the .temp/ originals] -_DENNIS_EMAILS: frozenset[str] = frozenset({ - "owner@example.com", - "owner.alt@example.com", -}) +_DENNIS_EMAILS: frozenset[str] = frozenset( + { + "owner@example.com", + "owner.alt@example.com", + } +) #: Maps known email addresses to short party names for filenames. #: [REDACTED for public repo — real values in the .temp/ originals] @@ -82,7 +82,6 @@ REPLY_HEADER_PATTERN = re.compile(r"^\*([^*]+):\*\s*", re.IGNORECASE) - def _log_warning(msg: str) -> None: """Print a warning and append it to .logs/mail_to_adoc.log.""" print(msg) @@ -90,6 +89,7 @@ def _log_warning(msg: str) -> None: _LOG_DIR.mkdir(parents=True, exist_ok=True) log_file = _LOG_DIR / "mail_to_adoc.log" from datetime import datetime + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(log_file, "a", encoding="utf-8") as f: f.write(f"[{timestamp}] {msg}\n") @@ -97,11 +97,11 @@ def _log_warning(msg: str) -> None: pass # log write failed silently - def _decode_rfc2047(value: str) -> str: """Decode RFC 2047 encoded-words in email headers, e.g. =?utf-8?Q?...?=""" from email.header import decode_header, make_header - if not value or '=?' not in value: + + if not value or "=?" not in value: return value try: return str(make_header(decode_header(value))) @@ -112,10 +112,13 @@ def _decode_rfc2047(value: str) -> str: def _decode_qp_body(text: str) -> str: """Decode any residual quoted-printable sequences (=XX) in plain text.""" import quopri - if '=' not in text: + + if "=" not in text: return text try: - return quopri.decodestring(text.encode('ascii', errors='replace')).decode('utf-8', errors='replace') + return quopri.decodestring(text.encode("ascii", errors="replace")).decode( + "utf-8", errors="replace" + ) except Exception: return text @@ -142,21 +145,24 @@ def __init__(self): # List rendering state — one entry per nesting level to support nested ul/ol self.list_depth = 0 self.list_type_stack: list[str] = [] # 'ul' or 'ol' for each open list - self.list_item_stack: list[list[str]] = ( - [] - ) # text fragments collected for each open
  • + self.list_item_stack: list[ + list[str] + ] = [] # text fragments collected for each open
  • # Table rendering state self.table_depth = 0 self.current_cell: list[str] | None = None # None when no / is open self.current_row_cells: list[str] = [] # cells accumulated for the current - self.table_rows: list[list[str]] = ( - [] - ) # all rows accumulated for the current + self.table_rows: list[ + list[str] + ] = [] # all rows accumulated for the current
    # Hyperlink state — href is set on , used while writing inner text, cleared on self.open_link_href: str | None = None + # Tags whose contents should be discarded entirely. + self.ignored_tag_depth = 0 + def _write(self, text: str) -> None: """Append text to the innermost active context: list item, table cell, or top level.""" if self.list_item_stack: @@ -168,6 +174,11 @@ def _write(self, text: str) -> None: def handle_starttag(self, tag, attrs): tag = tag.lower() + if tag in ("script", "style", "o:p"): + self.ignored_tag_depth += 1 + return + if self.ignored_tag_depth: + return attr_dict = dict(attrs) if tag == "table": self.table_depth += 1 @@ -200,6 +211,11 @@ def handle_starttag(self, tag, attrs): def handle_endtag(self, tag): tag = tag.lower() + if tag in ("script", "style", "o:p"): + self.ignored_tag_depth = max(0, self.ignored_tag_depth - 1) + return + if self.ignored_tag_depth: + return if tag == "table": if self.table_depth == 1 and self.table_rows: header_row = " ".join(f"|{c}" for c in self.table_rows[0]) @@ -264,6 +280,8 @@ def handle_endtag(self, tag): self._write("\n") def handle_data(self, data: str) -> None: + if self.ignored_tag_depth: + return data = re.sub( r"[\u200b\u200c\u200d\ufeff\u00ad]", "", data ) # zero-width / soft-hyphen chars @@ -284,12 +302,7 @@ def get_text(self) -> str: def strip_html(html: str) -> str: """Remove non-content HTML and convert the remainder to AsciiDoc markup.""" - html = re.sub(r"]*>.*?", "", html, flags=re.IGNORECASE | re.DOTALL) - html = re.sub( - r"]*>.*?", "", html, flags=re.IGNORECASE | re.DOTALL - ) html = re.sub(r"", "", html, flags=re.DOTALL) - html = re.sub(r"]*>.*?", "", html, flags=re.IGNORECASE | re.DOTALL) html = re.sub( r".*?", "", html, flags=re.IGNORECASE | re.DOTALL ) @@ -307,7 +320,6 @@ def normalize_body(body: str) -> str: return body - def _load_blocklist(script_dir: Path) -> set[str]: """Load MD5 checksums from attachment-blocklist.txt next to the skills folder.""" blocklist_path = script_dir.parent / "attachment-blocklist.txt" @@ -375,7 +387,6 @@ def _write_unique_file( return dest - def _clean_filename(subject: str) -> str: """Clean subject for filesystem use: 'AW: Foo' → 'AW_ Foo'. @@ -390,10 +401,10 @@ def _clean_filename(subject: str) -> str: _STEM_SEP = " — " # Bracket tags appended at the end of the filename stem (replaces the older emoji # markers ✉/✎𓂃/📎). Order in the stem: '{name} {direction}{[CC] if cc-only}{[+] if attachments}'. -_DIR_RECEIVED = "[FROM]" # mail Dennis received -_DIR_SENT = "[TO]" # mail Dennis sent -_CC_FLAG = "[CC]" # received mail where Dennis was only in Cc (not To) -_ATT_FLAG = "[+]" # mail has attachments +_DIR_RECEIVED = "[FROM]" # mail Dennis received +_DIR_SENT = "[TO]" # mail Dennis sent +_CC_FLAG = "[CC]" # received mail where Dennis was only in Cc (not To) +_ATT_FLAG = "[+]" # mail has attachments def _adoc_stem(mail_date, subject: str) -> str: @@ -425,7 +436,7 @@ def _meta_field(adoc: str, label: str) -> str: continue if m.group(1) == "a": # bulleted list continues on the following '* ' lines vals = [] - for nxt in lines[i + 1:]: + for nxt in lines[i + 1 :]: s = nxt.strip() if s.startswith("* "): vals.append(s[2:]) @@ -447,6 +458,7 @@ def _direction_tag( From/To header values extracted from the adoc metadata table; uses _DENNIS_EMAILS to detect sent mail and _EMAIL_NAME_MAP for party names. """ + def _extract_addr(field: str) -> str: m = re.search(r"<([^>]+)>", field) return (m.group(1) if m else field).strip().lower() @@ -469,8 +481,7 @@ def _name_for(field: str) -> str: from_addr = _extract_addr(from_v) if from_addr in _DENNIS_EMAILS: others = [ - a for a in re.findall(r"<([^>]+)>", to_v) - if a.lower() not in _DENNIS_EMAILS + a for a in re.findall(r"<([^>]+)>", to_v) if a.lower() not in _DENNIS_EMAILS ] _n = "MultipleRecipients" if len(others) > 1 else _name_for(to_v) return f"{_n} {_DIR_SENT}{_att}" @@ -637,7 +648,7 @@ def _addr_table_row(label: str, addr_str: str) -> str: return f"|{label} a|" + "\n".join(f"* {a}" for a in addrs) -_POSTPROCESS_LONG_LINE = 80 # lines longer than this are QP-wrapped paragraphs +_POSTPROCESS_LONG_LINE = 80 # lines longer than this are QP-wrapped paragraphs _POSTPROCESS_LIST_PREFIXES = ("* ", "** ", "*** ", ". ", ".. ", "... ", "- ") @@ -682,10 +693,22 @@ def _unwrap(m: re.Match) -> str: # thin rule in the rendered email. Insert AsciiDoc ''' before them. _REPLY_START = re.compile(r"^\*(Von|From|De|Van):\*", re.IGNORECASE) # All recognised reply-header field names (German + English). - _REPLY_HDR_KEYS = frozenset({ - "from", "sent", "to", "cc", "bcc", "subject", "date", - "von", "gesendet", "an", "betreff", "datum", - }) + _REPLY_HDR_KEYS = frozenset( + { + "from", + "sent", + "to", + "cc", + "bcc", + "subject", + "date", + "von", + "gesendet", + "an", + "betreff", + "datum", + } + ) def _is_reply_hdr(line: str) -> bool: m = re.match(r"^\*([^*]+):\*", line.strip(), re.IGNORECASE) @@ -712,21 +735,23 @@ def _is_reply_hdr(line: str) -> bool: # • Everything else that is followed by non-blank → add ' +' def _is_structural(line: str) -> bool: s = line.strip() - return (s.startswith("[") - or s in ("'''", "---", "***", "___") - or s.startswith("=")) + return ( + s.startswith("[") or s in ("'''", "---", "***", "___") or s.startswith("=") + ) lines = body.split("\n") out = [] for idx, line in enumerate(lines): raw = line.rstrip() next_line = lines[idx + 1] if idx + 1 < len(lines) else "" - if (raw - and next_line.strip() - and not raw.endswith(" +") - and not _is_structural(raw) - and not any(raw.lstrip().startswith(p) for p in _POSTPROCESS_LIST_PREFIXES) - and (_is_reply_hdr(raw) or len(raw) <= _POSTPROCESS_LONG_LINE)): + if ( + raw + and next_line.strip() + and not raw.endswith(" +") + and not _is_structural(raw) + and not any(raw.lstrip().startswith(p) for p in _POSTPROCESS_LIST_PREFIXES) + and (_is_reply_hdr(raw) or len(raw) <= _POSTPROCESS_LONG_LINE) + ): out.append(raw + " +") else: out.append(line) @@ -744,6 +769,7 @@ def _decode_thread_index(raw: str) -> str: Depth 0 = root message (no replies in chain yet). """ import base64 as _b64 + raw = raw.strip() if not raw: return "" @@ -759,18 +785,33 @@ def _decode_thread_index(raw: str) -> str: return raw - def _build_adoc( - subject: str, sender: str, to: str, cc: str, date: str, - attachment_links: list, body: str, - *, reply_to: str = "", bcc: str = "", importance: str = "", - sensitivity: str = "", categories: str = "", - thread_topic: str = "", thread_index: str = "", + subject: str, + sender: str, + to: str, + cc: str, + date: str, + attachment_links: list, + body: str, + *, + reply_to: str = "", + bcc: str = "", + importance: str = "", + sensitivity: str = "", + categories: str = "", + thread_topic: str = "", + thread_index: str = "", ) -> str: """Assemble the final AsciiDoc document from extracted mail fields.""" subject_escaped = subject.replace(": ", ": ") - lines = [f"= {subject_escaped}", "", "[%autowidth]", "|===", - f"|From |{sender}", f"|Sent |{date}"] + lines = [ + f"= {subject_escaped}", + "", + "[%autowidth]", + "|===", + f"|From |{sender}", + f"|Sent |{date}", + ] to_row = _addr_table_row("To ", to) if to_row: lines.append(to_row) @@ -799,7 +840,9 @@ def _build_adoc( lines += ["[NOTE]", "====", "*Attachments:*", ""] for orig_name, link_path, is_image in attachment_links: if is_image: - lines.append(f'image::{link_path}[{orig_name}, 120, link="{link_path}"]') + lines.append( + f'image::{link_path}[{orig_name}, 120, link="{link_path}"]' + ) else: lines.append(f"* link:{link_path}[{orig_name}]") lines += ["", "====", ""] @@ -831,7 +874,11 @@ def msg_to_adoc(msg_path: Path) -> str: # Prefer HTML (has charset) → fallback plain text html_bytes = msg.htmlBody if html_bytes: - html = _decode_msg_html(html_bytes) if isinstance(html_bytes, bytes) else html_bytes + html = ( + _decode_msg_html(html_bytes) + if isinstance(html_bytes, bytes) + else html_bytes + ) body = strip_html(html) else: body = msg.body or "" @@ -840,8 +887,11 @@ def msg_to_adoc(msg_path: Path) -> str: for att in msg.attachments: if getattr(att, "isInline", False): continue - name = re.sub(r"\s+", " ", - (att.longFilename or att.shortFilename or "").replace("\x00", "")).strip() + name = re.sub( + r"\s+", + " ", + (att.longFilename or att.shortFilename or "").replace("\x00", ""), + ).strip() if not name: continue raw_attachments.append((name, getattr(att, "data", None) or b"")) @@ -849,28 +899,57 @@ def msg_to_adoc(msg_path: Path) -> str: if mail_date is not None: # extract_msg may return a timezone-naive UTC datetime; convert to local. import datetime as _dt + if mail_date.tzinfo is None: mail_date = mail_date.replace(tzinfo=_dt.timezone.utc) mail_date = mail_date.astimezone(_LOCAL_TZ) reply_to = re.sub(r"[ \t]+", " ", getattr(msg, "reply_to", "") or "") bcc = re.sub(r"[ \t]+", " ", getattr(msg, "bcc", "") or "") - raw_imp = getattr(msg, "importanceText", None) or getattr(msg, "importance", None) - importance = (IMPORTANCE_LABELS.get(raw_imp, "") if isinstance(raw_imp, int) - else str(raw_imp).capitalize() if raw_imp else "") - raw_sens = getattr(msg, "sensitivityText", None) or getattr(msg, "sensitivity", None) - sensitivity = (SENSITIVITY_LABELS.get(raw_sens, "") if isinstance(raw_sens, int) - else str(raw_sens).capitalize() if raw_sens else "") + raw_imp = getattr(msg, "importanceText", None) or getattr( + msg, "importance", None + ) + importance = ( + IMPORTANCE_LABELS.get(raw_imp, "") + if isinstance(raw_imp, int) + else str(raw_imp).capitalize() + if raw_imp + else "" + ) + raw_sens = getattr(msg, "sensitivityText", None) or getattr( + msg, "sensitivity", None + ) + sensitivity = ( + SENSITIVITY_LABELS.get(raw_sens, "") + if isinstance(raw_sens, int) + else str(raw_sens).capitalize() + if raw_sens + else "" + ) categories = ", ".join(str(c) for c in (getattr(msg, "categories", None) or [])) thread_topic = (getattr(msg, "threadTopic", None) or "").strip() thread_index = (getattr(msg, "threadIndex", None) or "").strip() finally: msg.close() docs_dir = _PROJECT_ROOT / "01_Korrespondenz" / "Attachments" - attachment_links = process_attachments(raw_attachments, mail_date, docs_dir, msg_path) - return _build_adoc(subject, sender, to, cc, date, attachment_links, body, - reply_to=reply_to, bcc=bcc, importance=importance, - sensitivity=sensitivity, categories=categories, - thread_topic=thread_topic, thread_index=thread_index) + attachment_links = process_attachments( + raw_attachments, mail_date, docs_dir, msg_path + ) + return _build_adoc( + subject, + sender, + to, + cc, + date, + attachment_links, + body, + reply_to=reply_to, + bcc=bcc, + importance=importance, + sensitivity=sensitivity, + categories=categories, + thread_topic=thread_topic, + thread_index=thread_index, + ) def _unfold_header(value: str) -> str: @@ -915,16 +994,25 @@ def eml_to_adoc(eml_path: Path) -> str: payload = part.get_payload(decode=True) if isinstance(payload, (bytes, bytearray)) and payload: html_body = bytes(payload).decode( - part.get_content_charset() or "utf-8", errors="replace") + part.get_content_charset() or "utf-8", errors="replace" + ) elif content_type == "text/plain" and plain_body is None: payload = part.get_payload(decode=True) if isinstance(payload, (bytes, bytearray)) and payload: raw_plain = bytes(payload).decode( - part.get_content_charset() or "utf-8", errors="replace") + part.get_content_charset() or "utf-8", errors="replace" + ) # Some clients embed QP-encoded text without declaring CTE - plain_body = _decode_qp_body(raw_plain) if "=3D" in raw_plain or "=C3" in raw_plain else raw_plain - body = normalize_body(strip_html(html_body)) if html_body else ( - normalize_body(plain_body) if plain_body else "") + plain_body = ( + _decode_qp_body(raw_plain) + if "=3D" in raw_plain or "=C3" in raw_plain + else raw_plain + ) + body = ( + normalize_body(strip_html(html_body)) + if html_body + else (normalize_body(plain_body) if plain_body else "") + ) reply_to = _unfold_header(msg.get("Reply-To") or "") bcc = _unfold_header(msg.get("BCC") or msg.get("Bcc") or "") imp_hdr = msg.get("Importance") or msg.get("X-Priority") or "" @@ -934,11 +1022,25 @@ def eml_to_adoc(eml_path: Path) -> str: thread_topic = _decode_rfc2047(_unfold_header(msg.get("Thread-Topic") or "")) thread_index = _unfold_header(msg.get("Thread-Index") or "") docs_dir = _PROJECT_ROOT / "01_Korrespondenz" / "Attachments" - attachment_links = process_attachments(raw_attachments, mail_date, docs_dir, eml_path) - return _build_adoc(subject, sender, to, cc, date, attachment_links, body, - reply_to=reply_to, bcc=bcc, importance=importance, - sensitivity=sensitivity, categories=categories, - thread_topic=thread_topic, thread_index=thread_index) + attachment_links = process_attachments( + raw_attachments, mail_date, docs_dir, eml_path + ) + return _build_adoc( + subject, + sender, + to, + cc, + date, + attachment_links, + body, + reply_to=reply_to, + bcc=bcc, + importance=importance, + sensitivity=sensitivity, + categories=categories, + thread_topic=thread_topic, + thread_index=thread_index, + ) def main(): @@ -979,7 +1081,9 @@ def _emit(msg: str) -> None: _jr: dict = {} if not args: - print("Usage: python mail_to_adoc.py [--root ] [--overwrite] [--json] ") + print( + "Usage: python mail_to_adoc.py [--root ] [--overwrite] [--json] " + ) sys.exit(1) mail_path = Path(args[0]) @@ -1001,6 +1105,7 @@ def _emit(msg: str) -> None: # ── 2. Convert primary into .temp/ ──────────────────────────────────────── import tempfile as _tempfile + temp_dir = Path(_tempfile.gettempdir()) / "mail_to_adoc_temp" temp_dir.mkdir(parents=True, exist_ok=True) temp_primary = temp_dir / (stem + ".adoc") @@ -1014,10 +1119,14 @@ def _emit(msg: str) -> None: final_adoc = primary_adoc if counterpart.exists(): - import tempfile, shutil as _shutil - tmp_fd, tmp_name = tempfile.mkstemp(suffix=f".{counterpart_ext[1:]}.adoc", - prefix=stem + "_cmp_") - import os as _os; _os.close(tmp_fd) + import tempfile + + tmp_fd, tmp_name = tempfile.mkstemp( + suffix=f".{counterpart_ext[1:]}.adoc", prefix=stem + "_cmp_" + ) + import os as _os + + _os.close(tmp_fd) temp_cmp = Path(tmp_name) _, cmp_adoc = _write_converted_mail(counterpart, temp_cmp) _report_adoc_comparison(temp_primary, primary_adoc, temp_cmp, cmp_adoc) @@ -1038,7 +1147,7 @@ def _emit(msg: str) -> None: _fm = re.search(r"\|From\s*\|(.+)", final_adoc) _tm = re.search(r"\|To\s*\|(.+)", final_adoc) _from_v = _fm.group(1).strip() if _fm else "" - _to_v = _tm.group(1).strip() if _tm else "" + _to_v = _tm.group(1).strip() if _tm else "" _has_att = "*Attachments:*" in final_adoc # Dennis was only CC'd when he appears in the Cc field but not in To. _cc_only = _dennis_in(_meta_field(final_adoc, "CC")) and not _dennis_in( @@ -1050,6 +1159,7 @@ def _emit(msg: str) -> None: # ── 5. Copy from .temp/ to 01_Korrespondenz/{year}/{month}/ ───────────── import shutil as _shutil2 + out_dir = _PROJECT_ROOT / "01_Korrespondenz" / year / month out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / (full_stem + ".adoc") @@ -1057,7 +1167,9 @@ def _emit(msg: str) -> None: if out_path.exists(): try: existing = out_path.read_text(encoding="utf-8") - identical = existing.replace("\r\n", "\n") == final_adoc.replace("\r\n", "\n") + identical = existing.replace("\r\n", "\n") == final_adoc.replace( + "\r\n", "\n" + ) except OSError: identical = False # cloud-only file — treat as needing overwrite if identical: @@ -1066,7 +1178,9 @@ def _emit(msg: str) -> None: elif overwrite: # copy2 cannot open a cloud-only file for 'wb'; use rename instead: # write to a sibling temp file, then rename() over the cloud-only target. - import os as _os, tempfile as _tf + import os as _os + import tempfile as _tf + tmp_fd, tmp_name = _tf.mkstemp(dir=out_dir, prefix=".tmp_") _os.close(tmp_fd) _shutil2.copy2(str(temp_primary), tmp_name) @@ -1089,16 +1203,21 @@ def _emit(msg: str) -> None: # ── 6. Archive / rename source file ────────────────────────────────────── import filecmp as _filecmp + _parts = [p.lower() for p in mail_path.resolve().parts] _in_archive = any( - _parts[i] == "01_korrespondenz" and i + 1 < len(_parts) and _parts[i + 1] == "original" + _parts[i] == "01_korrespondenz" + and i + 1 < len(_parts) + and _parts[i + 1] == "original" for i in range(len(_parts) - 1) ) archive_ext = mail_path.suffix new_src_name = full_stem + archive_ext # match adoc stem if not _in_archive: - archive_dir = _PROJECT_ROOT / "01_Korrespondenz" / "Original" / f"{year}-{month}" + archive_dir = ( + _PROJECT_ROOT / "01_Korrespondenz" / "Original" / f"{year}-{month}" + ) archive_dir.mkdir(parents=True, exist_ok=True) dest = archive_dir / new_src_name @@ -1140,7 +1259,6 @@ def _emit(msg: str) -> None: _jr["archive_status"] = "skipped_rename" _emit(f"Umbenennung übersprungen (Ziel existiert): {dest.name}") - if json_mode: print(_json.dumps(_jr, ensure_ascii=True)) diff --git a/shared/skills/documents/mail-to-adoc/tests/README.md b/ai-artifacts/skills/shared/documents/mail-to-adoc/tests/README.md similarity index 100% rename from shared/skills/documents/mail-to-adoc/tests/README.md rename to ai-artifacts/skills/shared/documents/mail-to-adoc/tests/README.md diff --git a/shared/skills/documents/mail-to-adoc/tests/conftest.py b/ai-artifacts/skills/shared/documents/mail-to-adoc/tests/conftest.py similarity index 100% rename from shared/skills/documents/mail-to-adoc/tests/conftest.py rename to ai-artifacts/skills/shared/documents/mail-to-adoc/tests/conftest.py diff --git a/shared/skills/documents/mail-to-adoc/tests/test_mail_to_adoc.py b/ai-artifacts/skills/shared/documents/mail-to-adoc/tests/test_mail_to_adoc.py similarity index 93% rename from shared/skills/documents/mail-to-adoc/tests/test_mail_to_adoc.py rename to ai-artifacts/skills/shared/documents/mail-to-adoc/tests/test_mail_to_adoc.py index ed7d452..3010980 100644 --- a/shared/skills/documents/mail-to-adoc/tests/test_mail_to_adoc.py +++ b/ai-artifacts/skills/shared/documents/mail-to-adoc/tests/test_mail_to_adoc.py @@ -92,7 +92,9 @@ def test_process_attachments_skips_blocklist_and_dedupes_existing( msg_path=Path("mails/sample.eml"), ) - assert links == [("duplicate.pdf", "../../../docs/20260511_1452-evidence.pdf", False)] + assert links == [ + ("duplicate.pdf", "../../../docs/20260511_1452-evidence.pdf", False) + ] assert not (docs_dir / "20260511_1452-blocked.png").exists() @@ -130,6 +132,18 @@ def test_eml_to_adoc_prefers_html_and_writes_attachment_links(converter, tmp_pat assert f"* link:../../Attachments/{saved_name}[evidence.pdf]" in adoc +def test_strip_html_removes_script_tag_with_whitespace_before_closing_bracket( + converter, +): + html = "

    keep

    also keep

    " + + text = converter.strip_html(html) + + assert "alert('x')" not in text + assert "keep" in text + assert "also keep" in text + + # --- Filename direction/attachment markers (bracket tags, replacing emoji) ------ _DENNIS = "Dennis " diff --git a/shared/skills/planning/scratch-plan/METADATA.md b/ai-artifacts/skills/shared/planning/scratch-plan/METADATA.md similarity index 94% rename from shared/skills/planning/scratch-plan/METADATA.md rename to ai-artifacts/skills/shared/planning/scratch-plan/METADATA.md index 4bcf452..3ac06e2 100644 --- a/shared/skills/planning/scratch-plan/METADATA.md +++ b/ai-artifacts/skills/shared/planning/scratch-plan/METADATA.md @@ -6,7 +6,7 @@ resource: ./SKILL.md tags: [planning, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `planning` diff --git a/shared/skills/planning/scratch-plan/SKILL.md b/ai-artifacts/skills/shared/planning/scratch-plan/SKILL.md similarity index 88% rename from shared/skills/planning/scratch-plan/SKILL.md rename to ai-artifacts/skills/shared/planning/scratch-plan/SKILL.md index 04bc500..dc003ac 100644 --- a/shared/skills/planning/scratch-plan/SKILL.md +++ b/ai-artifacts/skills/shared/planning/scratch-plan/SKILL.md @@ -12,7 +12,7 @@ argument-hint: "[] — omit to review all features with TBD or ou # Scratch Plan Reviews `.scratch/` features and keeps the backlog current. Reads the ranking formula from -`skills/planning/scratch/RANKING.md` and the layout from `skills/planning/scratch/LAYOUT.md`. +`skills/planning/scratch/docs/RANKING.md` and the layout from `skills/planning/scratch/docs/LAYOUT.md`. ## Process @@ -41,15 +41,15 @@ If the user enters a non-standard value (e.g. "5h", "3 days"), round to the near midpoints between adjacent buckets (bucket hours: 4h=4, 1day=8, 2days=16, 1week=40, 2weeks=80, 1month=160, 2months=320): -| Input | Maps to | -|-------|---------| -| < 6h | 4h | -| 6h – < 12h | 1day | -| 12h – < 28h | 2days | -| 28h – < 60h | 1week | -| 60h – < 120h | 2weeks | -| 120h – < 240h | 1month | -| ≥ 240h | 2months | +| Input | Maps to | +| ------------- | ------- | +| < 6h | 4h | +| 6h – < 12h | 1day | +| 12h – < 28h | 2days | +| 28h – < 60h | 1week | +| 60h – < 120h | 2weeks | +| 120h – < 240h | 1month | +| ≥ 240h | 2months | Silently apply the rounding and confirm the mapped bucket to the user before continuing. @@ -72,7 +72,7 @@ When the user wants to raise a feature's rank outside the normal interview: ### 4. Rewrite BACKLOG.md After all interviews, compute each feature's score: `P × I × E` using the numeric values in -`skills/planning/scratch/RANKING.md`. Sort descending by score; apply tiebreakers in order: +`skills/planning/scratch/docs/RANKING.md`. Sort descending by score; apply tiebreakers in order: less effort → higher importance → higher priority → alphabetical. Rewrite `.scratch/BACKLOG.md` with the updated table, numbered ranks, and a `Last updated:` line. diff --git a/shared/skills/planning/scratch/METADATA.md b/ai-artifacts/skills/shared/planning/scratch/METADATA.md similarity index 94% rename from shared/skills/planning/scratch/METADATA.md rename to ai-artifacts/skills/shared/planning/scratch/METADATA.md index 045ce40..212cbeb 100644 --- a/shared/skills/planning/scratch/METADATA.md +++ b/ai-artifacts/skills/shared/planning/scratch/METADATA.md @@ -6,7 +6,7 @@ resource: ./SKILL.md tags: [planning, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `planning` diff --git a/shared/skills/planning/scratch/SKILL.md b/ai-artifacts/skills/shared/planning/scratch/SKILL.md similarity index 83% rename from shared/skills/planning/scratch/SKILL.md rename to ai-artifacts/skills/shared/planning/scratch/SKILL.md index 227427e..5412c40 100644 --- a/shared/skills/planning/scratch/SKILL.md +++ b/ai-artifacts/skills/shared/planning/scratch/SKILL.md @@ -5,7 +5,7 @@ description: > Manage .scratch/ feature entries and the backlog. Two modes: with $ARGUMENTS quick-captures a new feature idea as a stub PRD; without arguments, lists the ranked backlog. The canonical definition of the .scratch/ layout — other skills (to-issues, to-prd, triage, check-skill-updates) reference - [LAYOUT.md](scratch/LAYOUT.md) rather than restating the conventions. For re-ranking the backlog + [LAYOUT.md](docs/LAYOUT.md) rather than restating the conventions. For re-ranking the backlog use /planning:scratch-plan. argument-hint: "" --- @@ -24,7 +24,7 @@ When `$ARGUMENTS` is provided: 3. Create `.scratch//PRD.md` from the stub template below. 4. Append the feature to `.scratch/BACKLOG.md` with all ranking fields set to `TBD` and score `?`. If `BACKLOG.md` does not exist, create it from the template in - [LAYOUT.md](scratch/LAYOUT.md). + [LAYOUT.md](docs/LAYOUT.md). 5. Tell the user: `Created .scratch//. Run /planning:scratch-plan to set its rank.` ### PRD stub @@ -54,7 +54,7 @@ say so and suggest running with an argument to capture the first feature. ## Layout and ranking conventions -See [LAYOUT.md](scratch/LAYOUT.md) — the canonical `.scratch/` folder layout. Reference this from +See [LAYOUT.md](docs/LAYOUT.md) — the canonical `.scratch/` folder layout. Reference this from other skills; do not restate the conventions inline. -See [RANKING.md](scratch/RANKING.md) — the score formula and tiebreaker rules. +See [RANKING.md](docs/RANKING.md) — the score formula and tiebreaker rules. diff --git a/shared/skills/planning/scratch/LAYOUT.md b/ai-artifacts/skills/shared/planning/scratch/docs/LAYOUT.md similarity index 99% rename from shared/skills/planning/scratch/LAYOUT.md rename to ai-artifacts/skills/shared/planning/scratch/docs/LAYOUT.md index 9b45ccb..04bbd3e 100644 --- a/shared/skills/planning/scratch/LAYOUT.md +++ b/ai-artifacts/skills/shared/planning/scratch/docs/LAYOUT.md @@ -7,7 +7,7 @@ rather than restating the conventions. Every feature lives in its own folder: `.scratch//` -``` +```text .scratch/ BACKLOG.md ← ranked index of all features (repo-level, one file) / @@ -49,6 +49,7 @@ Supporting *inputs* only — reference material that feeds the work, **never the Deliverables (a skill, script, report, or durable finding) live in their proper repo home; the rule and the deliverable→home table are in the folder guide `.scratch/AGENTS.md`. No required structure; common sub-folders: + - `artifacts//` — upstream source files (when `.temp/` is gitignored) - `artifacts/global-prior/` — prior installed versions to mine for local customizations diff --git a/ai-artifacts/skills/shared/planning/scratch/docs/RANKING.md b/ai-artifacts/skills/shared/planning/scratch/docs/RANKING.md new file mode 100644 index 0000000..1f1f822 --- /dev/null +++ b/ai-artifacts/skills/shared/planning/scratch/docs/RANKING.md @@ -0,0 +1,52 @@ +# .scratch Ranking — Formula & Tiebreakers + +## Score formula + +```text +Score = P × I × E +``` + +| Axis | Value | Numeric | +| ------------------------------------------------------- | ------- | ------- | +| **P** (priority) | high | 3 | +| | medium | 2 | +| | low | 1 | +| **I** (importance) | high | 3 | +| | medium | 2 | +| | low | 1 | +| **E** (effort, **inverted** — less effort ranks higher) | 4h | 7 | +| | 1day | 6 | +| | 2days | 5 | +| | 1week | 4 | +| | 2weeks | 3 | +| | 1month | 2 | +| | 2months | 1 | + +Score range: 3 (low / low / 2months) — 63 (high / high / 4h). +Higher score = higher in the backlog. + +## Tiebreakers (equal score, applied in order) + +1. Less effort (lower E_label wins — quick wins first) +2. Higher importance +3. Higher priority +4. Alphabetical by feature slug + +## Escalation rule (used by `/planning:scratch-plan`) + +When a feature's rank should be raised: + +- If `importance < high` → raise importance one level. +- If `importance = high` → raise priority one level instead (if `priority < high`). + +This prevents phantom "super-high" rankings by routing excess urgency into priority. + +## Example + +| Feature | P | I | E | Score | +| -------------- | ---------- | ---------- | ----------- | ----- | +| auth-refactor | high (3) | high (3) | 1week (4) | 36 | +| fix-flaky-test | medium (2) | high (3) | 4h (7) | 42 | +| docs-overhaul | low (1) | medium (2) | 2months (1) | 2 | + +Ranked: fix-flaky-test (42) > auth-refactor (36) > docs-overhaul (2). diff --git a/shared/skills/session/caveman/METADATA.md b/ai-artifacts/skills/shared/session/caveman/METADATA.md similarity index 96% rename from shared/skills/session/caveman/METADATA.md rename to ai-artifacts/skills/shared/session/caveman/METADATA.md index e66ffb0..6e5ef03 100644 --- a/shared/skills/session/caveman/METADATA.md +++ b/ai-artifacts/skills/shared/session/caveman/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/productivity/caveman/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `session` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Minor edits. -# Citations +## Citations [1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/session/caveman/SKILL.md b/ai-artifacts/skills/shared/session/caveman/SKILL.md similarity index 93% rename from shared/skills/session/caveman/SKILL.md rename to ai-artifacts/skills/shared/session/caveman/SKILL.md index 0f60481..c25525f 100644 --- a/shared/skills/session/caveman/SKILL.md +++ b/ai-artifacts/skills/shared/session/caveman/SKILL.md @@ -27,11 +27,11 @@ Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" ### Examples -**"Why React component re-render?"** +#### "Why React component re-render?" > Inline obj prop -> new ref -> re-render. `useMemo`. -**"Explain database connection pooling."** +#### "Explain database connection pooling." > Pool = reuse DB conn. Skip handshake -> fast under load. diff --git a/shared/skills/session/grill-me/METADATA.md b/ai-artifacts/skills/shared/session/grill-me/METADATA.md similarity index 96% rename from shared/skills/session/grill-me/METADATA.md rename to ai-artifacts/skills/shared/session/grill-me/METADATA.md index 4bd1118..8792ba3 100644 --- a/shared/skills/session/grill-me/METADATA.md +++ b/ai-artifacts/skills/shared/session/grill-me/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/productivity/grill-me/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `session` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Absorbed engineering/grill-with-docs; upstream path tracks grill-me lineage only. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/session/grill-me/SKILL.md b/ai-artifacts/skills/shared/session/grill-me/SKILL.md similarity index 97% rename from shared/skills/session/grill-me/SKILL.md rename to ai-artifacts/skills/shared/session/grill-me/SKILL.md index 121fea2..244f98c 100644 --- a/shared/skills/session/grill-me/SKILL.md +++ b/ai-artifacts/skills/shared/session/grill-me/SKILL.md @@ -3,63 +3,64 @@ name: grill-me version: 1.0.0 description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Optionally challenges the plan against the project's existing language and drafts/revises documentation (CONTEXT.md, ADRs). Use when the user wants to stress-test a plan, get grilled on a design, says "grill me", or wants a plan checked against documented decisions and terminology. --- - - - -This skill runs in claude.ai (chat). There is no writable repo and no autonomous codebase access: the project filesystem cannot be edited in place, and nothing persists between conversations. Existing project files are available only when attached as Project knowledge or uploaded. Any documentation is *delivered as downloadable files* the user commits themselves — never written into their repo. - - - - - -Decide this once, at the start, before grilling — it governs the whole session: - -**Docs mode is OFF by default.** Run a pure grilling session: no glossary, no CONTEXT.md, no ADRs, no documentation output. Don't mention them. - -**Engage docs mode only if** either is true: -1. A `CONTEXT.md`, `CONTEXT-MAP.md`, or other project documentation is attached/uploaded, or -2. The user explicitly asks for documentation, a glossary, or ADRs, or signals it by phrasing such as "with docs", "using the docs", or "against the docs". - -If a documentable decision surfaces while docs mode is off, you may make **one** brief offer ("worth capturing this as an ADR?") and then drop it unless the user says yes. Never drift into glossary/ADR behavior unprompted. - - - - - -Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. - -Ask the questions one at a time, waiting for feedback on each before continuing. - -If a question can be answered from provided files (Project knowledge or uploads), check there first instead of asking. If the relevant code isn't provided, ask me to paste/upload it — or proceed on a stated assumption and flag it as unverified. - - - - - -Everything below applies **only when docs mode is engaged** (see mode-gate). - -## Domain awareness — three input states - -At the start, determine which applies from the files I've provided: - -1. **Baseline provided** — a `CONTEXT.md` (and/or `CONTEXT-MAP.md`, existing ADRs) is attached or uploaded. Read it and treat it as the authoritative baseline. Grill against its existing glossary; continue ADR numbering from the highest provided number. You revise *a copy* and hand it back — you cannot edit the original. -2. **Code/docs only** — source files but no glossary. Cross-reference claims against them; build a `CONTEXT.md` from scratch. -3. **Explicit request, nothing attached** — create a `CONTEXT.md` from scratch as terms resolve, working from my stated assumptions (flag each as unverified until I confirm). - -If a `CONTEXT-MAP.md` is provided, the project has multiple contexts; infer which one the current topic belongs to, and ask if unclear. Otherwise assume a single context. - -## During the session - -**Challenge against the glossary.** When I use a term that conflicts with the baseline `CONTEXT.md`, call it out: "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" - -**Sharpen fuzzy language.** When I use vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User?" - -**Discuss concrete scenarios.** Stress-test domain relationships with specific scenarios that probe edge cases and force precision about boundaries. - -**Cross-reference with provided code.** When I state how something works *and* the code is in the provided files, check whether the code agrees and surface contradictions. If the code isn't provided, don't guess — ask for it or flag the claim as unverified. - -**Maintain CONTEXT.md as we go.** Keep a working copy in the session. When a term resolves, update it and re-deliver the file as a download — don't wait and dump everything at the end. `CONTEXT.md` is a glossary and nothing else: no implementation details, not a spec, not a scratch pad. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). - -**Offer ADRs sparingly.** Only when all three are true: (1) hard to reverse, (2) surprising without context, (3) the result of a real trade-off. If any is missing, skip it. Deliver each ADR as a `docs/adr/NNNN-slug.md` file. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). - - + + + +This skill runs in claude.ai (chat). There is no writable repo and no autonomous codebase access: the project filesystem cannot be edited in place, and nothing persists between conversations. Existing project files are available only when attached as Project knowledge or uploaded. Any documentation is *delivered as downloadable files* the user commits themselves — never written into their repo. + + + + + +Decide this once, at the start, before grilling — it governs the whole session: + +**Docs mode is OFF by default.** Run a pure grilling session: no glossary, no CONTEXT.md, no ADRs, no documentation output. Don't mention them. + +**Engage docs mode only if** either is true: + +1. A `CONTEXT.md`, `CONTEXT-MAP.md`, or other project documentation is attached/uploaded, or +2. The user explicitly asks for documentation, a glossary, or ADRs, or signals it by phrasing such as "with docs", "using the docs", or "against the docs". + +If a documentable decision surfaces while docs mode is off, you may make **one** brief offer ("worth capturing this as an ADR?") and then drop it unless the user says yes. Never drift into glossary/ADR behavior unprompted. + + + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each before continuing. + +If a question can be answered from provided files (Project knowledge or uploads), check there first instead of asking. If the relevant code isn't provided, ask me to paste/upload it — or proceed on a stated assumption and flag it as unverified. + + + + + +Everything below applies **only when docs mode is engaged** (see mode-gate). + +## Domain awareness — three input states + +At the start, determine which applies from the files I've provided: + +1. **Baseline provided** — a `CONTEXT.md` (and/or `CONTEXT-MAP.md`, existing ADRs) is attached or uploaded. Read it and treat it as the authoritative baseline. Grill against its existing glossary; continue ADR numbering from the highest provided number. You revise *a copy* and hand it back — you cannot edit the original. +2. **Code/docs only** — source files but no glossary. Cross-reference claims against them; build a `CONTEXT.md` from scratch. +3. **Explicit request, nothing attached** — create a `CONTEXT.md` from scratch as terms resolve, working from my stated assumptions (flag each as unverified until I confirm). + +If a `CONTEXT-MAP.md` is provided, the project has multiple contexts; infer which one the current topic belongs to, and ask if unclear. Otherwise assume a single context. + +## During the session + +**Challenge against the glossary.** When I use a term that conflicts with the baseline `CONTEXT.md`, call it out: "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +**Sharpen fuzzy language.** When I use vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User?" + +**Discuss concrete scenarios.** Stress-test domain relationships with specific scenarios that probe edge cases and force precision about boundaries. + +**Cross-reference with provided code.** When I state how something works *and* the code is in the provided files, check whether the code agrees and surface contradictions. If the code isn't provided, don't guess — ask for it or flag the claim as unverified. + +**Maintain CONTEXT.md as we go.** Keep a working copy in the session. When a term resolves, update it and re-deliver the file as a download — don't wait and dump everything at the end. `CONTEXT.md` is a glossary and nothing else: no implementation details, not a spec, not a scratch pad. Use the format in [CONTEXT-FORMAT.md](docs/CONTEXT-FORMAT.md). + +**Offer ADRs sparingly.** Only when all three are true: (1) hard to reverse, (2) surprising without context, (3) the result of a real trade-off. If any is missing, skip it. Deliver each ADR as a `docs/adr/NNNN-slug.md` file. Use the format in [ADR-FORMAT.md](docs/ADR-FORMAT.md). + + diff --git a/shared/skills/session/grill-me/ADR-FORMAT.md b/ai-artifacts/skills/shared/session/grill-me/docs/ADR-FORMAT.md similarity index 100% rename from shared/skills/session/grill-me/ADR-FORMAT.md rename to ai-artifacts/skills/shared/session/grill-me/docs/ADR-FORMAT.md diff --git a/shared/skills/session/grill-me/CONTEXT-FORMAT.md b/ai-artifacts/skills/shared/session/grill-me/docs/CONTEXT-FORMAT.md similarity index 100% rename from shared/skills/session/grill-me/CONTEXT-FORMAT.md rename to ai-artifacts/skills/shared/session/grill-me/docs/CONTEXT-FORMAT.md diff --git a/shared/skills/session/handoff/METADATA.md b/ai-artifacts/skills/shared/session/handoff/METADATA.md similarity index 94% rename from shared/skills/session/handoff/METADATA.md rename to ai-artifacts/skills/shared/session/handoff/METADATA.md index ca6e0c8..2e9e955 100644 --- a/shared/skills/session/handoff/METADATA.md +++ b/ai-artifacts/skills/shared/session/handoff/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/productivity/handoff/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `session` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Minor edits. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/session/handoff/SKILL.md b/ai-artifacts/skills/shared/session/handoff/SKILL.md similarity index 100% rename from shared/skills/session/handoff/SKILL.md rename to ai-artifacts/skills/shared/session/handoff/SKILL.md diff --git a/shared/skills/session/recon/METADATA.md b/ai-artifacts/skills/shared/session/recon/METADATA.md similarity index 96% rename from shared/skills/session/recon/METADATA.md rename to ai-artifacts/skills/shared/session/recon/METADATA.md index 87d3ef2..fb9fa5a 100644 --- a/shared/skills/session/recon/METADATA.md +++ b/ai-artifacts/skills/shared/session/recon/METADATA.md @@ -6,9 +6,9 @@ resource: ./SKILL.md tags: [session, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `session` - Origin: local -- Notes: Local original. \ No newline at end of file +- Notes: Local original. diff --git a/shared/skills/session/recon/SKILL.md b/ai-artifacts/skills/shared/session/recon/SKILL.md similarity index 97% rename from shared/skills/session/recon/SKILL.md rename to ai-artifacts/skills/shared/session/recon/SKILL.md index 3d034f9..f58ef70 100644 --- a/shared/skills/session/recon/SKILL.md +++ b/ai-artifacts/skills/shared/session/recon/SKILL.md @@ -3,77 +3,79 @@ name: recon version: 1.0.0 description: Before generating code (SQL, PowerShell, Bash, Python, or any stack) whose correctness depends on environment facts you cannot see, generate a small read-only probe script the user runs to report ground truth — schema and column names/types, server version and edition, installed modules or importable packages, available cmdlets, file paths, config values, existing object definitions. The user pastes the probe's output back, and you generate the real code against what actually exists instead of against assumptions. Use when a request targets an existing system and correctness hinges on identifiers, versions, or availability you'd otherwise guess at; when the user says "recon", "check my environment first", "confirm what you know", or "help me help you"; or any time you're about to recite a column name, version, module, or path from memory. --- - -# Recon - -Generating code against an existing system from memory is guessing. Column names drift, versions differ, modules aren't installed, paths move, imports aren't present. **Recon replaces the guess with a fact** via a short handshake: emit a small read-only probe, the user runs it, the real output grounds the generated code. - -The probe is also a **proportionality check** — writing it forces you to name exactly which facts the code depends on. If you can't say what you'd probe for, you don't yet understand the request. - -**Boundary vs. `prototype`:** prototype is throwaway code to explore a design you're *inventing* by driving it by hand. Recon is read-only introspection to *discover* facts that already exist. Inventing vs. discovering — no overlap. - -## The handshake - -1. **Name the dependencies.** List the exact facts the requested code hinges on — table/column names + types, version/edition, module/package availability and versions, cmdlet existence, paths, config values, existing definitions. -2. **Emit a probe** scoped to just those facts (see contract below). Show it in chat so the user can read it before running. -3. **User runs it, pastes the output block back.** -4. **Generate the real code** grounded in the reported facts. Where a reported value contradicts what you'd have assumed, say so explicitly — that contradiction is the whole reason recon exists. - -## Scale the probe to the question - -Recon is not "always emit a big script." Match the probe to what's actually uncertain. - -- **One fact → one line.** "Which SQL Server version?" → emit `SELECT @@VERSION;`, not a sweep. A one-liner the user runs beats both guessing and a back-and-forth in chat. -- **Several interdependent facts → a surgical probe** (default). Collect exactly the named dependencies, nothing more. -- **First contact with an unknown environment → a broad baseline sweep is legitimate.** When you have no map at all, one wider probe to establish a baseline can be the right call. Treat it as a deliberate exception to "surgical," not the default — and still scope it to the domain in play (the relevant database, the relevant module set), not the whole machine. - -## Probe contract - -**Read-only is the high bar.** The default probe inspects structure and metadata, never the data itself — catalog/metadata views, version functions, module listings, path existence. This bar is high but **crossable when the data *is* the required fact**: distinct enum values stored in a column, an actual config-table value, row cardinality that changes the approach, the real stored format of a field. When you cross it: - -- Cross only for the specific value needed, never a dump. `TOP (n)`, `DISTINCT`, `COUNT`, a single keyed lookup — not `SELECT *`. -- Mask or omit anything plausibly sensitive (PII, secrets); report shape/cardinality over raw content where shape answers the question. -- **Flag the crossing** in the probe and again at handoff: state that the probe reads data, which data, and why metadata alone was insufficient. - -**Output convention** (applies to every stack): - -- Collect all results into one structure; flush **once** at the end as a single block. No interleaved result output scattered through execution. -- Emit the final block as **JSON** — the consumer is the model, and exact identifiers matter. Shape: `requested fact → value`, neutral, no assumptions baked in. `null` / `"not present"` is a valid reported value, not a failure. -- Progress and diagnostics go to a separate stream (verbose/progress/stderr), never into the result block. -- **Fail fast on infrastructure failure** (can't connect, command not found, permission denied) with a message naming what was missing. "Object not found" when *checking for existence* is a result, not a failure. -- Keep it short enough to read and verify as safe before running. - -## Probe self-check - -A probe whose own failures are silent is worse than no probe — it grounds code in facts that were never actually collected. Two failure modes must be prevented *structurally* (by output design, not by "being careful"): - -**A — a failed step vanishes.** Under `$ErrorActionPreference='Stop'` / `set -e`, a throwing step can abort before the flush, or a per-fact failure can leave its key simply absent while later facts succeed — you read the report and never notice fact 1 died. Prevent it: guard each fact probe so its failure is *recorded as a failure value*, never absent; flush the buffer in a `finally`/`trap` so a partial report always emits; include an integrity footer — `attempted` vs `captured` counts plus the names that errored. `attempted ≠ captured` means something was dropped. - -**B — empty and absent collapse.** Request dataA, derive dataB from it. If dataB is empty you cannot tell "dataA had no children" from "dataA was never obtained / came back malformed." Prevent it: record dataA's own outcome — obtained? row count? — *as its own fact, before* deriving dataB. Then an empty dataB has exactly one cause. - -This needs a **tri-state, not a bare `null`**: distinguish *present-with-value*, *present-but-empty* (0 rows is a real answer), *probed-but-errored*, and *not-probed*. `null` alone is overloaded. - -**Before emitting the probe, verify:** -- [ ] Every named dependency has a fact key — nothing silently skipped. -- [ ] Each fact carries a status; failure is a recorded value, not an absence. -- [ ] The flush runs even if a step throws (`finally`/`trap`); a genuine precondition failure (can't connect at all) still stops hard, outside the per-fact guard. -- [ ] Dependent facts record their source's outcome; an empty derived set is attributable. -- [ ] Output is valid parseable JSON — depth sufficient (no silent truncation), quotes/newlines escaped, no BOM, no locale-formatted numbers (decimal comma breaks JSON). - -**Before generating code from the pasted-back output, verify:** -- [ ] All expected keys present; `attempted == captured`; no errored fact you're about to build on. -- [ ] Values match expected shape — version matches a version pattern, counts are integers ≥ 0, expected arrays are arrays. -- [ ] A value that contradicts your assumption is surfaced; a fact that errored or is empty gets re-probed or asked about — never generate over a hole. - -## Stack recipes - -Per-stack probe anchors — what to query and how to emit it for SQL, PowerShell, Bash, and Python — live in [RECIPES.md](RECIPES.md). Load the one recipe for the stack in play; keep the probe as small as the question allows. - -## Anti-patterns - -- **Don't guess then caveat.** Emitting code with "adjust the column names if they differ" is the failure recon exists to kill. Probe first. -- **Don't over-collect.** Surgical by default; a broad sweep is a conscious baseline exception, not a habit. -- **Don't cross the data bar casually.** Metadata first; touch data only when the value itself is the fact, and flag it. -- **Don't bury the result.** One JSON block at the end; progress lives in another stream. -- **Don't continue silently on a broken probe.** Infra failure stops with a named reason; "not present" is reported, not fatal. -- **Don't probe what's already known.** If the user stated the fact, or it's visible in context, or it's stack-invariant, skip the probe and proceed. + +# Recon + +Generating code against an existing system from memory is guessing. Column names drift, versions differ, modules aren't installed, paths move, imports aren't present. **Recon replaces the guess with a fact** via a short handshake: emit a small read-only probe, the user runs it, the real output grounds the generated code. + +The probe is also a **proportionality check** — writing it forces you to name exactly which facts the code depends on. If you can't say what you'd probe for, you don't yet understand the request. + +**Boundary vs. `prototype`:** prototype is throwaway code to explore a design you're *inventing* by driving it by hand. Recon is read-only introspection to *discover* facts that already exist. Inventing vs. discovering — no overlap. + +## The handshake + +1. **Name the dependencies.** List the exact facts the requested code hinges on — table/column names + types, version/edition, module/package availability and versions, cmdlet existence, paths, config values, existing definitions. +2. **Emit a probe** scoped to just those facts (see contract below). Show it in chat so the user can read it before running. +3. **User runs it, pastes the output block back.** +4. **Generate the real code** grounded in the reported facts. Where a reported value contradicts what you'd have assumed, say so explicitly — that contradiction is the whole reason recon exists. + +## Scale the probe to the question + +Recon is not "always emit a big script." Match the probe to what's actually uncertain. + +- **One fact → one line.** "Which SQL Server version?" → emit `SELECT @@VERSION;`, not a sweep. A one-liner the user runs beats both guessing and a back-and-forth in chat. +- **Several interdependent facts → a surgical probe** (default). Collect exactly the named dependencies, nothing more. +- **First contact with an unknown environment → a broad baseline sweep is legitimate.** When you have no map at all, one wider probe to establish a baseline can be the right call. Treat it as a deliberate exception to "surgical," not the default — and still scope it to the domain in play (the relevant database, the relevant module set), not the whole machine. + +## Probe contract + +**Read-only is the high bar.** The default probe inspects structure and metadata, never the data itself — catalog/metadata views, version functions, module listings, path existence. This bar is high but **crossable when the data *is* the required fact**: distinct enum values stored in a column, an actual config-table value, row cardinality that changes the approach, the real stored format of a field. When you cross it: + +- Cross only for the specific value needed, never a dump. `TOP (n)`, `DISTINCT`, `COUNT`, a single keyed lookup — not `SELECT *`. +- Mask or omit anything plausibly sensitive (PII, secrets); report shape/cardinality over raw content where shape answers the question. +- **Flag the crossing** in the probe and again at handoff: state that the probe reads data, which data, and why metadata alone was insufficient. + +**Output convention** (applies to every stack): + +- Collect all results into one structure; flush **once** at the end as a single block. No interleaved result output scattered through execution. +- Emit the final block as **JSON** — the consumer is the model, and exact identifiers matter. Shape: `requested fact → value`, neutral, no assumptions baked in. `null` / `"not present"` is a valid reported value, not a failure. +- Progress and diagnostics go to a separate stream (verbose/progress/stderr), never into the result block. +- **Fail fast on infrastructure failure** (can't connect, command not found, permission denied) with a message naming what was missing. "Object not found" when *checking for existence* is a result, not a failure. +- Keep it short enough to read and verify as safe before running. + +## Probe self-check + +A probe whose own failures are silent is worse than no probe — it grounds code in facts that were never actually collected. Two failure modes must be prevented *structurally* (by output design, not by "being careful"): + +**A — a failed step vanishes.** Under `$ErrorActionPreference='Stop'` / `set -e`, a throwing step can abort before the flush, or a per-fact failure can leave its key simply absent while later facts succeed — you read the report and never notice fact 1 died. Prevent it: guard each fact probe so its failure is *recorded as a failure value*, never absent; flush the buffer in a `finally`/`trap` so a partial report always emits; include an integrity footer — `attempted` vs `captured` counts plus the names that errored. `attempted ≠ captured` means something was dropped. + +**B — empty and absent collapse.** Request dataA, derive dataB from it. If dataB is empty you cannot tell "dataA had no children" from "dataA was never obtained / came back malformed." Prevent it: record dataA's own outcome — obtained? row count? — *as its own fact, before* deriving dataB. Then an empty dataB has exactly one cause. + +This needs a **tri-state, not a bare `null`**: distinguish *present-with-value*, *present-but-empty* (0 rows is a real answer), *probed-but-errored*, and *not-probed*. `null` alone is overloaded. + +**Before emitting the probe, verify:** + +- [ ] Every named dependency has a fact key — nothing silently skipped. +- [ ] Each fact carries a status; failure is a recorded value, not an absence. +- [ ] The flush runs even if a step throws (`finally`/`trap`); a genuine precondition failure (can't connect at all) still stops hard, outside the per-fact guard. +- [ ] Dependent facts record their source's outcome; an empty derived set is attributable. +- [ ] Output is valid parseable JSON — depth sufficient (no silent truncation), quotes/newlines escaped, no BOM, no locale-formatted numbers (decimal comma breaks JSON). + +**Before generating code from the pasted-back output, verify:** + +- [ ] All expected keys present; `attempted == captured`; no errored fact you're about to build on. +- [ ] Values match expected shape — version matches a version pattern, counts are integers ≥ 0, expected arrays are arrays. +- [ ] A value that contradicts your assumption is surfaced; a fact that errored or is empty gets re-probed or asked about — never generate over a hole. + +## Stack recipes + +Per-stack probe anchors — what to query and how to emit it for SQL, PowerShell, Bash, and Python — live in [RECIPES.md](docs/RECIPES.md). Load the one recipe for the stack in play; keep the probe as small as the question allows. + +## Anti-patterns + +- **Don't guess then caveat.** Emitting code with "adjust the column names if they differ" is the failure recon exists to kill. Probe first. +- **Don't over-collect.** Surgical by default; a broad sweep is a conscious baseline exception, not a habit. +- **Don't cross the data bar casually.** Metadata first; touch data only when the value itself is the fact, and flag it. +- **Don't bury the result.** One JSON block at the end; progress lives in another stream. +- **Don't continue silently on a broken probe.** Infra failure stops with a named reason; "not present" is reported, not fatal. +- **Don't probe what's already known.** If the user stated the fact, or it's visible in context, or it's stack-invariant, skip the probe and proceed. diff --git a/shared/skills/session/recon/RECIPES.md b/ai-artifacts/skills/shared/session/recon/docs/RECIPES.md similarity index 100% rename from shared/skills/session/recon/RECIPES.md rename to ai-artifacts/skills/shared/session/recon/docs/RECIPES.md diff --git a/shared/skills/session/write-a-skill/METADATA.md b/ai-artifacts/skills/shared/session/write-a-skill/METADATA.md similarity index 95% rename from shared/skills/session/write-a-skill/METADATA.md rename to ai-artifacts/skills/shared/session/write-a-skill/METADATA.md index aea051d..e55026f 100644 --- a/shared/skills/session/write-a-skill/METADATA.md +++ b/ai-artifacts/skills/shared/session/write-a-skill/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/productivity/write-a-skill/SKILL.md upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `session` @@ -19,6 +19,6 @@ upstream-commit: aaf2453fbdfe7a15c07f11d861224f34ab4b53cb - Reconciled commit: `aaf2453fbdfe7a15c07f11d861224f34ab4b53cb` - Notes: Localized. Known issue: links REFERENCE.md but ships EXAMPLES.md. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/session/write-a-skill/SKILL.md b/ai-artifacts/skills/shared/session/write-a-skill/SKILL.md similarity index 90% rename from shared/skills/session/write-a-skill/SKILL.md rename to ai-artifacts/skills/shared/session/write-a-skill/SKILL.md index 3f3f440..14d6bd0 100644 --- a/shared/skills/session/write-a-skill/SKILL.md +++ b/ai-artifacts/skills/shared/session/write-a-skill/SKILL.md @@ -3,92 +3,93 @@ name: write-a-skill version: 1.0.0 description: Create, update, or improve agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, build, revise, update, or improve a skill or SKILL.md file. --- - -# Writing Skills - -## Process - -1. **Gather requirements** - ask user about: - - What task/domain does the skill cover? - - What specific use cases should it handle? - - Does it need executable scripts or just instructions? - - Any reference materials to include? - -2. **Draft the skill** - create: - - SKILL.md with concise instructions - - Additional reference files if content exceeds 500 lines - - Utility scripts if deterministic operations needed - -3. **Review with user** - present draft and ask: - - Does this cover your use cases? - - Anything missing or unclear? - - Should any section be more/less detailed? - -## Skill Structure - -``` -skill-name/ -├── SKILL.md # Main instructions (required) -├── REFERENCE.md # Detailed docs (if needed) -├── EXAMPLES.md # Usage examples (if needed) -└── scripts/ # Utility scripts (if needed) - └── helper.js -``` - -## SKILL.md Template - -```md ---- -name: skill-name -description: Brief description of capability. Use when [specific triggers]. ---- - -# Skill Name - -## Quick start - -[Minimal working example] - -## Workflows - -[Step-by-step processes with checklists for complex tasks] - -## Advanced features - -[Link to separate files: See [REFERENCE.md](REFERENCE.md)] -``` - -## Description Requirements - -The description is **the only thing your agent sees** when deciding which skill to load. - -**Format**: max 1024 chars, third person. First sentence: what it does. Second sentence: `Use when [triggers]` — include synonym variants (create/write/build, update/revise/improve). - -Good: `Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when user mentions PDFs, forms, or document extraction.` - -Bad: `Helps with documents.` - -## Scripts - -If the skill needs bundled executable scripts, see [SCRIPTS.md](SCRIPTS.md) -for when to add them and how to build them test-first. - -## When to Split Files - -Split into separate files when: - -- SKILL.md exceeds 100 lines -- Content has distinct domains -- Advanced features are rarely needed - -## Review Checklist - -After drafting, verify: - -- [ ] Description includes triggers ("Use when...") with synonym variants for create *and* update flows -- [ ] SKILL.md under 100 lines (including this checklist — count before delivery) -- [ ] No time-sensitive info -- [ ] Consistent terminology -- [ ] At least one complete realistic example exists (inline or in EXAMPLES.md) -- [ ] Any bundled script was built/updated test-first via the `tdd` skill -- [ ] References one level deep only + +# Writing Skills + +## Process + +1. **Gather requirements** - ask user about: + - What task/domain does the skill cover? + - What specific use cases should it handle? + - Does it need executable scripts or just instructions? + - Any reference materials to include? + +2. **Draft the skill** - create: + - SKILL.md with concise instructions + - Additional reference files if content exceeds 500 lines + - Utility scripts if deterministic operations needed + +3. **Review with user** - present draft and ask: + - Does this cover your use cases? + - Anything missing or unclear? + - Should any section be more/less detailed? + +## Skill Structure + +```text +skill-name/ +├── docs/ # Additional markdown docs (if needed) +│ ├── REFERENCE.md +│ └── EXAMPLES.md +├── SKILL.md # Main instructions (required) +└── scripts/ # Utility scripts (if needed) + └── helper.js +``` + +## SKILL.md Template + +```md +--- +name: skill-name +description: Brief description of capability. Use when [specific triggers]. +--- + +# Skill Name + +## Quick start + +[Minimal working example] + +## Workflows + +[Step-by-step processes with checklists for complex tasks] + +## Advanced features + +[Link to separate files: See [REFERENCE.md](docs/REFERENCE.md)] +``` + +## Description Requirements + +The description is **the only thing your agent sees** when deciding which skill to load. + +**Format**: max 1024 chars, third person. First sentence: what it does. Second sentence: `Use when [triggers]` — include synonym variants (create/write/build, update/revise/improve). + +Good: `Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when user mentions PDFs, forms, or document extraction.` + +Bad: `Helps with documents.` + +## Scripts + +If the skill needs bundled executable scripts, see [SCRIPTS.md](docs/SCRIPTS.md) +for when to add them and how to build them test-first. + +## When to Split Files + +Split into separate files when: + +- SKILL.md exceeds 100 lines +- Content has distinct domains +- Advanced features are rarely needed + +## Review Checklist + +After drafting, verify: + +- [ ] Description includes triggers ("Use when...") with synonym variants for create *and* update flows +- [ ] SKILL.md under 100 lines (including this checklist — count before delivery) +- [ ] No time-sensitive info +- [ ] Consistent terminology +- [ ] At least one complete realistic example exists (inline or in EXAMPLES.md) +- [ ] Any bundled script was built/updated test-first via the `tdd` skill +- [ ] References one level deep only diff --git a/shared/skills/session/write-a-skill/EXAMPLES.md b/ai-artifacts/skills/shared/session/write-a-skill/docs/EXAMPLES.md similarity index 68% rename from shared/skills/session/write-a-skill/EXAMPLES.md rename to ai-artifacts/skills/shared/session/write-a-skill/docs/EXAMPLES.md index f334496..54aeef0 100644 --- a/shared/skills/session/write-a-skill/EXAMPLES.md +++ b/ai-artifacts/skills/shared/session/write-a-skill/docs/EXAMPLES.md @@ -10,7 +10,7 @@ A realistic minimal skill covering a focused domain. ### File layout -``` +```text csv-import/ ├── SKILL.md └── EXAMPLES.md @@ -57,10 +57,10 @@ description: Parse, validate, and import CSV files into SQL databases or datafra ### What makes this a good skill -| Property | Value | -|---|---| -| Description discriminates | Yes — "CSV", "import", "ingestion", "tabular data" | -| Update trigger covered | Not applicable (no update flow for this domain) | -| Line count | 38 — well under 100 | -| Concrete example | Checklist steps are actionable without guessing | -| Split decision | EXAMPLES.md added because SQL sample output would push SKILL.md over limit | +| Property | Value | +| ------------------------- | -------------------------------------------------------------------------- | +| Description discriminates | Yes — "CSV", "import", "ingestion", "tabular data" | +| Update trigger covered | Not applicable (no update flow for this domain) | +| Line count | 38 — well under 100 | +| Concrete example | Checklist steps are actionable without guessing | +| Split decision | EXAMPLES.md added because SQL sample output would push SKILL.md over limit | diff --git a/shared/skills/session/write-a-skill/SCRIPTS.md b/ai-artifacts/skills/shared/session/write-a-skill/docs/SCRIPTS.md similarity index 100% rename from shared/skills/session/write-a-skill/SCRIPTS.md rename to ai-artifacts/skills/shared/session/write-a-skill/docs/SCRIPTS.md diff --git a/shared/skills/setup/check-skill-updates/METADATA.md b/ai-artifacts/skills/shared/setup/check-skill-updates/METADATA.md similarity index 94% rename from shared/skills/setup/check-skill-updates/METADATA.md rename to ai-artifacts/skills/shared/setup/check-skill-updates/METADATA.md index 132bf4d..40be5e7 100644 --- a/shared/skills/setup/check-skill-updates/METADATA.md +++ b/ai-artifacts/skills/shared/setup/check-skill-updates/METADATA.md @@ -6,9 +6,9 @@ resource: ./SKILL.md tags: [setup, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `setup` - Origin: local -- Notes: Local original; reads skill METADATA.md files for upstream drift checks. \ No newline at end of file +- Notes: Local original; reads skill METADATA.md files for upstream drift checks. diff --git a/shared/skills/setup/check-skill-updates/SKILL.md b/ai-artifacts/skills/shared/setup/check-skill-updates/SKILL.md similarity index 90% rename from shared/skills/setup/check-skill-updates/SKILL.md rename to ai-artifacts/skills/shared/setup/check-skill-updates/SKILL.md index b4dc6f7..651f4e1 100644 --- a/shared/skills/setup/check-skill-updates/SKILL.md +++ b/ai-artifacts/skills/shared/setup/check-skill-updates/SKILL.md @@ -14,8 +14,8 @@ skill — the actual update is triaged and done later (by a human or an agent pi item), preserving local customizations under review rather than auto-overwriting. - No local clone of any upstream is needed, and it works for **any** `upstream-repo`. -- Source of truth is `shared/skills///SKILL.md`; generated mirrors are rebuilt by - `scripts/sync-skills.ps1`. +- Source of truth is `ai-artifacts/skills/shared///SKILL.md`; generated mirrors are rebuilt by + `scripts/setup-repo.ps1 -SkipHooks`. - Skills whose `METADATA.md` has no `upstream-commit` are skipped. ## Prerequisite — GitHub CLI, authenticated @@ -33,7 +33,7 @@ changes count, not just `SKILL.md`) and compare to `upstream-commit`. $ErrorActionPreference = 'Stop' $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..') -$skillsRoot = Join-Path $repoRoot 'shared\skills' +$skillsRoot = Join-Path $repoRoot 'ai-artifacts\skills\shared' $results = foreach ($metadata in Get-ChildItem $skillsRoot -Recurse -Filter 'METADATA.md') { $content = Get-Content $metadata.FullName -Raw @@ -92,9 +92,9 @@ Upstream has moved past the commit this skill was last reconciled to. ## Action Review the upstream changes and merge the worthwhile ones into -`shared/skills///SKILL.md` (and resources), preserving local customizations, then bump -`upstream-commit` in `shared/skills///METADATA.md` and re-run -`scripts/sync-skills.ps1`. See "Appendix: actioning an update" in the check-skill-updates skill for +`ai-artifacts/skills/shared///SKILL.md` (and resources), preserving local customizations, then bump +`upstream-commit` in `ai-artifacts/skills/shared///METADATA.md` and re-run +`scripts/setup-repo.ps1 -SkipHooks`. See "Appendix: actioning an update" in the check-skill-updates skill for the three-way procedure. ``` @@ -114,14 +114,14 @@ $owner = ''; $repo = '' $upstreamPath = ''; $storedCommit = '' $baseline = (& gh api -H 'Accept: application/vnd.github.raw' "repos/$owner/$repo/contents/$upstreamPath`?ref=$storedCommit") -$installed = Get-Content '\shared\skills\\\SKILL.md' -Raw +$installed = Get-Content '\ai-artifacts\skills\shared\\\SKILL.md' -Raw $newUpstream = (& gh api -H 'Accept: application/vnd.github.raw' "repos/$owner/$repo/contents/$upstreamPath") ``` `$baseline` vs `$installed` = local customizations to keep. `$baseline` vs `$newUpstream` = upstream changes to consider. Apply upstream fixes/improvements; keep local substitutions and intentional merges; skip conflicting upstream changes and note them. Then bump `upstream-commit` in -`METADATA.md`, write to the source of truth, and re-run `scripts/sync-skills.ps1`. +`METADATA.md`, write to the source of truth, and re-run `scripts/setup-repo.ps1 -SkipHooks`. ## Notes diff --git a/shared/skills/setup/git-guardrails/METADATA.md b/ai-artifacts/skills/shared/setup/git-guardrails/METADATA.md similarity index 95% rename from shared/skills/setup/git-guardrails/METADATA.md rename to ai-artifacts/skills/shared/setup/git-guardrails/METADATA.md index 4309ec3..6ccb954 100644 --- a/shared/skills/setup/git-guardrails/METADATA.md +++ b/ai-artifacts/skills/shared/setup/git-guardrails/METADATA.md @@ -10,7 +10,7 @@ upstream-path: skills/misc/git-guardrails-claude-code/SKILL.md upstream-commit: 62f43a18177be6ec82da242e59ffbc490a4c22ea --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `setup` @@ -19,6 +19,6 @@ upstream-commit: 62f43a18177be6ec82da242e59ffbc490a4c22ea - Reconciled commit: `62f43a18177be6ec82da242e59ffbc490a4c22ea` - Notes: Localized from the global-prior; Claude-Code-hook skill, not generally useful in chat. -# Citations +## Citations -[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) \ No newline at end of file +[1] [https://github.com/mattpocock/skills](https://github.com/mattpocock/skills) diff --git a/shared/skills/setup/git-guardrails/SKILL.md b/ai-artifacts/skills/shared/setup/git-guardrails/SKILL.md similarity index 100% rename from shared/skills/setup/git-guardrails/SKILL.md rename to ai-artifacts/skills/shared/setup/git-guardrails/SKILL.md diff --git a/shared/skills/setup/git-guardrails/scripts/block-dangerous-git.ps1 b/ai-artifacts/skills/shared/setup/git-guardrails/scripts/block-dangerous-git.ps1 similarity index 90% rename from shared/skills/setup/git-guardrails/scripts/block-dangerous-git.ps1 rename to ai-artifacts/skills/shared/setup/git-guardrails/scripts/block-dangerous-git.ps1 index ba082b8..435ea80 100644 --- a/shared/skills/setup/git-guardrails/scripts/block-dangerous-git.ps1 +++ b/ai-artifacts/skills/shared/setup/git-guardrails/scripts/block-dangerous-git.ps1 @@ -1,6 +1,8 @@ +#Requires -Version 5.1 +# RuntimePolicy: dual-runtime # PreToolUse hook: block dangerous git commands before Claude Code runs them (PowerShell). # Reads the tool-call JSON on stdin; on a dangerous match, writes a message to stderr and exits 2 -# (which Claude Code treats as "blocked"); otherwise exits 0. No pwsh-7-only features — runs on 5.1+. +# (which Claude Code treats as "blocked"); otherwise exits 0. $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest diff --git a/shared/skills/setup/git-guardrails/scripts/block-dangerous-git.sh b/ai-artifacts/skills/shared/setup/git-guardrails/scripts/block-dangerous-git.sh similarity index 100% rename from shared/skills/setup/git-guardrails/scripts/block-dangerous-git.sh rename to ai-artifacts/skills/shared/setup/git-guardrails/scripts/block-dangerous-git.sh diff --git a/shared/skills/setup/import-upstream-skill/METADATA.md b/ai-artifacts/skills/shared/setup/import-upstream-skill/METADATA.md similarity index 97% rename from shared/skills/setup/import-upstream-skill/METADATA.md rename to ai-artifacts/skills/shared/setup/import-upstream-skill/METADATA.md index c6087ee..0962236 100644 --- a/shared/skills/setup/import-upstream-skill/METADATA.md +++ b/ai-artifacts/skills/shared/setup/import-upstream-skill/METADATA.md @@ -6,7 +6,7 @@ resource: ./SKILL.md tags: [setup, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `setup` diff --git a/shared/skills/setup/import-upstream-skill/SKILL.md b/ai-artifacts/skills/shared/setup/import-upstream-skill/SKILL.md similarity index 87% rename from shared/skills/setup/import-upstream-skill/SKILL.md rename to ai-artifacts/skills/shared/setup/import-upstream-skill/SKILL.md index 6b6e42c..84d2d29 100644 --- a/shared/skills/setup/import-upstream-skill/SKILL.md +++ b/ai-artifacts/skills/shared/setup/import-upstream-skill/SKILL.md @@ -10,9 +10,9 @@ A repeatable, **source-agnostic** process for bringing an outside skill into thi concrete driver was the mattpocock skills repo, but nothing here is specific to it — it works for any `upstream-repo` (or a local/global copy with no repo). -The single source of truth is `shared/skills///SKILL.md` (+ bundled resources). The +The single source of truth is `ai-artifacts/skills/shared///SKILL.md` (+ bundled resources). The `.claude/commands/` copies are a generated mirror — never edit them; rebuild with -`pwsh scripts/sync-skills.ps1`. This skill **imports and adapts**; the sibling +`pwsh scripts/setup-repo.ps1 -SkipHooks`. This skill **imports and adapts**; the sibling `/setup:check-skill-updates` is what later detects staleness against upstream. ## Process @@ -32,7 +32,7 @@ The single source of truth is `shared/skills///SKILL.md` (+ bundled (conversational/process skills) · `setup` (repo tooling and skill maintenance). Create a new group only when none fit — a new group is a deliberate decision, not a default. -3. **Place it** at `shared/skills///SKILL.md` with resources alongside. Runtime +3. **Place it** at `ai-artifacts/skills/shared///SKILL.md` with resources alongside. Runtime frontmatter should contain only harness-relevant fields plus `version: `; start new local imports at `version: 1.0.0` unless they are explicitly experimental (`0.x.y`). Use `git mv`-style care if you are relocating something already tracked. @@ -70,10 +70,10 @@ The single source of truth is `shared/skills///SKILL.md` (+ bundled 6. **Verify** the result with `/session:write-a-skill` (structure, description triggers, progressive disclosure, references one level deep). -7. **Update the origin map** — add a row to [shared/skills/README.md](../../README.md) (skill, group, +7. **Update the origin map** — add a row to [ai-artifacts/skills/shared/README.md](../../README.md) (skill, group, upstream, notes) so the human-readable summary matches `METADATA.md`. -8. **Rebuild the mirror:** `pwsh scripts/sync-skills.ps1`, then confirm `/group:name` resolves. If the +8. **Rebuild the mirror:** `pwsh scripts/setup-repo.ps1 -SkipHooks`, then confirm `/group:name` resolves. If the group is new, add its `/.claude/commands//` mirror path to `.gitignore`. ## Checklist @@ -84,5 +84,5 @@ The single source of truth is `shared/skills///SKILL.md` (+ bundled - [ ] `METADATA.md` has OKF `type` frontmatter and correct `upstream-*` fields — or fork/local-original rules applied (no dangling `upstream-commit`) - [ ] Capability-contract adaptation done; bash → pwsh; intent + local customizations preserved - [ ] Verified via `/session:write-a-skill` -- [ ] `shared/skills/README.md` origin map updated -- [ ] `scripts/sync-skills.ps1` run; `/group:name` resolves +- [ ] `ai-artifacts/skills/shared/README.md` origin map updated +- [ ] `scripts/setup-repo.ps1 -SkipHooks` run; `/group:name` resolves diff --git a/shared/skills/setup/setup-pre-commit/METADATA.md b/ai-artifacts/skills/shared/setup/setup-pre-commit/METADATA.md similarity index 88% rename from shared/skills/setup/setup-pre-commit/METADATA.md rename to ai-artifacts/skills/shared/setup/setup-pre-commit/METADATA.md index 0fd87bd..44eb0f8 100644 --- a/shared/skills/setup/setup-pre-commit/METADATA.md +++ b/ai-artifacts/skills/shared/setup/setup-pre-commit/METADATA.md @@ -6,9 +6,9 @@ resource: ./SKILL.md tags: [setup, skill] --- -# Skill Metadata +## Skill Metadata - Runtime skill: [SKILL.md](SKILL.md) - Group: `setup` - Origin: local -- Notes: Local fork of mattpocock skills/misc/setup-pre-commit. Diverged entirely; no upstream commit is tracked. \ No newline at end of file +- Notes: Local fork of mattpocock skills/misc/setup-pre-commit. Diverged entirely; no upstream commit is tracked. diff --git a/shared/skills/setup/setup-pre-commit/SKILL.md b/ai-artifacts/skills/shared/setup/setup-pre-commit/SKILL.md similarity index 99% rename from shared/skills/setup/setup-pre-commit/SKILL.md rename to ai-artifacts/skills/shared/setup/setup-pre-commit/SKILL.md index 6fc6a14..a12e7ef 100644 --- a/shared/skills/setup/setup-pre-commit/SKILL.md +++ b/ai-artifacts/skills/shared/setup/setup-pre-commit/SKILL.md @@ -201,6 +201,6 @@ This will run through the new hooks — a good smoke test. - `pre-commit` caches environments; first run per hook is slow, subsequent runs are fast - `sqlfluff --fix` is aggressive — review its changes before committing -- For vale, pick a style pack matching your writing style (`Microsoft`, `Google`, `Vale`) from https://vale.sh/hub/ +- For vale, pick a style pack matching your writing style (`Microsoft`, `Google`, `Vale`) from - To skip hooks temporarily: `git commit --no-verify` (use sparingly) - To update all hooks to latest versions: `pre-commit autoupdate` diff --git a/ai-artifacts/skills/shared/setup/setup-repo/METADATA.md b/ai-artifacts/skills/shared/setup/setup-repo/METADATA.md new file mode 100644 index 0000000..4ebda20 --- /dev/null +++ b/ai-artifacts/skills/shared/setup/setup-repo/METADATA.md @@ -0,0 +1,14 @@ +--- +type: Agent Skill Metadata +title: "setup-repo" +description: "Bootstrap this repository after clone by enabling git hooks and syncing generated skill mirrors." +resource: ./SKILL.md +tags: [setup, skill] +--- + +## Skill Metadata + +- Runtime skill: [SKILL.md](SKILL.md) +- Group: `setup` +- Origin: local +- Notes: Local original that combines local hook setup and mirror sync into one bootstrap entrypoint. diff --git a/ai-artifacts/skills/shared/setup/setup-repo/SKILL.md b/ai-artifacts/skills/shared/setup/setup-repo/SKILL.md new file mode 100644 index 0000000..ec46f5f --- /dev/null +++ b/ai-artifacts/skills/shared/setup/setup-repo/SKILL.md @@ -0,0 +1,83 @@ +--- +name: setup-repo +version: 1.0.0 +description: Bootstrap this repository after clone by enabling git hooks and syncing generated skill mirrors. Use when user asks to set up the repo, bootstrap local tooling, or initialize mirrors/hooks for this clone. +--- + +# Setup Repo + +Bootstraps this clone in one pass: + +1. Activates repo git hooks +2. Syncs generated skill mirrors for Claude, Codex, and Copilot + +Implementation layout: + +- Canonical entrypoint: `ai-artifacts/skills/shared/setup/setup-repo/scripts/Invoke-SetupRepo.ps1` +- Repo convenience wrapper: `scripts/setup-repo.ps1` + +## Applicability (capability contract) + +This skill needs shell + filesystem access in a local git checkout. In chat-only environments, +provide the exact commands and explain what each command configures. + +## Default command + +```powershell +pwsh scripts/setup-repo.ps1 +``` + +## Common variants + +Project-local mirrors for one harness: + +```powershell +pwsh scripts/setup-repo.ps1 -Target Copilot -Scope Project +``` + +User-scope mirrors for all harnesses: + +```powershell +pwsh scripts/setup-repo.ps1 -Scope User +``` + +Bootstrap only missing generated mirrors (keeps existing generated files untouched): + +```powershell +pwsh scripts/setup-repo.ps1 -IfMissing +``` + +Refresh generated mirrors without touching git hooks: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks +``` + +Check generated mirrors for drift: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks -Check +pwsh scripts/setup-repo.ps1 -SkipHooks -Target Codex -Skill tdd -Check +``` + +Run only one phase: + +```powershell +pwsh scripts/setup-repo.ps1 -SkipHooks +pwsh scripts/setup-repo.ps1 -SkipSkillSync +``` + +## Verify + +```powershell +git config --get core.hooksPath +Test-Path .claude/commands +Test-Path .agents/skills +Test-Path .github/skills +pwsh scripts/setup-repo.ps1 -SkipHooks -Check +``` + +Expected: + +- `core.hooksPath` prints `.githooks` +- mirror folders exist for selected targets diff --git a/ai-artifacts/skills/shared/setup/setup-repo/scripts/Invoke-SetupRepo.ps1 b/ai-artifacts/skills/shared/setup/setup-repo/scripts/Invoke-SetupRepo.ps1 new file mode 100644 index 0000000..d1c0f05 --- /dev/null +++ b/ai-artifacts/skills/shared/setup/setup-repo/scripts/Invoke-SetupRepo.ps1 @@ -0,0 +1,584 @@ +#Requires -Version 7.0 +#Requires -PSEdition Core +# RuntimePolicy: core-first + +[CmdletBinding()] +param( + [string]$RepoRoot = "", + + [ValidateSet('All', 'Claude', 'Codex', 'Copilot')] + [string]$Target = 'All', + + [Alias('Scope')] + [ValidateSet('Project', 'User')] + [string]$MirrorScope = 'Project', + + [string]$Skill = '', + + [switch]$IfMissing, + [switch]$Check, + [switch]$SkipHooks, + [switch]$SkipSkillSync +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($Check -and $IfMissing) { + throw "-Check is read-only; do not combine it with -IfMissing." +} + +function Get-UserHomePath { + if ($env:HOME) { return $env:HOME } + if ($env:USERPROFILE) { return $env:USERPROFILE } + throw "Cannot resolve user home path from HOME/USERPROFILE." +} + +function Get-RelativePathText { + param( + [string]$BasePath, + [string]$TargetPath + ) + + $rel = [System.IO.Path]::GetRelativePath($BasePath, $TargetPath) + return $rel.TrimStart('\', '/') +} + +function Get-MirrorRootPath { + param( + [ValidateSet('Claude', 'Codex', 'Copilot')] + [string]$TargetName, + + [ValidateSet('Project', 'User')] + [string]$ScopeName, + + [string]$ResolvedRepoRoot + ) + + switch ("$TargetName|$ScopeName") { + 'Claude|Project' { return (Join-Path $ResolvedRepoRoot '.claude/commands') } + 'Claude|User' { return (Join-Path (Get-UserHomePath) '.claude/commands') } + 'Codex|Project' { return (Join-Path $ResolvedRepoRoot '.agents/skills') } + 'Codex|User' { return (Join-Path (Get-UserHomePath) '.codex/skills') } + 'Copilot|Project' { return (Join-Path $ResolvedRepoRoot '.github/skills') } + 'Copilot|User' { return (Join-Path (Get-UserHomePath) '.copilot/skills') } + default { throw "Unsupported mirror target/scope combination: $TargetName / $ScopeName" } + } +} + +function Get-SkillDirectories { + param( + [string]$SkillsRoot, + [string]$SkillName + ) + + $dirs = @(Get-ChildItem $SkillsRoot -Recurse -Filter 'SKILL.md' -File | ForEach-Object { $_.Directory }) + if ($SkillName) { + $dirs = @($dirs | Where-Object { $_.Name -eq $SkillName }) + if ($dirs.Count -eq 0) { + throw "No SKILL.md found for skill '$SkillName' under $SkillsRoot" + } + } + if ($dirs.Count -eq 0) { + throw "No SKILL.md files found under $SkillsRoot" + } + return $dirs +} + +function Get-SyncReportEntry { + param( + [string]$Name, + [string]$Detail + ) + + return [PSCustomObject]@{ + Name = $Name + Detail = $Detail + } +} + +function Write-SyncReport { + param( + [string]$Flavor, + [string]$TargetPath, + [object[]]$Entries, + [string]$Scope + ) + + $scopeText = if ($Scope) { " (scope=$Scope)" } else { '' } + Write-Output "Synced $Flavor -> $TargetPath$scopeText" + + if (-not $Entries -or @($Entries).Count -eq 0) { + return + } + + $nameWidth = ($Entries | ForEach-Object { $_.Name.Length } | Measure-Object -Maximum).Maximum + if (-not $nameWidth) { $nameWidth = 0 } + + foreach ($entry in $Entries) { + Write-Output (" {0} {1}" -f $entry.Name.PadRight($nameWidth), $entry.Detail) + } +} + +function Get-SkillResources { + param([string]$SkillDir) + + return @(Get-ChildItem $SkillDir -Recurse -File | + Where-Object { $_.Name -notin @('SKILL.md', 'METADATA.md') }) +} + +function Copy-MirroredResources { + param( + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetBase + ) + + foreach ($res in $Resources) { + $rel = Get-RelativePathText -BasePath $SourceBase -TargetPath $res.FullName + $dest = Join-Path $TargetBase $rel + New-Item -ItemType Directory -Path (Split-Path $dest -Parent) -Force | Out-Null + Copy-Item $res.FullName $dest -Force + } +} + +function Test-MirroredResourcesMatch { + param( + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetBase, + [switch]$ExcludeTargetSkillFile + ) + + foreach ($res in $Resources) { + $rel = Get-RelativePathText -BasePath $SourceBase -TargetPath $res.FullName + $dest = Join-Path $TargetBase $rel + if (-not (Test-Path $dest) -or + (Get-FileHash $res.FullName).Hash -ne (Get-FileHash $dest).Hash) { + return $false + } + } + + $resourceCount = @($Resources).Count + $mirroredCount = if (Test-Path $TargetBase) { + @(Get-ChildItem $TargetBase -Recurse -File | + Where-Object { -not $ExcludeTargetSkillFile -or $_.Name -ne 'SKILL.md' }).Count + } + else { + 0 + } + + return $mirroredCount -eq $resourceCount +} + +function Get-SkillMirrorState { + param( + [string]$TargetSkillPath, + [string]$ExpectedSkill, + [System.IO.FileInfo[]]$Resources, + [string]$SourceBase, + [string]$TargetResourceBase, + [switch]$ExcludeTargetSkillFile + ) + + if (-not (Test-Path $TargetSkillPath)) { return 'MISSING' } + if ((Get-Content $TargetSkillPath -Raw) -cne $ExpectedSkill) { return 'STALE' } + + if (-not (Test-MirroredResourcesMatch -Resources $Resources -SourceBase $SourceBase -TargetBase $TargetResourceBase -ExcludeTargetSkillFile:$ExcludeTargetSkillFile)) { + return 'STALE' + } + + return 'UP-TO-DATE' +} + +function Convert-ResourceLinks { + param( + [string]$Body, + [string]$Name, + [string[]]$ResourceRelPaths + ) + + foreach ($path in $ResourceRelPaths) { + $Body = $Body.Replace("](./$path)", "]($Name/$path)") + $Body = $Body.Replace("]($path)", "]($Name/$path)") + $Body = $Body -replace ("(?<=@)" + [regex]::Escape($path) + "\b"), "$Name/$path" + } + return $Body +} + +function ConvertTo-YamlScalar { + param([string]$Value) + + if ($null -eq $Value) { return '""' } + return '"' + ($Value -replace '\\', '\\' -replace '"', '\"') + '"' +} + +function Get-FrontmatterValue { + param( + [string]$Document, + [string]$Key + ) + + if ($Document -notmatch "(?s)\A---\r?\n(.*?)\r?\n---\r?\n") { return $null } + $frontmatter = $Matches[1] + $match = [regex]::Match($frontmatter, "(?m)^$([regex]::Escape($Key)):\s*(.*)$") + if (-not $match.Success) { return $null } + + $value = $match.Groups[1].Value.Trim() + if (($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'"))) { + $value = $value.Substring(1, $value.Length - 2) + } + return $value +} + +function Get-SkillBody { + param([string]$Document) + + if ($Document -match "(?s)\A---\r?\n.*?\r?\n---\r?\n(.*)\z") { + return $Matches[1].TrimStart() + } + return $Document.TrimStart() +} + +function ConvertTo-CodexSkill { + param( + [string]$SourceDocument, + [string]$CodexName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $CodexName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function ConvertTo-CopilotSkill { + param( + [string]$SourceDocument, + [string]$SkillName, + [string]$SourceRelPath + ) + + $description = Get-FrontmatterValue -Document $SourceDocument -Key 'description' + if (-not $description) { $description = "Repo skill mirrored from $SourceRelPath." } + $version = Get-FrontmatterValue -Document $SourceDocument -Key 'version' + + $body = Get-SkillBody -Document $SourceDocument + $frontmatter = @( + '---' + "name: $(ConvertTo-YamlScalar $SkillName)" + "description: $(ConvertTo-YamlScalar $description)" + $(if ($version) { "version: $(ConvertTo-YamlScalar $version)" }) + '---' + '' + "" + '' + ) -join "`n" + + return $frontmatter + $body +} + +function Invoke-InstallGitHooksInternal { + param([string]$ResolvedRepoRoot) + + $hooksDir = Join-Path $ResolvedRepoRoot '.githooks' + if (Test-Path $hooksDir) { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + Get-ChildItem -Path $hooksDir -File | ForEach-Object { + $raw = [System.IO.File]::ReadAllText($_.FullName) + $normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n" + if ($normalized -ne $raw) { + [System.IO.File]::WriteAllText($_.FullName, $normalized, $utf8NoBom) + Write-Output "Normalized LF line endings: $($_.Name)" + } + } + } + + Write-Output '== Git hooks ==' + git config core.hooksPath .githooks + Write-Output 'Configured core.hooksPath to .githooks' + + if ($IsLinux -or $IsMacOS) { + $preCommit = Join-Path $hooksDir 'pre-commit' + if (Test-Path $preCommit) { + & chmod +x $preCommit + } + } + + Write-Output 'Git hooks are now active for this clone.' +} + +function Sync-ClaudeSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck + ) + + $commandsRoot = Get-MirrorRootPath -TargetName 'Claude' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + Write-Output '' + Write-Output '== Claude Code skill mirror ==' + + foreach ($dir in $SkillDirs) { + $name = $dir.Name + $group = $dir.Parent.Name + $groupDir = Join-Path $commandsRoot $group + $targetMd = Join-Path $groupDir "$name.md" + $targetRes = Join-Path $groupDir $name + + if ($OnlyIfMissing -and (Test-Path $targetMd)) { + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail 'present - skipped (-IfMissing)')) + continue + } + + $resources = Get-SkillResources -SkillDir $dir.FullName + $resRelPaths = $resources | ForEach-Object { + (Get-RelativePathText -BasePath $dir.FullName -TargetPath $_.FullName).Replace('\\', '/') + } + + $body = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + if ($resRelPaths) { + $body = Convert-ResourceLinks -Body $body -Name $name -ResourceRelPaths $resRelPaths + } + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetMd -ExpectedSkill $body -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetRes + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail $state)) + continue + } + + if (Test-Path $targetMd) { Remove-Item $targetMd -Force } + if (Test-Path $targetRes) { Remove-Item $targetRes -Recurse -Force } + New-Item -ItemType Directory -Path $groupDir -Force | Out-Null + Set-Content -Path $targetMd -Value $body -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetRes + + $entries.Add((Get-SyncReportEntry -Name ("/{0}:{1}" -f $group, $name) -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + Write-SyncReport -Flavor 'Claude skills' -TargetPath $commandsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CodexSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $agentsRoot = Get-MirrorRootPath -TargetName 'Codex' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + Remove-Item $agentsRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Codex skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $codexName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($codexName) + $targetDir = Join-Path $agentsRoot $codexName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CodexSkill -SourceDocument $sourceDocument -CodexName $codexName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $codexName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { + foreach ($dir in Get-ChildItem $agentsRoot -Directory) { + if (-not $expectedSkillNames.Contains($dir.Name)) { + $driftCount++ + $entries.Add((Get-SyncReportEntry -Name $dir.Name -Detail 'EXTRA')) + } + } + } + + Write-SyncReport -Flavor 'Codex skills' -TargetPath $agentsRoot -Entries $entries -Scope $ScopeName + if ($ReadOnlyCheck -and $driftCount) { + $script:LastSyncExitCode = 1 + } +} + +function Sync-CopilotSkillsInternal { + param( + [System.IO.DirectoryInfo[]]$SkillDirs, + [string]$ResolvedRepoRoot, + [string]$ScopeName, + [switch]$OnlyIfMissing, + [switch]$ReadOnlyCheck, + [string]$SelectedSkill + ) + + $skillsMirrorRoot = Get-MirrorRootPath -TargetName 'Copilot' -ScopeName $ScopeName -ResolvedRepoRoot $ResolvedRepoRoot + $entries = [System.Collections.Generic.List[object]]::new() + $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() + $driftCount = 0 + $script:LastSyncExitCode = 0 + + if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + Remove-Item $skillsMirrorRoot -Recurse -Force + } + + Write-Output '' + Write-Output '== Copilot skill mirror ==' + + foreach ($dir in $SkillDirs) { + $sourceName = $dir.Name + $group = $dir.Parent.Name + $copilotSkillName = "$group`_$sourceName" + [void]$expectedSkillNames.Add($copilotSkillName) + $targetDir = Join-Path $skillsMirrorRoot $copilotSkillName + $targetSkill = Join-Path $targetDir 'SKILL.md' + + if ($OnlyIfMissing -and (Test-Path $targetSkill)) { + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail 'present - skipped (-IfMissing)')) + continue + } + + $sourceRelPath = "ai-artifacts/skills/shared/$group/$sourceName/SKILL.md" + $sourceDocument = Get-Content (Join-Path $dir.FullName 'SKILL.md') -Raw + $expectedSkill = ConvertTo-CopilotSkill -SourceDocument $sourceDocument -SkillName $copilotSkillName -SourceRelPath $sourceRelPath + $resources = Get-SkillResources -SkillDir $dir.FullName + + if ($ReadOnlyCheck) { + $state = Get-SkillMirrorState -TargetSkillPath $targetSkill -ExpectedSkill $expectedSkill -Resources $resources -SourceBase $dir.FullName -TargetResourceBase $targetDir -ExcludeTargetSkillFile + if ($state -ne 'UP-TO-DATE') { $driftCount++ } + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail $state)) + continue + } + + if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline + Copy-MirroredResources -Resources $resources -SourceBase $dir.FullName -TargetBase $targetDir + + $entries.Add((Get-SyncReportEntry -Name $copilotSkillName -Detail ("{0,2} resource file(s)" -f @($resources).Count))) + } + + if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $skillsMirrorRoot)) { + foreach ($dir in Get-ChildItem $skillsMirrorRoot -Directory) { + $skillFile = Join-Path $dir.FullName 'SKILL.md' + if (-not (Test-Path $skillFile)) { continue } + if ((Get-Content $skillFile -Raw) -notmatch '" - '' - ) -join "`n" - - return $frontmatter + $body -} - -function Get-CodexCompatibilityWarnings { - param( - [string]$Document, - [string]$SkillName - ) - - $patterns = [ordered]@{ - 'claude-path' = '\.claude|CLAUDE_PROJECT_DIR' - 'claude-command' = '(^|\s)/[A-Za-z0-9_-]+:[A-Za-z0-9_-]+|\$ARGUMENTS|!command' - 'codex-case' = '\.Codex' - 'old-skills-root' = 'skills//|(? $commandsRoot (target=Claude, scope=$ClaudeScope)" - Write-Output $report.ToString().TrimEnd() - if ($driftCount) { - Write-Output "STALE MIRROR: $driftCount Claude skill(s) stale or missing - rebuild with: pwsh scripts/sync-skills.ps1 -Target Claude" - $script:LastSyncExitCode = 1 - return - } - Write-Output "Claude skills up to date." - return - } - - Write-Output "Synced Claude skills -> $commandsRoot (scope=$ClaudeScope)" - Write-Output $report.ToString().TrimEnd() - return -} - -function Sync-CodexSkills { - param( - [System.IO.DirectoryInfo[]]$SkillDirs, - [string]$Root, - [string]$SelectedSkill, - [switch]$OnlyIfMissing, - [switch]$ReadOnlyCheck - ) - - $agentsRoot = Join-Path $RepoRoot '.agents\skills' - $report = [System.Text.StringBuilder]::new() - $warnings = [System.Collections.Generic.List[string]]::new() - $expectedSkillNames = [System.Collections.Generic.HashSet[string]]::new() - $driftCount = 0 - $script:LastSyncExitCode = 0 - - if (-not $ReadOnlyCheck -and -not $OnlyIfMissing -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { - Remove-Item $agentsRoot -Recurse -Force - } - - foreach ($dir in $SkillDirs) { - $sourceName = $dir.Name - $group = $dir.Parent.Name - $codexName = "$group-$sourceName" - [void]$expectedSkillNames.Add($codexName) - $targetDir = Join-Path $agentsRoot $codexName - $targetSkill = Join-Path $targetDir 'SKILL.md' - - if ($OnlyIfMissing -and (Test-Path $targetSkill)) { - [void]$report.AppendLine((" {0,-36} present - skipped (-IfMissing)" -f $codexName)) - continue - } - - $sourceSkill = Join-Path $dir.FullName 'SKILL.md' - $sourceRelPath = "shared/skills/$group/$sourceName/SKILL.md" - $sourceDocument = Get-Content $sourceSkill -Raw - $expectedSkill = ConvertTo-CodexSkill -SourceDocument $sourceDocument -CodexName $codexName -SourceRelPath $sourceRelPath - foreach ($warning in Get-CodexCompatibilityWarnings -Document $sourceDocument -SkillName $codexName) { - $warnings.Add($warning) - } - - $resources = @(Get-ChildItem $dir.FullName -Recurse -File | - Where-Object { $_.Name -notin @('SKILL.md', 'METADATA.md') }) - - if ($ReadOnlyCheck) { - $state = 'UP-TO-DATE' - if (-not (Test-Path $targetSkill)) { $state = 'MISSING' } - elseif ((Get-Content $targetSkill -Raw) -cne $expectedSkill) { $state = 'STALE' } - else { - foreach ($res in $resources) { - $rel = $res.FullName.Substring($dir.FullName.Length).TrimStart('\') - $dest = Join-Path $targetDir $rel - if (-not (Test-Path $dest) -or - (Get-FileHash $res.FullName).Hash -ne (Get-FileHash $dest).Hash) { - $state = 'STALE' - break - } - } - if ($state -eq 'UP-TO-DATE') { - $mirrored = @(Get-ChildItem $targetDir -Recurse -File | Where-Object { $_.Name -ne 'SKILL.md' }).Count - if ($mirrored -ne $resources.Count) { $state = 'STALE' } - } - } - - if ($state -ne 'UP-TO-DATE') { $driftCount++ } - [void]$report.AppendLine((" {0,-36} {1}" -f $codexName, $state)) - continue - } - - if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force } - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - Set-Content -Path $targetSkill -Value $expectedSkill -Encoding utf8 -NoNewline - - foreach ($res in $resources) { - $rel = $res.FullName.Substring($dir.FullName.Length).TrimStart('\') - $dest = Join-Path $targetDir $rel - New-Item -ItemType Directory -Path (Split-Path $dest -Parent) -Force | Out-Null - Copy-Item $res.FullName $dest -Force - } - - [void]$report.AppendLine((" {0,-36} {1} resource file(s)" -f $codexName, $resources.Count)) - } - - if ($ReadOnlyCheck -and -not $SelectedSkill -and (Test-Path $agentsRoot)) { - foreach ($dir in Get-ChildItem $agentsRoot -Directory) { - if (-not $expectedSkillNames.Contains($dir.Name)) { - $driftCount++ - [void]$report.AppendLine((" {0,-36} EXTRA" -f $dir.Name)) - } - } - } - - if ($ReadOnlyCheck) { - Write-Output "Drift check: $Root -> $agentsRoot (target=Codex)" - Write-Output $report.ToString().TrimEnd() - if ($warnings.Count) { - Write-Output "Compatibility warnings:" - $warnings | Sort-Object -Unique | ForEach-Object { Write-Output " $_" } - } - if ($driftCount) { - Write-Output "STALE MIRROR: $driftCount Codex skill(s) stale or missing - rebuild with: pwsh scripts/sync-skills.ps1 -Target Codex" - $script:LastSyncExitCode = 1 - return - } - Write-Output "Codex skills up to date." - return - } - - Write-Output "Synced Codex skills -> $agentsRoot" - Write-Output $report.ToString().TrimEnd() - if ($warnings.Count) { - Write-Output "Compatibility warnings:" - $warnings | Sort-Object -Unique | ForEach-Object { Write-Output " $_" } - } - return -} - -$skillDirs = Get-SkillDirectories -Root $skillsRoot -Name $Skill -$exitCodes = [System.Collections.Generic.List[int]]::new() - -if ($Target -in @('All', 'Claude')) { - Write-Output "== Claude Code skill mirror ==" - Sync-ClaudeSkills -SkillDirs $skillDirs -Root $skillsRoot -ClaudeScope $Scope -OnlyIfMissing:$IfMissing -ReadOnlyCheck:$Check - $exitCodes.Add($script:LastSyncExitCode) -} - -if ($Target -in @('All', 'Codex')) { - Write-Output "== Codex skill mirror ==" - Sync-CodexSkills -SkillDirs $skillDirs -Root $skillsRoot -SelectedSkill $Skill -OnlyIfMissing:$IfMissing -ReadOnlyCheck:$Check - $exitCodes.Add($script:LastSyncExitCode) -} - -if ($exitCodes | Where-Object { $_ -ne 0 }) { exit 1 } diff --git a/shared/skills/planning/scratch/RANKING.md b/shared/skills/planning/scratch/RANKING.md deleted file mode 100644 index a7ea4c2..0000000 --- a/shared/skills/planning/scratch/RANKING.md +++ /dev/null @@ -1,51 +0,0 @@ -# .scratch Ranking — Formula & Tiebreakers - -## Score formula - -``` -Score = P × I × E -``` - -| Axis | Value | Numeric | -|------|-------|---------| -| **P** (priority) | high | 3 | -| | medium | 2 | -| | low | 1 | -| **I** (importance) | high | 3 | -| | medium | 2 | -| | low | 1 | -| **E** (effort, **inverted** — less effort ranks higher) | 4h | 7 | -| | 1day | 6 | -| | 2days | 5 | -| | 1week | 4 | -| | 2weeks | 3 | -| | 1month | 2 | -| | 2months | 1 | - -Score range: 3 (low / low / 2months) — 63 (high / high / 4h). -Higher score = higher in the backlog. - -## Tiebreakers (equal score, applied in order) - -1. Less effort (lower E_label wins — quick wins first) -2. Higher importance -3. Higher priority -4. Alphabetical by feature slug - -## Escalation rule (used by `/planning:scratch-plan`) - -When a feature's rank should be raised: -- If `importance < high` → raise importance one level. -- If `importance = high` → raise priority one level instead (if `priority < high`). - -This prevents phantom "super-high" rankings by routing excess urgency into priority. - -## Example - -| Feature | P | I | E | Score | -|---------|---|---|---|-------| -| auth-refactor | high (3) | high (3) | 1week (4) | 36 | -| fix-flaky-test | medium (2) | high (3) | 4h (7) | 42 | -| docs-overhaul | low (1) | medium (2) | 2months (1) | 2 | - -Ranked: fix-flaky-test (42) > auth-refactor (36) > docs-overhaul (2).