@@ -134,3 +134,374 @@ function Test-DirHasFiles {
134134 return $false
135135 }
136136}
137+
138+ # Resolve repository root. Prefer git information when available, but fall back
139+ # to searching for repository markers so the workflow still functions in repositories that
140+ # were initialised with --no-git.
141+ function Find-RepositoryRoot {
142+ param (
143+ [string ]$StartDir ,
144+ [string []]$Markers = @ (' .git' , ' .specify' )
145+ )
146+ $current = Resolve-Path $StartDir
147+ while ($true ) {
148+ foreach ($marker in $Markers ) {
149+ if (Test-Path (Join-Path $current $marker )) {
150+ return $current
151+ }
152+ }
153+ $parent = Split-Path $current - Parent
154+ if ($parent -eq $current ) {
155+ # Reached filesystem root without finding markers
156+ return $null
157+ }
158+ $current = $parent
159+ }
160+ }
161+
162+ function Write-Info {
163+ param (
164+ [Parameter (Mandatory = $true )]
165+ [string ]$Message
166+ )
167+ Write-Host " INFO: $Message "
168+ }
169+
170+ function Write-Success {
171+ param (
172+ [Parameter (Mandatory = $true )]
173+ [string ]$Message
174+ )
175+ Write-Host " $ ( [char ]0x2713 ) $Message "
176+ }
177+
178+ function Write-WarningMsg {
179+ param (
180+ [Parameter (Mandatory = $true )]
181+ [string ]$Message
182+ )
183+ Write-Warning $Message
184+ }
185+
186+ function Write-Err {
187+ param (
188+ [Parameter (Mandatory = $true )]
189+ [string ]$Message
190+ )
191+ Write-Host " ERROR: $Message " - ForegroundColor Red
192+ }
193+
194+ function Validate-Environment {
195+ if (-not $CURRENT_BRANCH ) {
196+ Write-Err ' Unable to determine current feature'
197+ if ($HAS_GIT ) { Write-Info " Make sure you're on a feature branch" } else { Write-Info ' Set SPECIFY_FEATURE environment variable or create a feature first' }
198+ exit 1
199+ }
200+ if (-not (Test-Path $NEW_PLAN )) {
201+ Write-Err " No plan.md found at $NEW_PLAN "
202+ Write-Info ' Ensure you are working on a feature with a corresponding spec directory'
203+ if (-not $HAS_GIT ) { Write-Info ' Use: $env:SPECIFY_FEATURE=your-feature-name or create a new feature first' }
204+ exit 1
205+ }
206+ if (-not (Test-Path $TEMPLATE_FILE )) {
207+ Write-Err " Template file not found at $TEMPLATE_FILE "
208+ Write-Info ' Run specify init to scaffold .specify/templates, or add agent-file-template.md there.'
209+ exit 1
210+ }
211+ }
212+
213+ function Extract-PlanField {
214+ param (
215+ [Parameter (Mandatory = $true )]
216+ [string ]$FieldPattern ,
217+ [Parameter (Mandatory = $true )]
218+ [string ]$PlanFile
219+ )
220+ if (-not (Test-Path $PlanFile )) { return ' ' }
221+ # Lines like **Language/Version**: Python 3.12
222+ $regex = " ^\*\*$ ( [Regex ]::Escape($FieldPattern )) \*\*: (.+)$"
223+ Get-Content - LiteralPath $PlanFile - Encoding utf8 | ForEach-Object {
224+ if ($_ -match $regex ) {
225+ $val = $Matches [1 ].Trim()
226+ if ($val -notin @ (' NEEDS CLARIFICATION' , ' N/A' )) { return $val }
227+ }
228+ } | Select-Object - First 1
229+ }
230+
231+ function Parse-PlanData {
232+ param (
233+ [Parameter (Mandatory = $true )]
234+ [string ]$PlanFile
235+ )
236+ if (-not (Test-Path $PlanFile )) { Write-Err " Plan file not found: $PlanFile " ; return $false }
237+ Write-Info " Parsing plan data from $PlanFile "
238+ $script :NEW_LANG = Extract- PlanField - FieldPattern ' Language/Version' - PlanFile $PlanFile
239+ $script :NEW_FRAMEWORK = Extract- PlanField - FieldPattern ' Primary Dependencies' - PlanFile $PlanFile
240+ $script :NEW_DB = Extract- PlanField - FieldPattern ' Storage' - PlanFile $PlanFile
241+ $script :NEW_PROJECT_TYPE = Extract- PlanField - FieldPattern ' Project Type' - PlanFile $PlanFile
242+
243+ if ($NEW_LANG ) { Write-Info " Found language: $NEW_LANG " } else { Write-WarningMsg ' No language information found in plan' }
244+ if ($NEW_FRAMEWORK ) { Write-Info " Found framework: $NEW_FRAMEWORK " }
245+ if ($NEW_DB -and $NEW_DB -ne ' N/A' ) { Write-Info " Found database: $NEW_DB " }
246+ if ($NEW_PROJECT_TYPE ) { Write-Info " Found project type: $NEW_PROJECT_TYPE " }
247+ return $true
248+ }
249+
250+ function Format-TechnologyStack {
251+ param (
252+ [Parameter (Mandatory = $false )]
253+ [string ]$Lang ,
254+ [Parameter (Mandatory = $false )]
255+ [string ]$Framework
256+ )
257+ $parts = @ ()
258+ if ($Lang -and $Lang -ne ' NEEDS CLARIFICATION' ) { $parts += $Lang }
259+ if ($Framework -and $Framework -notin @ (' NEEDS CLARIFICATION' , ' N/A' )) { $parts += $Framework }
260+ if (-not $parts ) { return ' ' }
261+ return ($parts -join ' + ' )
262+ }
263+
264+ function Get-ProjectStructure {
265+ param (
266+ [Parameter (Mandatory = $false )]
267+ [string ]$ProjectType
268+ )
269+ if ($ProjectType -match ' web' ) { return " backend/`n frontend/`n tests/" } else { return " src/`n tests/" }
270+ }
271+
272+ function Get-CommandsForLanguage {
273+ param (
274+ [Parameter (Mandatory = $false )]
275+ [string ]$Lang
276+ )
277+ switch - Regex ($Lang ) {
278+ ' Python' { return ' cd src; pytest; ruff check .' }
279+ ' Rust' { return ' cargo test; cargo clippy' }
280+ ' JavaScript|TypeScript' { return ' npm test; npm run lint' }
281+ default { return " # Add commands for $Lang " }
282+ }
283+ }
284+
285+ function Get-LanguageConventions {
286+ param (
287+ [Parameter (Mandatory = $false )]
288+ [string ]$Lang
289+ )
290+ if ($Lang ) { " ${Lang} : Follow standard conventions" } else { ' General: Follow standard conventions' }
291+ }
292+
293+ function New-AgentFile {
294+ param (
295+ [Parameter (Mandatory = $true )]
296+ [string ]$TargetFile ,
297+ [Parameter (Mandatory = $true )]
298+ [string ]$ProjectName ,
299+ [Parameter (Mandatory = $true )]
300+ [datetime ]$Date
301+ )
302+ if (-not (Test-Path $TEMPLATE_FILE )) { Write-Err " Template not found at $TEMPLATE_FILE " ; return $false }
303+ $temp = New-TemporaryFile
304+ Copy-Item - LiteralPath $TEMPLATE_FILE - Destination $temp - Force
305+
306+ $projectStructure = Get-ProjectStructure - ProjectType $NEW_PROJECT_TYPE
307+ $commands = Get-CommandsForLanguage - Lang $NEW_LANG
308+ $languageConventions = Get-LanguageConventions - Lang $NEW_LANG
309+
310+ $escaped_lang = $NEW_LANG
311+ $escaped_framework = $NEW_FRAMEWORK
312+ $escaped_branch = $CURRENT_BRANCH
313+
314+ $content = Get-Content - LiteralPath $temp - Raw - Encoding utf8
315+ $content = $content -replace ' \[PROJECT NAME\]' , $ProjectName
316+ $content = $content -replace ' \[DATE\]' , $Date.ToString (' yyyy-MM-dd' )
317+
318+ # Build the technology stack string safely
319+ $techStackForTemplate = ' '
320+ if ($escaped_lang -and $escaped_framework ) {
321+ $techStackForTemplate = " - $escaped_lang + $escaped_framework ($escaped_branch )"
322+ } elseif ($escaped_lang ) {
323+ $techStackForTemplate = " - $escaped_lang ($escaped_branch )"
324+ } elseif ($escaped_framework ) {
325+ $techStackForTemplate = " - $escaped_framework ($escaped_branch )"
326+ }
327+
328+ $content = $content -replace ' \[EXTRACTED FROM ALL PLAN.MD FILES\]' , $techStackForTemplate
329+ # For project structure we manually embed (keep newlines)
330+ $escapedStructure = [Regex ]::Escape($projectStructure )
331+ $content = $content -replace ' \[ACTUAL STRUCTURE FROM PLANS\]' , $escapedStructure
332+ # Replace escaped newlines placeholder after all replacements
333+ $content = $content -replace ' \[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]' , $commands
334+ $content = $content -replace ' \[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]' , $languageConventions
335+
336+ # Build the recent changes string safely
337+ $recentChangesForTemplate = ' '
338+ if ($escaped_lang -and $escaped_framework ) {
339+ $recentChangesForTemplate = " - ${escaped_branch} : Added ${escaped_lang} + ${escaped_framework} "
340+ } elseif ($escaped_lang ) {
341+ $recentChangesForTemplate = " - ${escaped_branch} : Added ${escaped_lang} "
342+ } elseif ($escaped_framework ) {
343+ $recentChangesForTemplate = " - ${escaped_branch} : Added ${escaped_framework} "
344+ }
345+
346+ $content = $content -replace ' \[LAST 3 FEATURES AND WHAT THEY ADDED\]' , $recentChangesForTemplate
347+ # Convert literal \n sequences introduced by Escape to real newlines
348+ $content = $content -replace ' \\n' , [Environment ]::NewLine
349+
350+ $parent = Split-Path - Parent $TargetFile
351+ if (-not (Test-Path $parent )) { New-Item - ItemType Directory - Path $parent | Out-Null }
352+ Set-Content - LiteralPath $TargetFile - Value $content - NoNewline - Encoding utf8
353+ Remove-Item $temp - Force
354+ return $true
355+ }
356+
357+ function Update-ExistingAgentFile {
358+ param (
359+ [Parameter (Mandatory = $true )]
360+ [string ]$TargetFile ,
361+ [Parameter (Mandatory = $true )]
362+ [datetime ]$Date
363+ )
364+ if (-not (Test-Path $TargetFile )) { return (New-AgentFile - TargetFile $TargetFile - ProjectName (Split-Path $REPO_ROOT - Leaf) - Date $Date ) }
365+
366+ $techStack = Format-TechnologyStack - Lang $NEW_LANG - Framework $NEW_FRAMEWORK
367+ $newTechEntries = @ ()
368+ if ($techStack ) {
369+ $escapedTechStack = [Regex ]::Escape($techStack )
370+ if (-not (Select-String - Pattern $escapedTechStack - Path $TargetFile - Quiet)) {
371+ $newTechEntries += " - $techStack ($CURRENT_BRANCH )"
372+ }
373+ }
374+ if ($NEW_DB -and $NEW_DB -notin @ (' N/A' , ' NEEDS CLARIFICATION' )) {
375+ $escapedDB = [Regex ]::Escape($NEW_DB )
376+ if (-not (Select-String - Pattern $escapedDB - Path $TargetFile - Quiet)) {
377+ $newTechEntries += " - $NEW_DB ($CURRENT_BRANCH )"
378+ }
379+ }
380+ $newChangeEntry = ' '
381+ if ($techStack ) { $newChangeEntry = " - ${CURRENT_BRANCH} : Added ${techStack} " }
382+ elseif ($NEW_DB -and $NEW_DB -notin @ (' N/A' , ' NEEDS CLARIFICATION' )) { $newChangeEntry = " - ${CURRENT_BRANCH} : Added ${NEW_DB} " }
383+
384+ $lines = Get-Content - LiteralPath $TargetFile - Encoding utf8
385+ $output = New-Object System.Collections.Generic.List[string ]
386+ $inTech = $false ; $inChanges = $false ; $techAdded = $false ; $changeAdded = $false ; $existingChanges = 0
387+
388+ for ($i = 0 ; $i -lt $lines.Count ; $i ++ ) {
389+ $line = $lines [$i ]
390+ if ($line -eq ' ## Active Technologies' ) {
391+ $output.Add ($line )
392+ $inTech = $true
393+ continue
394+ }
395+ if ($inTech -and $line -match ' ^##\s' ) {
396+ if (-not $techAdded -and $newTechEntries.Count -gt 0 ) { $newTechEntries | ForEach-Object { $output.Add ($_ ) }; $techAdded = $true }
397+ $output.Add ($line ); $inTech = $false ; continue
398+ }
399+ if ($inTech -and [string ]::IsNullOrWhiteSpace($line )) {
400+ if (-not $techAdded -and $newTechEntries.Count -gt 0 ) { $newTechEntries | ForEach-Object { $output.Add ($_ ) }; $techAdded = $true }
401+ $output.Add ($line ); continue
402+ }
403+ if ($line -eq ' ## Recent Changes' ) {
404+ $output.Add ($line )
405+ if ($newChangeEntry ) { $output.Add ($newChangeEntry ); $changeAdded = $true }
406+ $inChanges = $true
407+ continue
408+ }
409+ if ($inChanges -and $line -match ' ^##\s' ) { $output.Add ($line ); $inChanges = $false ; continue }
410+ if ($inChanges -and $line -match ' ^- ' ) {
411+ if ($existingChanges -lt 2 ) { $output.Add ($line ); $existingChanges ++ }
412+ continue
413+ }
414+ if ($line -match ' \*\*Last updated\*\*: .*\d{4}-\d{2}-\d{2}' ) {
415+ $output.Add (($line -replace ' \d{4}-\d{2}-\d{2}' , $Date.ToString (' yyyy-MM-dd' )))
416+ continue
417+ }
418+ $output.Add ($line )
419+ }
420+
421+ # Post-loop check: if we're still in the Active Technologies section and haven't added new entries
422+ if ($inTech -and -not $techAdded -and $newTechEntries.Count -gt 0 ) {
423+ $newTechEntries | ForEach-Object { $output.Add ($_ ) }
424+ }
425+
426+ Set-Content - LiteralPath $TargetFile - Value ($output -join [Environment ]::NewLine) - Encoding utf8
427+ return $true
428+ }
429+
430+ function Update-AgentFile {
431+ param (
432+ [Parameter (Mandatory = $true )]
433+ [string ]$TargetFile ,
434+ [Parameter (Mandatory = $true )]
435+ [string ]$AgentName
436+ )
437+ if (-not $TargetFile -or -not $AgentName ) { Write-Err ' Update-AgentFile requires TargetFile and AgentName' ; return $false }
438+ Write-Info " Updating $AgentName context file: $TargetFile "
439+ $projectName = Split-Path $REPO_ROOT - Leaf
440+ $date = Get-Date
441+
442+ $dir = Split-Path - Parent $TargetFile
443+ if (-not (Test-Path $dir )) { New-Item - ItemType Directory - Path $dir | Out-Null }
444+
445+ if (-not (Test-Path $TargetFile )) {
446+ if (New-AgentFile - TargetFile $TargetFile - ProjectName $projectName - Date $date ) { Write-Success " Created new $AgentName context file" } else { Write-Err ' Failed to create new agent file' ; return $false }
447+ } else {
448+ try {
449+ if (Update-ExistingAgentFile - TargetFile $TargetFile - Date $date ) { Write-Success " Updated existing $AgentName context file" } else { Write-Err ' Failed to update agent file' ; return $false }
450+ } catch {
451+ Write-Err " Cannot access or update existing file: $TargetFile . $_ "
452+ return $false
453+ }
454+ }
455+ return $true
456+ }
457+
458+ function Update-SpecificAgent {
459+ param (
460+ [Parameter (Mandatory = $true )]
461+ [string ]$Type
462+ )
463+ switch ($Type ) {
464+ ' claude' { Update-AgentFile - TargetFile $CLAUDE_FILE - AgentName ' Claude Code' }
465+ ' gemini' { Update-AgentFile - TargetFile $GEMINI_FILE - AgentName ' Gemini CLI' }
466+ ' copilot' { Update-AgentFile - TargetFile $COPILOT_FILE - AgentName ' GitHub Copilot' }
467+ ' cursor' { Update-AgentFile - TargetFile $CURSOR_FILE - AgentName ' Cursor IDE' }
468+ ' qwen' { Update-AgentFile - TargetFile $QWEN_FILE - AgentName ' Qwen Code' }
469+ ' opencode' { Update-AgentFile - TargetFile $AGENTS_FILE - AgentName ' opencode' }
470+ ' codex' { Update-AgentFile - TargetFile $AGENTS_FILE - AgentName ' Codex CLI' }
471+ ' windsurf' { Update-AgentFile - TargetFile $WINDSURF_FILE - AgentName ' Windsurf' }
472+ ' kilocode' { Update-AgentFile - TargetFile $KILOCODE_FILE - AgentName ' Kilo Code' }
473+ ' auggie' { Update-AgentFile - TargetFile $AUGGIE_FILE - AgentName ' Auggie CLI' }
474+ ' roo' { Update-AgentFile - TargetFile $ROO_FILE - AgentName ' Roo Code' }
475+ default { Write-Err " Unknown agent type '$Type '" ; Write-Err ' Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo' ; return $false }
476+ }
477+ }
478+
479+ function Update-AllExistingAgents {
480+ $found = $false
481+ $ok = $true
482+ if (Test-Path $CLAUDE_FILE ) { if (-not (Update-AgentFile - TargetFile $CLAUDE_FILE - AgentName ' Claude Code' )) { $ok = $false }; $found = $true }
483+ if (Test-Path $GEMINI_FILE ) { if (-not (Update-AgentFile - TargetFile $GEMINI_FILE - AgentName ' Gemini CLI' )) { $ok = $false }; $found = $true }
484+ if (Test-Path $COPILOT_FILE ) { if (-not (Update-AgentFile - TargetFile $COPILOT_FILE - AgentName ' GitHub Copilot' )) { $ok = $false }; $found = $true }
485+ if (Test-Path $CURSOR_FILE ) { if (-not (Update-AgentFile - TargetFile $CURSOR_FILE - AgentName ' Cursor IDE' )) { $ok = $false }; $found = $true }
486+ if (Test-Path $QWEN_FILE ) { if (-not (Update-AgentFile - TargetFile $QWEN_FILE - AgentName ' Qwen Code' )) { $ok = $false }; $found = $true }
487+ if (Test-Path $AGENTS_FILE ) { if (-not (Update-AgentFile - TargetFile $AGENTS_FILE - AgentName ' Codex/opencode' )) { $ok = $false }; $found = $true }
488+ if (Test-Path $WINDSURF_FILE ) { if (-not (Update-AgentFile - TargetFile $WINDSURF_FILE - AgentName ' Windsurf' )) { $ok = $false }; $found = $true }
489+ if (Test-Path $KILOCODE_FILE ) { if (-not (Update-AgentFile - TargetFile $KILOCODE_FILE - AgentName ' Kilo Code' )) { $ok = $false }; $found = $true }
490+ if (Test-Path $AUGGIE_FILE ) { if (-not (Update-AgentFile - TargetFile $AUGGIE_FILE - AgentName ' Auggie CLI' )) { $ok = $false }; $found = $true }
491+ if (Test-Path $ROO_FILE ) { if (-not (Update-AgentFile - TargetFile $ROO_FILE - AgentName ' Roo Code' )) { $ok = $false }; $found = $true }
492+ if (-not $found ) {
493+ Write-Info ' No existing agent files found, creating default Claude file...'
494+ if (-not (Update-AgentFile - TargetFile $CLAUDE_FILE - AgentName ' Claude Code' )) { $ok = $false }
495+ }
496+ return $ok
497+ }
498+
499+ function Show-Summary {
500+ Write-Host ' '
501+ Write-Info ' Summary of changes:'
502+ if ($NEW_LANG ) { Write-Host " - Added language: $NEW_LANG " }
503+ if ($NEW_FRAMEWORK ) { Write-Host " - Added framework: $NEW_FRAMEWORK " }
504+ if ($NEW_DB -and $NEW_DB -ne ' N/A' ) { Write-Host " - Added database: $NEW_DB " }
505+ Write-Host ' '
506+ Write-Info ' Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo]'
507+ }
0 commit comments