Skip to content

Commit 77cf88b

Browse files
amweissclaude
andcommitted
feat: add timestamp-based branch naming option for specify init
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d2559d7 commit 77cf88b

File tree

9 files changed

+453
-56
lines changed

9 files changed

+453
-56
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ The `specify` command supports the following options:
222222
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
223223
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
224224
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`) |
225+
| `--branch-numbering` | Option | Branch numbering strategy: `sequential` (default — `001`, `002`, `003`) or `timestamp` (`YYYYMMDD-HHMMSS`). Timestamp mode is useful for distributed teams to avoid numbering conflicts |
225226

226227
### Examples
227228

@@ -296,6 +297,9 @@ specify init my-project --ai claude --ai-skills
296297
# Initialize in current directory with agent skills
297298
specify init --here --ai gemini --ai-skills
298299

300+
# Use timestamp-based branch numbering (useful for distributed teams)
301+
specify init my-project --ai claude --branch-numbering timestamp
302+
299303
# Check system requirements
300304
specify check
301305
```

scripts/bash/common.sh

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,27 @@ get_current_branch() {
3333
if [[ -d "$specs_dir" ]]; then
3434
local latest_feature=""
3535
local highest=0
36+
local latest_timestamp=""
3637

3738
for dir in "$specs_dir"/*; do
3839
if [[ -d "$dir" ]]; then
3940
local dirname=$(basename "$dir")
40-
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
41+
if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
42+
# Timestamp-based branch: compare lexicographically
43+
local ts="${BASH_REMATCH[1]}"
44+
if [[ "$ts" > "$latest_timestamp" ]]; then
45+
latest_timestamp="$ts"
46+
latest_feature=$dirname
47+
fi
48+
elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then
4149
local number=${BASH_REMATCH[1]}
4250
number=$((10#$number))
4351
if [[ "$number" -gt "$highest" ]]; then
4452
highest=$number
45-
latest_feature=$dirname
53+
# Only update if no timestamp branch found yet
54+
if [[ -z "$latest_timestamp" ]]; then
55+
latest_feature=$dirname
56+
fi
4657
fi
4758
fi
4859
fi
@@ -72,9 +83,9 @@ check_feature_branch() {
7283
return 0
7384
fi
7485

75-
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
86+
if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
7687
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
77-
echo "Feature branches should be named like: 001-feature-name" >&2
88+
echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2
7889
return 1
7990
fi
8091

@@ -90,15 +101,18 @@ find_feature_dir_by_prefix() {
90101
local branch_name="$2"
91102
local specs_dir="$repo_root/specs"
92103

93-
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
94-
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
95-
# If branch doesn't have numeric prefix, fall back to exact match
104+
# Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)
105+
local prefix=""
106+
if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
107+
prefix="${BASH_REMATCH[1]}"
108+
elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then
109+
prefix="${BASH_REMATCH[1]}"
110+
else
111+
# If branch doesn't have a recognized prefix, fall back to exact match
96112
echo "$specs_dir/$branch_name"
97113
return
98114
fi
99115

100-
local prefix="${BASH_REMATCH[1]}"
101-
102116
# Search for directories in specs/ that start with this prefix
103117
local matches=()
104118
if [[ -d "$specs_dir" ]]; then

scripts/bash/create-new-feature.sh

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ set -e
55
JSON_MODE=false
66
SHORT_NAME=""
77
BRANCH_NUMBER=""
8+
USE_TIMESTAMP=false
89
ARGS=()
910
i=1
1011
while [ $i -le $# ]; do
1112
arg="${!i}"
1213
case "$arg" in
13-
--json)
14-
JSON_MODE=true
14+
--json)
15+
JSON_MODE=true
1516
;;
1617
--short-name)
1718
if [ $((i + 1)) -gt $# ]; then
@@ -40,22 +41,27 @@ while [ $i -le $# ]; do
4041
fi
4142
BRANCH_NUMBER="$next_arg"
4243
;;
43-
--help|-h)
44-
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
44+
--timestamp)
45+
USE_TIMESTAMP=true
46+
;;
47+
--help|-h)
48+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
4549
echo ""
4650
echo "Options:"
4751
echo " --json Output in JSON format"
4852
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
4953
echo " --number N Specify branch number manually (overrides auto-detection)"
54+
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
5055
echo " --help, -h Show this help message"
5156
echo ""
5257
echo "Examples:"
5358
echo " $0 'Add user authentication system' --short-name 'user-auth'"
5459
echo " $0 'Implement OAuth2 integration for API' --number 5"
60+
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
5561
exit 0
5662
;;
57-
*)
58-
ARGS+=("$arg")
63+
*)
64+
ARGS+=("$arg")
5965
;;
6066
esac
6167
i=$((i + 1))
@@ -96,10 +102,13 @@ get_highest_from_specs() {
96102
for dir in "$specs_dir"/*; do
97103
[ -d "$dir" ] || continue
98104
dirname=$(basename "$dir")
99-
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
100-
number=$((10#$number))
101-
if [ "$number" -gt "$highest" ]; then
102-
highest=$number
105+
# Only match sequential prefixes (###-*), skip timestamp dirs
106+
if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then
107+
number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')
108+
number=$((10#$number))
109+
if [ "$number" -gt "$highest" ]; then
110+
highest=$number
111+
fi
103112
fi
104113
done
105114
fi
@@ -242,29 +251,42 @@ else
242251
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
243252
fi
244253

245-
# Determine branch number
246-
if [ -z "$BRANCH_NUMBER" ]; then
247-
if [ "$HAS_GIT" = true ]; then
248-
# Check existing branches on remotes
249-
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
250-
else
251-
# Fall back to local directory check
252-
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
253-
BRANCH_NUMBER=$((HIGHEST + 1))
254-
fi
254+
# Warn if --number and --timestamp are both specified
255+
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
256+
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
257+
BRANCH_NUMBER=""
255258
fi
256259

257-
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
258-
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
259-
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
260+
# Determine branch prefix
261+
if [ "$USE_TIMESTAMP" = true ]; then
262+
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
263+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
264+
else
265+
# Determine branch number
266+
if [ -z "$BRANCH_NUMBER" ]; then
267+
if [ "$HAS_GIT" = true ]; then
268+
# Check existing branches on remotes
269+
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
270+
else
271+
# Fall back to local directory check
272+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
273+
BRANCH_NUMBER=$((HIGHEST + 1))
274+
fi
275+
fi
276+
277+
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
278+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
279+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
280+
fi
260281

261282
# GitHub enforces a 244-byte limit on branch names
262283
# Validate and truncate if necessary
263284
MAX_BRANCH_LENGTH=244
264285
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
265286
# Calculate how much we need to trim from suffix
266-
# Account for: feature number (3) + hyphen (1) = 4 chars
267-
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
287+
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
288+
PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))
289+
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
268290

269291
# Truncate suffix at word boundary if possible
270292
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)

scripts/powershell/common.ps1

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,28 @@ function Get-CurrentBranch {
3838
if (Test-Path $specsDir) {
3939
$latestFeature = ""
4040
$highest = 0
41-
41+
$latestTimestamp = ""
42+
4243
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
43-
if ($_.Name -match '^(\d{3})-') {
44+
if ($_.Name -match '^(\d{8}-\d{6})-') {
45+
# Timestamp-based branch: compare lexicographically
46+
$ts = $matches[1]
47+
if ($ts -gt $latestTimestamp) {
48+
$latestTimestamp = $ts
49+
$latestFeature = $_.Name
50+
}
51+
} elseif ($_.Name -match '^(\d{3})-') {
4452
$num = [int]$matches[1]
4553
if ($num -gt $highest) {
4654
$highest = $num
47-
$latestFeature = $_.Name
55+
# Only update if no timestamp branch found yet
56+
if (-not $latestTimestamp) {
57+
$latestFeature = $_.Name
58+
}
4859
}
4960
}
5061
}
51-
62+
5263
if ($latestFeature) {
5364
return $latestFeature
5465
}
@@ -79,9 +90,9 @@ function Test-FeatureBranch {
7990
return $true
8091
}
8192

82-
if ($Branch -notmatch '^[0-9]{3}-') {
93+
if ($Branch -notmatch '^[0-9]{3}-' -and $Branch -notmatch '^\d{8}-\d{6}-') {
8394
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
84-
Write-Output "Feature branches should be named like: 001-feature-name"
95+
Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name"
8596
return $false
8697
}
8798
return $true

scripts/powershell/create-new-feature.ps1

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ param(
66
[string]$ShortName,
77
[Parameter()]
88
[int]$Number = 0,
9+
[switch]$Timestamp,
910
[switch]$Help,
1011
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
1112
[string[]]$FeatureDescription
@@ -14,17 +15,19 @@ $ErrorActionPreference = 'Stop'
1415

1516
# Show help if requested
1617
if ($Help) {
17-
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
18+
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
1819
Write-Host ""
1920
Write-Host "Options:"
2021
Write-Host " -Json Output in JSON format"
2122
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
2223
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
24+
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
2325
Write-Host " -Help Show this help message"
2426
Write-Host ""
2527
Write-Host "Examples:"
2628
Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'"
2729
Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'"
30+
Write-Host " ./create-new-feature.ps1 -Timestamp -ShortName 'user-auth' 'Add user authentication'"
2831
exit 0
2932
}
3033

@@ -72,7 +75,7 @@ function Get-HighestNumberFromSpecs {
7275
$highest = 0
7376
if (Test-Path $SpecsDir) {
7477
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
75-
if ($_.Name -match '^(\d+)') {
78+
if ($_.Name -match '^(\d{3})-') {
7679
$num = [int]$matches[1]
7780
if ($num -gt $highest) { $highest = $num }
7881
}
@@ -216,27 +219,40 @@ if ($ShortName) {
216219
$branchSuffix = Get-BranchName -Description $featureDesc
217220
}
218221

219-
# Determine branch number
220-
if ($Number -eq 0) {
221-
if ($hasGit) {
222-
# Check existing branches on remotes
223-
$Number = Get-NextBranchNumber -SpecsDir $specsDir
224-
} else {
225-
# Fall back to local directory check
226-
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
227-
}
222+
# Warn if -Number and -Timestamp are both specified
223+
if ($Timestamp -and $Number -ne 0) {
224+
Write-Warning "[specify] Warning: -Number is ignored when -Timestamp is used"
225+
$Number = 0
228226
}
229227

230-
$featureNum = ('{0:000}' -f $Number)
231-
$branchName = "$featureNum-$branchSuffix"
228+
# Determine branch prefix
229+
if ($Timestamp) {
230+
$featureNum = Get-Date -Format 'yyyyMMdd-HHmmss'
231+
$branchName = "$featureNum-$branchSuffix"
232+
} else {
233+
# Determine branch number
234+
if ($Number -eq 0) {
235+
if ($hasGit) {
236+
# Check existing branches on remotes
237+
$Number = Get-NextBranchNumber -SpecsDir $specsDir
238+
} else {
239+
# Fall back to local directory check
240+
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
241+
}
242+
}
243+
244+
$featureNum = ('{0:000}' -f $Number)
245+
$branchName = "$featureNum-$branchSuffix"
246+
}
232247

233248
# GitHub enforces a 244-byte limit on branch names
234249
# Validate and truncate if necessary
235250
$maxBranchLength = 244
236251
if ($branchName.Length -gt $maxBranchLength) {
237252
# Calculate how much we need to trim from suffix
238-
# Account for: feature number (3) + hyphen (1) = 4 chars
239-
$maxSuffixLength = $maxBranchLength - 4
253+
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
254+
$prefixLength = $featureNum.Length + 1
255+
$maxSuffixLength = $maxBranchLength - $prefixLength
240256

241257
# Truncate suffix
242258
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))

src/specify_cli/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,6 +1479,7 @@ def init(
14791479
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
14801480
ai_skills: bool = typer.Option(False, "--ai-skills", help="Install Prompt.MD templates as agent skills (requires --ai)"),
14811481
preset: str = typer.Option(None, "--preset", help="Install a preset during initialization (by preset ID)"),
1482+
branch_numbering: str = typer.Option(None, "--branch-numbering", help="Branch numbering strategy: 'sequential' (001, 002, ...) or 'timestamp' (YYYYMMDD-HHMMSS)"),
14821483
):
14831484
"""
14841485
Initialize a new Specify project from the latest template.
@@ -1546,6 +1547,11 @@ def init(
15461547
console.print("[yellow]Usage:[/yellow] specify init <project> --ai <agent> --ai-skills")
15471548
raise typer.Exit(1)
15481549

1550+
BRANCH_NUMBERING_CHOICES = {"sequential", "timestamp"}
1551+
if branch_numbering and branch_numbering not in BRANCH_NUMBERING_CHOICES:
1552+
console.print(f"[red]Error:[/red] Invalid --branch-numbering value '{branch_numbering}'. Choose from: {', '.join(sorted(BRANCH_NUMBERING_CHOICES))}")
1553+
raise typer.Exit(1)
1554+
15491555
if here:
15501556
project_name = Path.cwd().name
15511557
project_path = Path.cwd()
@@ -1781,6 +1787,7 @@ def init(
17811787
"ai": selected_ai,
17821788
"ai_skills": ai_skills,
17831789
"ai_commands_dir": ai_commands_dir,
1790+
"branch_numbering": branch_numbering or "sequential",
17841791
"here": here,
17851792
"preset": preset,
17861793
"script": selected_script,

templates/commands/specify.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,16 @@ Given that feature description, do this:
7373
- "Create a dashboard for analytics" → "analytics-dashboard"
7474
- "Fix payment processing timeout bug" → "fix-payment-timeout"
7575
76-
2. **Create the feature branch** by running the script with `--short-name` (and `--json`), and do NOT pass `--number` (the script auto-detects the next globally available number across all branches and spec directories):
76+
2. **Create the feature branch** by running the script with `--short-name` (and `--json`). In sequential mode, do NOT pass `--number` — the script auto-detects the next available number. In timestamp mode, the script generates a `YYYYMMDD-HHMMSS` prefix automatically:
77+
78+
**Branch numbering mode**: Before running the script, check if `.specify/init-options.json` exists and read the `branch_numbering` value.
79+
- If `"timestamp"`, add `--timestamp` (Bash) or `-Timestamp` (PowerShell) to the script invocation
80+
- If `"sequential"` or absent, do not add any extra flag (default behavior)
7781
7882
- Bash example: `{SCRIPT} --json --short-name "user-auth" "Add user authentication"`
83+
- Bash (timestamp): `{SCRIPT} --json --timestamp --short-name "user-auth" "Add user authentication"`
7984
- PowerShell example: `{SCRIPT} -Json -ShortName "user-auth" "Add user authentication"`
85+
- PowerShell (timestamp): `{SCRIPT} -Json -Timestamp -ShortName "user-auth" "Add user authentication"`
8086
8187
**IMPORTANT**:
8288
- Do NOT pass `--number` — the script determines the correct next number automatically

0 commit comments

Comments
 (0)