specpro-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. specpro_cli/__init__.py +16 -0
  2. specpro_cli/assets/commands/specpro.analyze.md +1102 -0
  3. specpro_cli/assets/commands/specpro.checklist.md +335 -0
  4. specpro_cli/assets/commands/specpro.clarify.md +581 -0
  5. specpro_cli/assets/commands/specpro.constitution.md +488 -0
  6. specpro_cli/assets/commands/specpro.feature.md +115 -0
  7. specpro_cli/assets/commands/specpro.implement.md +1881 -0
  8. specpro_cli/assets/commands/specpro.manual-test.md +206 -0
  9. specpro_cli/assets/commands/specpro.plan.md +3284 -0
  10. specpro_cli/assets/commands/specpro.qc.md +1489 -0
  11. specpro_cli/assets/commands/specpro.scenarios.md +154 -0
  12. specpro_cli/assets/commands/specpro.specify.md +1449 -0
  13. specpro_cli/assets/commands/specpro.status.md +863 -0
  14. specpro_cli/assets/commands/specpro.tasks.md +1207 -0
  15. specpro_cli/assets/commands/specpro.test-implement.md +462 -0
  16. specpro_cli/assets/commands/specpro.test-plan.md +383 -0
  17. specpro_cli/assets/commands/specpro.user-manual.md +178 -0
  18. specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
  19. specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
  20. specpro_cli/assets/scripts/bash/common.sh +88 -0
  21. specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
  22. specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
  23. specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
  24. specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
  25. specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
  26. specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
  27. specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
  28. specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
  29. specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
  30. specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
  31. specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
  32. specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
  33. specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
  34. specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
  35. specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
  36. specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
  37. specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
  38. specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
  39. specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
  40. specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
  41. specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
  42. specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
  43. specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
  44. specpro_cli/assets/templates/agent-context-template.md +49 -0
  45. specpro_cli/assets/templates/assumptions-template.md +248 -0
  46. specpro_cli/assets/templates/checklist-template.md +40 -0
  47. specpro_cli/assets/templates/clarifications-template.md +155 -0
  48. specpro_cli/assets/templates/constitution-template.md +50 -0
  49. specpro_cli/assets/templates/feature-spec-template.md +66 -0
  50. specpro_cli/assets/templates/plan-overview-template.md +150 -0
  51. specpro_cli/assets/templates/plan-template.md +387 -0
  52. specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
  53. specpro_cli/assets/templates/requirements-template.md +356 -0
  54. specpro_cli/assets/templates/spec-template.md +267 -0
  55. specpro_cli/assets/templates/tasks-template.md +252 -0
  56. specpro_cli/assets/templates/test-tasks-template.md +174 -0
  57. specpro_cli/cli/__init__.py +5 -0
  58. specpro_cli/cli/cmd_init.py +416 -0
  59. specpro_cli/cli/cmd_remove.py +122 -0
  60. specpro_cli/cli/entry.py +181 -0
  61. specpro_cli/integrations/__init__.py +36 -0
  62. specpro_cli/integrations/base.py +601 -0
  63. specpro_cli/integrations/claude/__init__.py +101 -0
  64. specpro_cli/integrations/copilot/__init__.py +153 -0
  65. specpro_cli/integrations/cursor_agent/__init__.py +51 -0
  66. specpro_cli/integrations/gemini/__init__.py +44 -0
  67. specpro_cli/integrations/opencode/__init__.py +48 -0
  68. specpro_cli/integrations/qodercli/__init__.py +54 -0
  69. specpro_cli/integrations/registry.py +88 -0
  70. specpro_cli/packaged/__init__.py +5 -0
  71. specpro_cli/packaged/sync.py +106 -0
  72. specpro_cli-0.1.0.dist-info/METADATA +117 -0
  73. specpro_cli-0.1.0.dist-info/RECORD +76 -0
  74. specpro_cli-0.1.0.dist-info/WHEEL +4 -0
  75. specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
  76. specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ Consolidated prerequisite checking for the Spec-Driven Development workflow.
5
+ .DESCRIPTION
6
+ Contract-equivalent to `scripts/bash/check-prerequisites.sh`: the same options, the
7
+ same two output modes, and the same exit codes.
8
+
9
+ ⚠️ Arguments are parsed with a manual loop rather than a `param()` block, and that
10
+ is deliberate: callers are command documents that invoke the Bash side as
11
+ `check-prerequisites.sh --json`, and a `param()` block would reject `--json`
12
+ (PowerShell binds `-Json`). Accepting both spellings keeps one call site valid on
13
+ both platforms — the alternative is two call conventions that will drift.
14
+
15
+ .EXAMPLE
16
+ ./check-prerequisites.ps1 --json --require-tasks --include-tasks
17
+ ./check-prerequisites.ps1 --paths-only
18
+ #>
19
+ Set-StrictMode -Version Latest
20
+ $ErrorActionPreference = 'Stop'
21
+
22
+ . (Join-Path $PSScriptRoot 'common.ps1')
23
+
24
+ $jsonMode = $false
25
+ $requireTasks = $false
26
+ $includeTasks = $false
27
+ $pathsOnly = $false
28
+
29
+ foreach ($arg in $args) {
30
+ switch -Regex ($arg) {
31
+ '^--?json$' { $jsonMode = $true; continue }
32
+ '^--?require-tasks$' { $requireTasks = $true; continue }
33
+ '^--?include-tasks$' { $includeTasks = $true; continue }
34
+ '^--?paths-only$' { $pathsOnly = $true; continue }
35
+ '^--?help$|^-h$' {
36
+ # ⚠️ Reproduces the Bash help text byte-for-byte EXCEPT the script's own
37
+ # name: a usage message that named the other platform's script would send
38
+ # the reader looking for a file that is not there. Script name is the ONE
39
+ # intentional difference; the ` ` (two-space) lines below are not blanks
40
+ # by accident — they are what the Bash heredoc emits.
41
+ @'
42
+ Usage: check-prerequisites.ps1 [OPTIONS]
43
+
44
+ Consolidated prerequisite checking for Spec-Driven Development workflow.
45
+
46
+ OPTIONS:
47
+ --json Output in JSON format
48
+ --require-tasks Require tasks.md to exist (for implementation phase)
49
+ --include-tasks Include tasks.md in AVAILABLE_DOCS list
50
+ --paths-only Only output path variables (no prerequisite validation)
51
+ --help, -h Show this help message
52
+
53
+ EXAMPLES:
54
+ # Check task prerequisites (plan.md required)
55
+ ./check-prerequisites.ps1 --json
56
+
57
+ # Check implementation prerequisites (plan.md + tasks.md required)
58
+ ./check-prerequisites.ps1 --json --require-tasks --include-tasks
59
+
60
+ # Get feature paths only (no validation)
61
+ ./check-prerequisites.ps1 --paths-only
62
+
63
+ '@ | Write-Output
64
+ exit 0
65
+ }
66
+ default {
67
+ [Console]::Error.WriteLine("ERROR: Unknown option '$arg'. Use --help for usage information.")
68
+ exit 1
69
+ }
70
+ }
71
+ }
72
+
73
+ $paths = Get-FeaturePaths
74
+ $featureDir = $paths.FEATURE_DIR
75
+ $implPlan = $paths.IMPL_PLAN
76
+ $featureSpec = $paths.FEATURE_SPEC
77
+ $tasks = $paths.TASKS
78
+
79
+ # --- paths-only: emit and exit (no validation) ----------------------------------------
80
+ if ($pathsOnly) {
81
+ if ($jsonMode) {
82
+ # Minimal JSON paths payload. Hand-built to keep the key order and the exact
83
+ # byte shape the Bash side prints.
84
+ # ⚠️ Literal braces in a `-f` format string MUST be doubled: a bare `{` opens a
85
+ # format item. The Bash side needs no escape, which is why the two expressions
86
+ # look alike and are not — the parity check is what catches a missed doubling.
87
+ $json = '{{"REPO_ROOT":"{0}","BRANCH":"{1}","FEATURE_SCOPE":"{2}","FEATURE_DIR":"{3}","FEATURE_SPEC":"{4}","IMPL_PLAN":"{5}","TASKS":"{6}"}}' -f `
88
+ $paths.REPO_ROOT, $paths.CURRENT_BRANCH, $paths.FEATURE_SCOPE, $paths.FEATURE_DIR, $paths.FEATURE_SPEC, $paths.IMPL_PLAN, $paths.TASKS
89
+ Write-Output $json
90
+ } else {
91
+ Write-Output "REPO_ROOT: $($paths.REPO_ROOT)"
92
+ Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
93
+ Write-Output "FEATURE_SCOPE: $($paths.FEATURE_SCOPE)"
94
+ Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
95
+ Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
96
+ Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
97
+ Write-Output "TASKS: $($paths.TASKS)"
98
+ }
99
+ exit 0
100
+ }
101
+
102
+ # --- validation ------------------------------------------------------------------------
103
+ if (-not (Test-Path -LiteralPath $featureDir -PathType Container)) {
104
+ [Console]::Error.WriteLine("ERROR: Feature directory not found: $featureDir")
105
+ [Console]::Error.WriteLine('Run /specpro-specify first to create the feature structure.')
106
+ exit 1
107
+ }
108
+
109
+ if (-not (Test-Path -LiteralPath $implPlan -PathType Leaf)) {
110
+ [Console]::Error.WriteLine("ERROR: plan.md not found in $featureDir")
111
+ [Console]::Error.WriteLine('Run /specpro-plan first to create the implementation plan.')
112
+ exit 1
113
+ }
114
+
115
+ if ($requireTasks -and -not (Test-Path -LiteralPath $tasks -PathType Leaf)) {
116
+ [Console]::Error.WriteLine("ERROR: tasks.md not found in $featureDir")
117
+ [Console]::Error.WriteLine('Run /specpro-tasks first to create the task list.')
118
+ exit 1
119
+ }
120
+
121
+ # --- available documents ---------------------------------------------------------------
122
+ $docs = [System.Collections.Generic.List[string]]::new()
123
+ if (Test-Path -LiteralPath $featureSpec -PathType Leaf) { $docs.Add('spec.md') }
124
+ if (Test-Path -LiteralPath $paths.RESEARCH -PathType Leaf) { $docs.Add('research.md') }
125
+ if (Test-Path -LiteralPath $paths.DATA_MODEL -PathType Leaf) { $docs.Add('data-model.md') }
126
+ if ((Test-Path -LiteralPath $paths.CONTRACTS_DIR -PathType Container) -and
127
+ ((Get-ChildItem -LiteralPath $paths.CONTRACTS_DIR -Force | Measure-Object).Count -gt 0)) {
128
+ $docs.Add('contracts/')
129
+ }
130
+ if (Test-Path -LiteralPath $paths.QUICKSTART -PathType Leaf) { $docs.Add('quickstart.md') }
131
+ if ($includeTasks -and (Test-Path -LiteralPath $tasks -PathType Leaf)) { $docs.Add('tasks.md') }
132
+ # Optional high-level test plan — never required.
133
+ if (Test-Path -LiteralPath $paths.TEST_TASKS -PathType Leaf) { $docs.Add('test-tasks.md') }
134
+
135
+ if ($jsonMode) {
136
+ $jsonDocs = '[' + (($docs | ForEach-Object { '"' + $_ + '"' }) -join ',') + ']'
137
+ Write-Output ('{{"FEATURE_DIR":"{0}","FEATURE_SCOPE":"{1}","AVAILABLE_DOCS":{2}}}' -f $featureDir, $paths.FEATURE_SCOPE, $jsonDocs)
138
+ } else {
139
+ Write-Output "FEATURE_DIR:$featureDir"
140
+ Write-Output 'AVAILABLE_DOCS:'
141
+ Write-CheckFile $featureSpec 'spec.md'
142
+ Write-CheckFile $paths.RESEARCH 'research.md'
143
+ Write-CheckFile $paths.DATA_MODEL 'data-model.md'
144
+ Write-CheckDir $paths.CONTRACTS_DIR 'contracts/'
145
+ Write-CheckFile $paths.QUICKSTART 'quickstart.md'
146
+ if ($includeTasks) { Write-CheckFile $tasks 'tasks.md' }
147
+ Write-CheckFile $paths.TEST_TASKS 'test-tasks.md'
148
+ }
@@ -0,0 +1,95 @@
1
+ # Common functions for all specpro support scripts (PowerShell).
2
+ #
3
+ # Contract-equivalent to `scripts/bash/common.sh`. "Contract" here means what callers
4
+ # depend on: the same three queries, the same returned path set, and the same fallbacks
5
+ # — NOT the same function names (PowerShell casing is the convention on this side) and
6
+ # not the same implementation.
7
+ #
8
+ # Dot-source this file; it defines functions and has no side effects.
9
+
10
+ Set-StrictMode -Version Latest
11
+
12
+ # Repository root, with a fallback for non-git repositories.
13
+ function Get-RepoRoot {
14
+ if (Test-HasGit) {
15
+ # `git rev-parse --show-toplevel` already returns an absolute, forward-slash path.
16
+ $root = & git rev-parse --show-toplevel 2>$null
17
+ if ($LASTEXITCODE -eq 0 -and $root) { return $root.Trim() }
18
+ }
19
+ # Fall back to the script location: <repo>/scripts/powershell/common.ps1 -> <repo>
20
+ return (Resolve-Path (Join-Path $PSScriptRoot '..' '..')).Path
21
+ }
22
+
23
+ # Current branch — informational only. Branch information is NOT used for directory
24
+ # structure; every spec file lives directly under specs/.
25
+ function Get-CurrentBranch {
26
+ if (Test-HasGit) {
27
+ $branch = & git rev-parse --abbrev-ref HEAD 2>$null
28
+ if ($LASTEXITCODE -eq 0 -and $branch) { return $branch.Trim() }
29
+ }
30
+ return 'unknown'
31
+ }
32
+
33
+ # Is git available and is this a repository?
34
+ function Test-HasGit {
35
+ & git rev-parse --show-toplevel *> $null
36
+ return ($LASTEXITCODE -eq 0)
37
+ }
38
+
39
+ # Every path a caller needs. Mirrors `get_feature_paths` one key at a time: a caller
40
+ # that reads a key the Bash side emits must find it here too.
41
+ function Get-FeaturePaths {
42
+ $repoRoot = Get-RepoRoot
43
+ $specsDir = Join-Path $repoRoot 'specs'
44
+ # ── Feature-scope detection (FR-063 / T050 · T255, v0.23) ──
45
+ # cwd inside specs/fNNN-简称/ ⇒ feature scope: spec/plan/tasks/test-tasks resolve
46
+ # to THAT directory; constitution/research stay mother-side (living-reference
47
+ # inheritance, FR-014). Greppable anchors: 「feature 作用域」/「母作用域」.
48
+ # FEATURE_SCOPE is an ASCII value ('true'/'false') — the same decision that keeps
49
+ # the Bash side safe through eval word-splitting.
50
+ $scopeDir = [System.IO.Path]::GetFullPath((Get-Location).ProviderPath)
51
+ $inFeature = 'false'
52
+ if ($scopeDir -match '/specs/f[0-9][0-9][0-9]-') { $inFeature = 'true' }
53
+ if ($inFeature -eq 'true') {
54
+ $fd = $scopeDir
55
+ $implan = Join-Path $scopeDir 'plan.md'
56
+ $tasksPath = Join-Path $scopeDir 'tasks.md'
57
+ $tt = Join-Path $scopeDir 'test-tasks.md'
58
+ } else {
59
+ $fd = $specsDir
60
+ $implan = Join-Path $specsDir 'plan.md'
61
+ $tasksPath = Join-Path $specsDir 'tasks.md'
62
+ $tt = Join-Path $specsDir 'test-tasks.md'
63
+ }
64
+ [ordered]@{
65
+ REPO_ROOT = $repoRoot
66
+ CURRENT_BRANCH = Get-CurrentBranch
67
+ HAS_GIT = if (Test-HasGit) { 'true' } else { 'false' }
68
+ FEATURE_SCOPE = $inFeature
69
+ FEATURE_DIR = $fd
70
+ FEATURE_SPEC = Join-Path $fd 'spec.md'
71
+ IMPL_PLAN = $implan
72
+ TASKS = $tasksPath
73
+ RESEARCH = Join-Path $specsDir 'research.md'
74
+ DATA_MODEL = Join-Path $specsDir 'data-model.md'
75
+ QUICKSTART = Join-Path $specsDir 'quickstart.md'
76
+ TEST_TASKS = $tt
77
+ CONTRACTS_DIR = Join-Path $fd 'contracts'
78
+ }
79
+ }
80
+
81
+ # The two status lines the text output mode prints. Same glyphs and same leading
82
+ # spacing as the Bash side — these are read by humans comparing the two outputs.
83
+ function Write-CheckFile {
84
+ param([string]$Path, [string]$Label)
85
+ if (Test-Path -LiteralPath $Path -PathType Leaf) { Write-Output " ✓ $Label" }
86
+ else { Write-Output " ✗ $Label" }
87
+ }
88
+
89
+ function Write-CheckDir {
90
+ param([string]$Path, [string]$Label)
91
+ $hasContent = (Test-Path -LiteralPath $Path -PathType Container) -and
92
+ ((Get-ChildItem -LiteralPath $Path -Force | Measure-Object).Count -gt 0)
93
+ if ($hasContent) { Write-Output " ✓ $Label" }
94
+ else { Write-Output " ✗ $Label" }
95
+ }
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ Ensure specs/spec.md exists, seeding it from the template, and report the feature number.
5
+ .DESCRIPTION
6
+ Contract-equivalent to `scripts/bash/create-new-feature.sh`.
7
+
8
+ ⚠️ Despite that script's filename and its own header calling it legacy, what it
9
+ actually does is the **current** layout: it writes `specs/spec.md` directly and
10
+ creates no branch subdirectory. Its header describes what it used to do. The port
11
+ follows the behaviour, which is what the callers depend on — see ISS-50.
12
+
13
+ Branch names are computed and reported but **do not direct any directory**; every
14
+ spec file lives directly under specs/.
15
+
16
+ .EXAMPLE
17
+ ./create-new-feature.ps1 --json "Add multi-platform install support"
18
+ ./create-new-feature.ps1 --json --short-name "plugin-arch" --number 7
19
+ #>
20
+ Set-StrictMode -Version Latest
21
+ $ErrorActionPreference = 'Stop'
22
+
23
+ . (Join-Path $PSScriptRoot 'common.ps1')
24
+
25
+ $jsonMode = $false
26
+ $shortName = ''
27
+ $branchNumber = ''
28
+ $positional = @()
29
+
30
+ for ($i = 0; $i -lt $args.Count; $i++) {
31
+ $arg = $args[$i]
32
+ switch -Regex ($arg) {
33
+ '^--?json$' { $jsonMode = $true; continue }
34
+ '^--?short-name$' {
35
+ if ($i + 1 -ge $args.Count -or $args[$i + 1] -like '-*') {
36
+ Write-Error 'Error: --short-name requires a value'
37
+ exit 1
38
+ }
39
+ $shortName = $args[++$i]; continue
40
+ }
41
+ '^--?number$' {
42
+ if ($i + 1 -ge $args.Count -or $args[$i + 1] -like '-*') {
43
+ Write-Error 'Error: --number requires a value'
44
+ exit 1
45
+ }
46
+ $branchNumber = $args[++$i]; continue
47
+ }
48
+ '^--?help$|^-h$' {
49
+ Write-Output 'Usage: create-new-feature.ps1 [OPTIONS] [FEATURE_DESCRIPTION]'
50
+ Write-Output ' --json Output in JSON format'
51
+ Write-Output ' --short-name NAME Use NAME as the branch suffix'
52
+ Write-Output ' --number N Use N as the feature number'
53
+ exit 0
54
+ }
55
+ default { $positional += $arg; continue }
56
+ }
57
+ }
58
+ $featureDescription = ($positional -join ' ')
59
+
60
+ # A description is REQUIRED — the options modify how one is turned into a number and a
61
+ # slug, they do not replace it. Without this check `--short-name x` alone would succeed
62
+ # and produce a branch name with no feature behind it, where the Bash side exits 1.
63
+ if ([string]::IsNullOrEmpty($featureDescription)) {
64
+ # `$PSCommandPath` is the running script — the ONE intentional difference from the
65
+ # Bash side, which prints `$0`. Naming the other platform's file would send the
66
+ # reader looking for something that is not there.
67
+ [Console]::Error.WriteLine("Usage: scripts/powershell/create-new-feature.ps1 [--json] [--short-name <name>] [--number N] <feature_description>")
68
+ exit 1
69
+ }
70
+
71
+ # --- repository root -------------------------------------------------------------------
72
+ # Prefer git; fall back to walking up for a repository marker, so the workflow still
73
+ # functions in repositories initialised with --no-git.
74
+ function Find-RepoRoot {
75
+ param([string]$Start)
76
+ $dir = $Start
77
+ while ($dir -and $dir -ne [System.IO.Path]::GetPathRoot($dir)) {
78
+ if ((Test-Path (Join-Path $dir '.git')) -or (Test-Path (Join-Path $dir '.specpro'))) {
79
+ return $dir
80
+ }
81
+ $dir = Split-Path $dir -Parent
82
+ }
83
+ return $null
84
+ }
85
+
86
+ if (Test-HasGit) {
87
+ $repoRoot = (& git rev-parse --show-toplevel).Trim()
88
+ $hasGit = $true
89
+ } else {
90
+ $repoRoot = Find-RepoRoot $PSScriptRoot
91
+ if (-not $repoRoot) {
92
+ Write-Error 'Error: Could not determine repository root. Please run this script from within the repository.'
93
+ exit 1
94
+ }
95
+ $hasGit = $false
96
+ }
97
+ Set-Location $repoRoot
98
+ $specsDir = Join-Path $repoRoot 'specs'
99
+ New-Item -ItemType Directory -Force -Path $specsDir | Out-Null
100
+
101
+ # --- numbering helpers -----------------------------------------------------------------
102
+
103
+ function Get-HighestFromSpecs {
104
+ param([string]$SpecsDir)
105
+ $highest = 0
106
+ if (Test-Path -LiteralPath $SpecsDir -PathType Container) {
107
+ foreach ($d in Get-ChildItem -LiteralPath $SpecsDir -Directory -Force) {
108
+ if ($d.Name -match '^([0-9]+)') {
109
+ # Base-10 explicitly: a leading zero would otherwise read as octal.
110
+ $n = [int]$Matches[1]
111
+ if ($n -gt $highest) { $highest = $n }
112
+ }
113
+ }
114
+ }
115
+ return $highest
116
+ }
117
+
118
+ function Get-HighestFromBranches {
119
+ $highest = 0
120
+ $branches = & git branch -a 2>$null
121
+ if ($LASTEXITCODE -ne 0 -or -not $branches) { return 0 }
122
+ foreach ($b in $branches) {
123
+ $clean = ($b -replace '^[* ]*', '') -replace '^remotes/[^/]+/', ''
124
+ if ($clean -match '^([0-9]{3})-') {
125
+ $n = [int]$Matches[1]
126
+ if ($n -gt $highest) { $highest = $n }
127
+ }
128
+ }
129
+ return $highest
130
+ }
131
+
132
+ function Get-NextBranchNumber {
133
+ param([string]$SpecsDir)
134
+ & git fetch --all --prune *> $null # failures are expected with no remote
135
+ $fromBranches = Get-HighestFromBranches
136
+ $fromSpecs = Get-HighestFromSpecs $SpecsDir
137
+ return ([Math]::Max($fromBranches, $fromSpecs) + 1)
138
+ }
139
+
140
+ function Get-CleanBranchName {
141
+ param([string]$Name)
142
+ $s = $Name.ToLowerInvariant() -replace '[^a-z0-9]', '-'
143
+ $s = $s -replace '-+', '-'
144
+ return $s.Trim('-')
145
+ }
146
+
147
+ function Get-GeneratedBranchName {
148
+ param([string]$Description)
149
+ $stopWords = '^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$'
150
+
151
+ $words = ($Description.ToLowerInvariant() -replace '[^a-z0-9]', ' ') -split '\s+'
152
+ $meaningful = @()
153
+ foreach ($w in $words) {
154
+ if ([string]::IsNullOrEmpty($w)) { continue }
155
+ if ($w -match $stopWords) { continue }
156
+ if ($w.Length -ge 3) { $meaningful += $w; continue }
157
+ # Short words survive only when the ORIGINAL text has them uppercased — the
158
+ # usual signal for an acronym the description cares about.
159
+ #
160
+ # ⚠️ **This is the one place the two implementations deliberately differ.** The
161
+ # Bash side writes `${word^^}` for the uppercasing, which is a Bash 4.0+ expansion
162
+ # — on the Bash 3.2 that macOS ships it fails with "bad substitution", the grep
163
+ # gets a broken pattern, and the short word is silently dropped. So Bash's output
164
+ # here is not a contract: it differs between Bash 3.2 and Bash 4+ on the same
165
+ # input. This side implements the documented INTENT. See ISS-51.
166
+ if ($Description -cmatch "\b$([regex]::Escape($w.ToUpperInvariant()))\b") { $meaningful += $w }
167
+ }
168
+ if ($meaningful.Count -gt 0) {
169
+ $max = if ($meaningful.Count -eq 4) { 4 } else { 3 }
170
+ $take = $meaningful | Select-Object -First $max
171
+ return ($take -join '-')
172
+ }
173
+ $cleaned = Get-CleanBranchName $Description
174
+ $parts = @($cleaned -split '-' | Where-Object { $_ -ne '' } | Select-Object -First 3)
175
+ return ($parts -join '-')
176
+ }
177
+
178
+ # --- compose the branch name ------------------------------------------------------------
179
+ $branchSuffix = if ($shortName) { Get-CleanBranchName $shortName } else { Get-GeneratedBranchName $featureDescription }
180
+
181
+ if (-not $branchNumber) {
182
+ $branchNumber = if ($hasGit) { Get-NextBranchNumber $specsDir } else { (Get-HighestFromSpecs $specsDir) + 1 }
183
+ }
184
+ $featureNum = '{0:d3}' -f [int]$branchNumber
185
+ $branchName = "$featureNum-$branchSuffix"
186
+
187
+ # GitHub enforces a 244-byte limit on branch names.
188
+ $maxBranchLength = 244
189
+ if ($branchName.Length -gt $maxBranchLength) {
190
+ $maxSuffixLength = $maxBranchLength - 4 # number (3) + hyphen (1)
191
+ $truncated = $branchSuffix.Substring(0, [Math]::Min($maxSuffixLength, $branchSuffix.Length)).TrimEnd('-')
192
+ $original = $branchName
193
+ $branchName = "$featureNum-$truncated"
194
+ [Console]::Error.WriteLine("[specify] Warning: Branch name exceeded GitHub's 244-byte limit")
195
+ [Console]::Error.WriteLine("[specify] Original: $original ($($original.Length) bytes)")
196
+ [Console]::Error.WriteLine("[specify] Truncated to: $branchName ($($branchName.Length) bytes)")
197
+ }
198
+
199
+ # --- produce the spec file --------------------------------------------------------------
200
+ # No Git branch is created for the specification: one project has one spec.md that
201
+ # evolves. Branch information is reported, never used for directory structure.
202
+ $featureDir = $specsDir
203
+ New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
204
+ $specFile = Join-Path $featureDir 'spec.md'
205
+
206
+ if (-not (Test-Path -LiteralPath $specFile -PathType Leaf)) {
207
+ $template = Join-Path $repoRoot '.specpro' 'templates' 'spec-template.md'
208
+ if (Test-Path -LiteralPath $template -PathType Leaf) {
209
+ Copy-Item -LiteralPath $template -Destination $specFile
210
+ [Console]::Error.WriteLine("[specify] Created initial specification: $specFile")
211
+ } else {
212
+ New-Item -ItemType File -Force -Path $specFile | Out-Null
213
+ [Console]::Error.WriteLine("[specify] Created empty specification: $specFile")
214
+ }
215
+ } else {
216
+ [Console]::Error.WriteLine("[specify] Specification already exists: $specFile")
217
+ [Console]::Error.WriteLine('[specify] Use /specpro-specify with update intent to evolve the existing spec')
218
+ }
219
+
220
+ $env:SPECIFY_FEATURE = $branchName
221
+
222
+ if ($jsonMode) {
223
+ Write-Output ('{{"BRANCH_NAME":"{0}","SPEC_FILE":"{1}","FEATURE_NUM":"{2}"}}' -f $branchName, $specFile, $featureNum)
224
+ } else {
225
+ Write-Output "BRANCH_NAME: $branchName"
226
+ Write-Output "SPEC_FILE: $specFile"
227
+ Write-Output "FEATURE_NUM: $featureNum"
228
+ Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
229
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ QC Auto-Fix — the write-back implementation for `/specpro-qc`.
5
+ .DESCRIPTION
6
+ Contract-equivalent to `scripts/bash/qc-auto-fix.sh`. The contract is deliberately
7
+ strong here: **for the same inputs, both implementations print byte-identical output
8
+ and return the same exit code** (FR-008) — this script is invoked from a command
9
+ document, and a pair that disagrees across platforms is worse than one that is merely
10
+ missing, because each side looks authoritative.
11
+
12
+ Why this exists (T186 / ISS-143, settled 2026-09-19): these bodies used to live inside
13
+ `commands/specpro.qc.md` as bash fences. That put an executable implementation in a
14
+ Markdown document, which the landing policy (`plan.md` -> 落点政策 §1) forbids: an
15
+ implementation that lives in prose can only be verified by EXTRACTING it, and that
16
+ extraction depends on the Markdown's shape and returns an empty string — silently —
17
+ the moment the shape changes. T164 verified its own guard exactly that way.
18
+
19
+ What the move must not lose: `apply_fix` is the ONE write-back path, and its value is
20
+ entirely in three guards added the hard way (T164) —
21
+
22
+ 1. the destination is a REQUIRED argument (an omitted target ABORTS, it does not
23
+ guess: a defaulted parameter once delivered the CHECKLIST's content into
24
+ `spec.md`, observed as a 202-byte spec.md replaced by a 100-byte checklist)
25
+ 2. a non-zero transformer status aborts
26
+ 3. an empty result aborts
27
+
28
+ …and then exactly ONE move.
29
+
30
+ .PARAMETER Command
31
+ `apply-fix` — and that is the whole surface. ⚠️ Two more subcommands were removed
32
+ on 2026-09-19 (`ISS-186` / `T220`): the one that rewrote `spec.md` prose, and the
33
+ one that authorised repairing another command's artifact. The ruling made
34
+ `/specpro-qc` a checker that **does not repair** (`FR-058`), so there is nothing
35
+ left for an authorisation guard to guard — see the Bash twin's header for the full
36
+ reasoning, which is kept in one place on purpose.
37
+ .PARAMETER Args
38
+ apply-fix: <tmp> <gen-status> <dest>
39
+ .OUTPUTS
40
+ Exit 0 = the write happened; 1 = aborted, nothing written.
41
+ #>
42
+
43
+ param(
44
+ [Parameter(Position = 0)][string]$Command,
45
+ [Parameter(Position = 1, ValueFromRemainingArguments = $true)][string[]]$Args
46
+ )
47
+
48
+ $ErrorActionPreference = 'Stop'
49
+
50
+ function Write-Usage {
51
+ # ⚠️ The usage names the COMMAND, not this file — same reason as the Bash twin's note.
52
+ [Console]::Error.WriteLine('usage: qc-auto-fix apply-fix <tmp> <gen-status> <dest>')
53
+ }
54
+
55
+ # =======================================================================================
56
+ # Invoke-ApplyFix — the ONE write-back path
57
+ # =======================================================================================
58
+ function Invoke-ApplyFix {
59
+ param([string]$Tmp, [string]$GenStatus, [string]$Target)
60
+
61
+ if ([string]::IsNullOrEmpty($Tmp)) {
62
+ [Console]::Error.WriteLine(' ✗ fix aborted: no result to write — apply_fix needs a source')
63
+ return 1
64
+ }
65
+
66
+ if ([string]::IsNullOrEmpty($Target)) {
67
+ [Console]::Error.WriteLine(' ✗ fix aborted: no destination given — apply_fix does not guess')
68
+ Remove-Item -Force -ErrorAction SilentlyContinue $Tmp
69
+ return 1
70
+ }
71
+
72
+ if ([int]$GenStatus -ne 0) {
73
+ [Console]::Error.WriteLine(" ✗ fix aborted: the transformer exited $GenStatus — $Target left untouched")
74
+ Remove-Item -Force -ErrorAction SilentlyContinue $Tmp
75
+ return 1
76
+ }
77
+
78
+ # An empty result is a failure, not a fix: every caller's output is derived from an
79
+ # input it had to READ, so a zero-byte result means that read failed.
80
+ if ((-not (Test-Path -LiteralPath $Tmp)) -or ((Get-Item -LiteralPath $Tmp).Length -eq 0)) {
81
+ [Console]::Error.WriteLine(" ✗ fix aborted: the result is empty — $Target left untouched")
82
+ Remove-Item -Force -ErrorAction SilentlyContinue $Tmp
83
+ return 1
84
+ }
85
+
86
+ Move-Item -LiteralPath $Tmp -Destination $Target -Force
87
+ return 0
88
+ }
89
+
90
+ # =======================================================================================
91
+ # dispatch
92
+ # =======================================================================================
93
+ # ⚠️ `@($Args)` is NOT empty when no arguments were passed — PowerShell binds one $null
94
+ # element, so `.Count` reads 1 and every arity guard below is skipped. Measured: with the
95
+ # null element left in, `apply-fix` with no arguments fell through to the write-back path
96
+ # and died on an empty path instead of printing the usage the Bash side prints.
97
+ $rest = @($Args | Where-Object { $null -ne $_ -and $_ -ne '' })
98
+ switch ($Command) {
99
+ 'apply-fix' {
100
+ if ($rest.Count -lt 1) { Write-Usage; exit 1 }
101
+ $tmp = $rest[0]
102
+ $gen = if ($rest.Count -ge 2) { $rest[1] } else { '0' }
103
+ $dest = if ($rest.Count -ge 3) { $rest[2] } else { '' }
104
+ exit (Invoke-ApplyFix -Tmp $tmp -GenStatus $gen -Target $dest)
105
+ }
106
+ default {
107
+ Write-Usage
108
+ exit 1
109
+ }
110
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ Prepare the plan artifact for the current project.
5
+ .DESCRIPTION
6
+ Contract-equivalent to `scripts/bash/setup-plan.sh`: ensure specs/ exists, seed
7
+ plan.md from the template when it is absent, and report the paths in either JSON or
8
+ text form.
9
+
10
+ As with the other scripts here, arguments are parsed manually so the `--json`
11
+ spelling used by the Bash side keeps working.
12
+ #>
13
+ Set-StrictMode -Version Latest
14
+ $ErrorActionPreference = 'Stop'
15
+
16
+ . (Join-Path $PSScriptRoot 'common.ps1')
17
+
18
+ $jsonMode = $false
19
+ foreach ($arg in $args) {
20
+ switch -Regex ($arg) {
21
+ '^--?json$' { $jsonMode = $true; continue }
22
+ '^--?help$|^-h$' {
23
+ Write-Output "Usage: setup-plan.ps1 [--json]"
24
+ Write-Output " --json Output results in JSON format"
25
+ Write-Output " --help Show this help message"
26
+ exit 0
27
+ }
28
+ default { continue } # The Bash side collects unknown args and ignores them.
29
+ }
30
+ }
31
+
32
+ $paths = Get-FeaturePaths
33
+ $featureDir = $paths.FEATURE_DIR
34
+ $implPlan = $paths.IMPL_PLAN
35
+
36
+ New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
37
+
38
+ $template = Join-Path $paths.REPO_ROOT '.specpro' 'templates' 'plan-template.md'
39
+
40
+ if (Test-Path -LiteralPath $implPlan -PathType Leaf) {
41
+ Write-Output "Using existing plan at $implPlan"
42
+ } elseif (Test-Path -LiteralPath $template -PathType Leaf) {
43
+ Copy-Item -LiteralPath $template -Destination $implPlan
44
+ Write-Output "Created plan from template at $implPlan"
45
+ } else {
46
+ Write-Output "Warning: Plan template not found at $template"
47
+ # Same fallback as the Bash side: leave a file behind so later steps have something
48
+ # to write into rather than failing on a missing path.
49
+ New-Item -ItemType File -Force -Path $implPlan | Out-Null
50
+ }
51
+
52
+ if ($jsonMode) {
53
+ Write-Output ('{{"FEATURE_SPEC":"{0}","IMPL_PLAN":"{1}","SPECS_DIR":"{2}","BRANCH":"{3}","HAS_GIT":"{4}"}}' -f `
54
+ $paths.FEATURE_SPEC, $implPlan, $featureDir, $paths.CURRENT_BRANCH, $paths.HAS_GIT)
55
+ } else {
56
+ Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
57
+ Write-Output "IMPL_PLAN: $implPlan"
58
+ Write-Output "SPECS_DIR: $featureDir"
59
+ Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
60
+ Write-Output "HAS_GIT: $($paths.HAS_GIT)"
61
+ }